rune-runtime 2.2.0__tar.gz → 2.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rune_runtime-2.2.0/src/rune_runtime.egg-info → rune_runtime-2.2.1}/PKG-INFO +1 -1
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/src/rune/runtime/base_data_class.py +25 -7
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/src/rune/runtime/version.py +3 -3
- {rune_runtime-2.2.0 → rune_runtime-2.2.1/src/rune_runtime.egg-info}/PKG-INFO +1 -1
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/src/rune_runtime.egg-info/SOURCES.txt +1 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/src/rune_runtime.egg-info/scm_file_list.json +1 -0
- rune_runtime-2.2.1/src/rune_runtime.egg-info/scm_version.json +8 -0
- rune_runtime-2.2.1/test/cdm/test_validation.py +46 -0
- rune_runtime-2.2.1/test/local_data/ird-ex01-vanilla-swap-versioned.json +307 -0
- rune_runtime-2.2.1/test/test_deep_keys_and_references.py +247 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/test_deferred_ref_deserialization.py +30 -3
- rune_runtime-2.2.0/src/rune_runtime.egg-info/scm_version.json +0 -8
- rune_runtime-2.2.0/test/cdm/test_validation.py +0 -23
- rune_runtime-2.2.0/test/test_deep_keys_and_references.py +0 -124
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/.github/CODE_OF_CONDUCT.md +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/.github/ISSUE_TEMPLATE/Bug_report.md +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/.github/ISSUE_TEMPLATE/Feature_request.md +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/.github/ISSUE_TEMPLATE/Support_question.md +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/.github/ISSUE_TEMPLATE/meeting-minutes.md +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/.github/workflows/build-python-package.yml +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/.github/workflows/cve-scanning-python.yml +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/.github/workflows/license-scanning-python.yml +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/.github/workflows/release.yml +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/.github/workflows/run-tests.yml +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/.gitignore +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/.vscode/settings.json +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/CONTRIBUTING.md +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/EASYCLA_CHANGEME.md +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/LICENSE +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/LICENSE.spdx +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/MAINTAINERS.md +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/NOTICE +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/README.md +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/build_wheel.sh +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/dev_clean_setup.sh +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/docs/home.md +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/netlify.toml +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/pyproject.toml +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/renovate.json +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/safety-policy.yml +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/setup.cfg +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/src/rune/runtime/__init__.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/src/rune/runtime/conditions.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/src/rune/runtime/cow.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/src/rune/runtime/extensions/__init__.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/src/rune/runtime/extensions/cached_json_rune_object_deserializer.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/src/rune/runtime/func_proxy.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/src/rune/runtime/metadata.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/src/rune/runtime/native_registry.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/src/rune/runtime/object_builder.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/src/rune/runtime/object_registry.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/src/rune/runtime/py.typed +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/src/rune/runtime/utils.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/src/rune_runtime.egg-info/dependency_links.txt +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/src/rune_runtime.egg-info/requires.txt +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/src/rune_runtime.egg-info/top_level.txt +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/__init__.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/cdm/EUR-Vanilla-account.json +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/cdm/test_create_irs.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/cdm/test_trade_creation.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/functions/specs/cdm/_bundle.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/functions/specs/cdm/base/math/Abs.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/functions/specs/cdm/base/math/RoundToNearest.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/functions/specs/cdm/base/math/RoundingModeEnum.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/functions/specs/test_native_registry.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/functions/specs/test_simple.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/run_runtime_tests.sh +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/serializer-round-trip/test_basic.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/serializer-round-trip/test_conditions.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/serializer-round-trip/test_enumtype.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/serializer-round-trip/test_extension.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/serializer-round-trip/test_metakey.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/serializer-round-trip/test_metalocation.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/serializer-round-trip/test_metascheme.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/test_basic_types_with_meta.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/test_cached_json_rune_object_deserializer.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/test_complex_types_with_meta.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/test_cow.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/test_func_proxy.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/test_keys_and_references.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/test_list_cardinality_with_meta.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/test_namespace_prefix.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/test_object_builder.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/test_operations.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/test_parametrized_basic_types.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/test_rune_parent.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/test_rune_str.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/test_subtype_identity.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/test_utils.py +0 -0
- {rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/test_zoned_date_time.py +0 -0
|
@@ -125,6 +125,15 @@ class BaseDataClass(BaseModel, ComplexTypeMetaDataMixin):
|
|
|
125
125
|
return aux
|
|
126
126
|
obj = handler(data)
|
|
127
127
|
obj._init_rune_parent() # pylint: disable=protected-access
|
|
128
|
+
if isinstance(data, dict):
|
|
129
|
+
# Deferred annotations can bypass the field metadata validator.
|
|
130
|
+
# Retain target keys before resolving references to this model.
|
|
131
|
+
keys = {k: v for k, v in metadata.items() if k.startswith('@key')}
|
|
132
|
+
if keys:
|
|
133
|
+
# Field-level validators handle allowed metadata, including
|
|
134
|
+
# scoped keys that are not declared on the target class.
|
|
135
|
+
obj.set_meta(check_allowed=False, **keys)
|
|
136
|
+
obj._register_keys(keys) # pylint: disable=protected-access
|
|
128
137
|
obj.resolve_references(ignore_dangling=True, recurse=False)
|
|
129
138
|
# transfer refs that were established on the original before
|
|
130
139
|
# re-validation wiped them
|
|
@@ -140,9 +149,12 @@ class BaseDataClass(BaseModel, ComplexTypeMetaDataMixin):
|
|
|
140
149
|
self.__dict__[RUNE_OBJ_MAPS] = {}
|
|
141
150
|
|
|
142
151
|
for prop_nm, obj in self.__dict__.items():
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
|
|
152
|
+
if prop_nm.startswith('__') or prop_nm in refs:
|
|
153
|
+
continue
|
|
154
|
+
children = obj if isinstance(obj, (MutableSequence, tuple)) else (obj,)
|
|
155
|
+
for child in children:
|
|
156
|
+
if isinstance(child, BaseMetaDataMixin):
|
|
157
|
+
child._set_rune_parent(self) # pylint: disable=protected-access
|
|
146
158
|
|
|
147
159
|
def rune_serialize(
|
|
148
160
|
self,
|
|
@@ -306,11 +318,17 @@ class BaseDataClass(BaseModel, ComplexTypeMetaDataMixin):
|
|
|
306
318
|
def resolve_references(self, ignore_dangling=False, recurse=True):
|
|
307
319
|
'''resolves all attributes which are references'''
|
|
308
320
|
if recurse:
|
|
321
|
+
resolved_refs = self._get_rune_refs_container()
|
|
309
322
|
for prop_nm, obj in self.__dict__.items():
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
323
|
+
# Follow containment only; resolved references can point back
|
|
324
|
+
# to an ancestor and must not be traversed again.
|
|
325
|
+
if prop_nm.startswith('__') or prop_nm in resolved_refs:
|
|
326
|
+
continue
|
|
327
|
+
children = obj if isinstance(obj, (MutableSequence, tuple)) else (obj,)
|
|
328
|
+
for child in children:
|
|
329
|
+
if isinstance(child, BaseDataClass):
|
|
330
|
+
child.resolve_references(ignore_dangling=ignore_dangling,
|
|
331
|
+
recurse=recurse)
|
|
314
332
|
|
|
315
333
|
refs = []
|
|
316
334
|
for prop_nm, obj in self.__dict__.items():
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '2.2.
|
|
22
|
-
__version_tuple__ = version_tuple = (2, 2,
|
|
21
|
+
__version__ = version = '2.2.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 2, 1)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g05cabe482'
|
|
@@ -75,6 +75,7 @@ test/functions/specs/cdm/_bundle.py
|
|
|
75
75
|
test/functions/specs/cdm/base/math/Abs.py
|
|
76
76
|
test/functions/specs/cdm/base/math/RoundToNearest.py
|
|
77
77
|
test/functions/specs/cdm/base/math/RoundingModeEnum.py
|
|
78
|
+
test/local_data/ird-ex01-vanilla-swap-versioned.json
|
|
78
79
|
test/serializer-round-trip/test_basic.py
|
|
79
80
|
test/serializer-round-trip/test_conditions.py
|
|
80
81
|
test/serializer-round-trip/test_enumtype.py
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"test/functions/specs/cdm/base/math/RoundingModeEnum.py",
|
|
51
51
|
"test/functions/specs/test_native_registry.py",
|
|
52
52
|
"test/functions/specs/test_simple.py",
|
|
53
|
+
"test/local_data/ird-ex01-vanilla-swap-versioned.json",
|
|
53
54
|
"test/run_runtime_tests.sh",
|
|
54
55
|
"test/serializer-round-trip/test_basic.py",
|
|
55
56
|
"test/serializer-round-trip/test_conditions.py",
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'''Full attribute validation - pydantic and constraints'''
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import pytest
|
|
4
|
+
from pydantic import ValidationError
|
|
5
|
+
from rune.runtime.base_data_class import BaseDataClass
|
|
6
|
+
try:
|
|
7
|
+
# pylint: disable=unused-import
|
|
8
|
+
# type: ignore
|
|
9
|
+
from cdm.base.math.NonNegativeQuantity import NonNegativeQuantity
|
|
10
|
+
from cdm.base.math.UnitType import UnitType
|
|
11
|
+
NO_SER_TEST_MOD = False
|
|
12
|
+
except ImportError:
|
|
13
|
+
NO_SER_TEST_MOD = True
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.mark.skipif(NO_SER_TEST_MOD, reason='CDM package not found')
|
|
17
|
+
def test_bad_attrib_validation():
|
|
18
|
+
'''Invalid attribute assigned'''
|
|
19
|
+
unit = UnitType(currency='EUR')
|
|
20
|
+
mq = NonNegativeQuantity(value=10, unit=unit)
|
|
21
|
+
mq.frequency = 'Blah'
|
|
22
|
+
with pytest.raises(ValidationError):
|
|
23
|
+
mq.validate_model()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_vanilla_swap_list_reference_validation():
|
|
27
|
+
'''Validate the reported sample when local data and the CDM bundle exist.'''
|
|
28
|
+
sample = Path(__file__).resolve().parents[1] / 'local_data' / 'ird-ex01-vanilla-swap-versioned.json'
|
|
29
|
+
if not sample.is_file():
|
|
30
|
+
pytest.skip('Local CDM sample not found')
|
|
31
|
+
pytest.importorskip('finos')
|
|
32
|
+
|
|
33
|
+
model = BaseDataClass.rune_deserialize(
|
|
34
|
+
sample.read_text(encoding='utf-8'),
|
|
35
|
+
namespace_prefix='finos',
|
|
36
|
+
check_rune_constraints=False,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
assert len(model.trade.counterparty) == 2
|
|
40
|
+
parties = {party.get_meta('@key:external'): party for party in model.trade.party}
|
|
41
|
+
for counterparty in model.trade.counterparty:
|
|
42
|
+
key = counterparty.resolve_ref_key('partyReference')
|
|
43
|
+
assert counterparty.partyReference is parties[key]
|
|
44
|
+
assert model.validate_model(check_rune_constraints=False) == []
|
|
45
|
+
|
|
46
|
+
# EOF
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
{
|
|
2
|
+
"@model" : "cdm",
|
|
3
|
+
"@type" : "cdm.event.common.TradeState",
|
|
4
|
+
"@version" : "0.0.0.master-SNAPSHOT",
|
|
5
|
+
"@key" : "3afe24c4",
|
|
6
|
+
"trade" : {
|
|
7
|
+
"product" : {
|
|
8
|
+
"taxonomy" : [ {
|
|
9
|
+
"source" : "ISDA",
|
|
10
|
+
"value" : {
|
|
11
|
+
"name" : {
|
|
12
|
+
"@data" : "InterestRate_IRSwap_FixedFloat"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"calculated" : true
|
|
16
|
+
} ],
|
|
17
|
+
"economicTerms" : {
|
|
18
|
+
"payout" : [ {
|
|
19
|
+
"@type" : "cdm.product.asset.InterestRatePayout",
|
|
20
|
+
"payerReceiver" : {
|
|
21
|
+
"payer" : "Party1",
|
|
22
|
+
"receiver" : "Party2"
|
|
23
|
+
},
|
|
24
|
+
"priceQuantity" : {
|
|
25
|
+
"quantitySchedule" : {
|
|
26
|
+
"@ref:scoped" : "quantity-1"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"rateSpecification" : {
|
|
30
|
+
"@type" : "cdm.product.asset.FloatingRateSpecification",
|
|
31
|
+
"rateOption" : {
|
|
32
|
+
"@ref:scoped" : "InterestRateIndex-1"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"dayCountFraction" : {
|
|
36
|
+
"@data" : "ACT/360"
|
|
37
|
+
},
|
|
38
|
+
"calculationPeriodDates" : {
|
|
39
|
+
"@key:external" : "floatingCalcPeriodDates",
|
|
40
|
+
"effectiveDate" : {
|
|
41
|
+
"adjustableDate" : {
|
|
42
|
+
"unadjustedDate" : "1994-12-14",
|
|
43
|
+
"dateAdjustments" : {
|
|
44
|
+
"businessDayConvention" : "NONE"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"terminationDate" : {
|
|
49
|
+
"adjustableDate" : {
|
|
50
|
+
"unadjustedDate" : "1999-12-14",
|
|
51
|
+
"dateAdjustments" : {
|
|
52
|
+
"businessDayConvention" : "MODFOLLOWING"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"calculationPeriodDatesAdjustments" : {
|
|
57
|
+
"businessDayConvention" : "MODFOLLOWING",
|
|
58
|
+
"businessCenters" : {
|
|
59
|
+
"businessCentersReference" : {
|
|
60
|
+
"@ref:external" : "primaryBusinessCenters"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"calculationPeriodFrequency" : {
|
|
65
|
+
"periodMultiplier" : 6,
|
|
66
|
+
"period" : "M",
|
|
67
|
+
"rollConvention" : "14"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"paymentDates" : {
|
|
71
|
+
"paymentFrequency" : {
|
|
72
|
+
"periodMultiplier" : 6,
|
|
73
|
+
"period" : "M"
|
|
74
|
+
},
|
|
75
|
+
"payRelativeTo" : "CalculationPeriodEndDate",
|
|
76
|
+
"paymentDatesAdjustments" : {
|
|
77
|
+
"businessDayConvention" : "MODFOLLOWING",
|
|
78
|
+
"businessCenters" : {
|
|
79
|
+
"businessCentersReference" : {
|
|
80
|
+
"@ref:external" : "primaryBusinessCenters"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"resetDates" : {
|
|
86
|
+
"@key:external" : "resetDates",
|
|
87
|
+
"calculationPeriodDatesReference" : {
|
|
88
|
+
"@ref:external" : "floatingCalcPeriodDates"
|
|
89
|
+
},
|
|
90
|
+
"resetRelativeTo" : "CalculationPeriodStartDate",
|
|
91
|
+
"fixingDates" : {
|
|
92
|
+
"periodMultiplier" : -2,
|
|
93
|
+
"period" : "D",
|
|
94
|
+
"dayType" : "Business",
|
|
95
|
+
"businessDayConvention" : "NONE",
|
|
96
|
+
"dateRelativeTo" : {
|
|
97
|
+
"@ref" : "a0134250",
|
|
98
|
+
"@ref:external" : "resetDates"
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"resetFrequency" : {
|
|
102
|
+
"periodMultiplier" : 6,
|
|
103
|
+
"period" : "M"
|
|
104
|
+
},
|
|
105
|
+
"resetDatesAdjustments" : {
|
|
106
|
+
"businessDayConvention" : "MODFOLLOWING",
|
|
107
|
+
"businessCenters" : {
|
|
108
|
+
"businessCentersReference" : {
|
|
109
|
+
"@ref:external" : "primaryBusinessCenters"
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}, {
|
|
115
|
+
"@type" : "cdm.product.asset.InterestRatePayout",
|
|
116
|
+
"payerReceiver" : {
|
|
117
|
+
"payer" : "Party2",
|
|
118
|
+
"receiver" : "Party1"
|
|
119
|
+
},
|
|
120
|
+
"priceQuantity" : {
|
|
121
|
+
"quantitySchedule" : {
|
|
122
|
+
"@ref:scoped" : "quantity-2"
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
"rateSpecification" : {
|
|
126
|
+
"@type" : "cdm.product.asset.FixedRateSpecification",
|
|
127
|
+
"rateSchedule" : {
|
|
128
|
+
"price" : {
|
|
129
|
+
"@ref:scoped" : "price-1"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
"dayCountFraction" : {
|
|
134
|
+
"@data" : "30E/360"
|
|
135
|
+
},
|
|
136
|
+
"calculationPeriodDates" : {
|
|
137
|
+
"@key:external" : "fixedCalcPeriodDates",
|
|
138
|
+
"effectiveDate" : {
|
|
139
|
+
"adjustableDate" : {
|
|
140
|
+
"unadjustedDate" : "1994-12-14",
|
|
141
|
+
"dateAdjustments" : {
|
|
142
|
+
"businessDayConvention" : "NONE"
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
"terminationDate" : {
|
|
147
|
+
"adjustableDate" : {
|
|
148
|
+
"unadjustedDate" : "1999-12-14",
|
|
149
|
+
"dateAdjustments" : {
|
|
150
|
+
"businessDayConvention" : "MODFOLLOWING",
|
|
151
|
+
"businessCenters" : {
|
|
152
|
+
"businessCentersReference" : {
|
|
153
|
+
"@ref:external" : "primaryBusinessCenters"
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
"calculationPeriodDatesAdjustments" : {
|
|
160
|
+
"businessDayConvention" : "MODFOLLOWING",
|
|
161
|
+
"businessCenters" : {
|
|
162
|
+
"businessCentersReference" : {
|
|
163
|
+
"@ref:external" : "primaryBusinessCenters"
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
"calculationPeriodFrequency" : {
|
|
168
|
+
"periodMultiplier" : 1,
|
|
169
|
+
"period" : "Y",
|
|
170
|
+
"rollConvention" : "14"
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
"paymentDates" : {
|
|
174
|
+
"paymentFrequency" : {
|
|
175
|
+
"periodMultiplier" : 1,
|
|
176
|
+
"period" : "Y"
|
|
177
|
+
},
|
|
178
|
+
"payRelativeTo" : "CalculationPeriodEndDate",
|
|
179
|
+
"paymentDatesAdjustments" : {
|
|
180
|
+
"businessDayConvention" : "MODFOLLOWING",
|
|
181
|
+
"businessCenters" : {
|
|
182
|
+
"businessCentersReference" : {
|
|
183
|
+
"@ref:external" : "primaryBusinessCenters"
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
} ]
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
"tradeLot" : [ {
|
|
192
|
+
"priceQuantity" : [ {
|
|
193
|
+
"quantity" : {
|
|
194
|
+
"@key:scoped" : "quantity-1",
|
|
195
|
+
"value" : 50000000.00,
|
|
196
|
+
"unit" : {
|
|
197
|
+
"currency" : {
|
|
198
|
+
"@scheme" : "http://www.fpml.org/coding-scheme/external/iso4217",
|
|
199
|
+
"@data" : "EUR"
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
"observable" : {
|
|
204
|
+
"@key:scoped" : "observable-1",
|
|
205
|
+
"@data" : {
|
|
206
|
+
"@type" : "cdm.observable.asset.InterestRateIndex",
|
|
207
|
+
"@key:scoped" : "InterestRateIndex-1",
|
|
208
|
+
"@data" : {
|
|
209
|
+
"@type" : "cdm.observable.asset.FloatingRateIndex",
|
|
210
|
+
"identifier" : [ {
|
|
211
|
+
"identifier" : {
|
|
212
|
+
"@data" : "EUR-LIBOR-BBA"
|
|
213
|
+
},
|
|
214
|
+
"identifierType" : "Other"
|
|
215
|
+
} ],
|
|
216
|
+
"assetType" : "Other",
|
|
217
|
+
"assetClass" : "InterestRate",
|
|
218
|
+
"floatingRateIndex" : {
|
|
219
|
+
"@data" : "EUR-LIBOR-BBA"
|
|
220
|
+
},
|
|
221
|
+
"indexTenor" : {
|
|
222
|
+
"periodMultiplier" : 6,
|
|
223
|
+
"period" : "M"
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}, {
|
|
229
|
+
"price" : [ {
|
|
230
|
+
"@key:scoped" : "price-1",
|
|
231
|
+
"value" : 0.06,
|
|
232
|
+
"unit" : {
|
|
233
|
+
"currency" : {
|
|
234
|
+
"@scheme" : "http://www.fpml.org/coding-scheme/external/iso4217",
|
|
235
|
+
"@data" : "EUR"
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
"perUnitOf" : {
|
|
239
|
+
"currency" : {
|
|
240
|
+
"@scheme" : "http://www.fpml.org/coding-scheme/external/iso4217",
|
|
241
|
+
"@data" : "EUR"
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
"priceType" : "InterestRate"
|
|
245
|
+
} ],
|
|
246
|
+
"quantity" : {
|
|
247
|
+
"@key:scoped" : "quantity-2",
|
|
248
|
+
"value" : 50000000.00,
|
|
249
|
+
"unit" : {
|
|
250
|
+
"currency" : {
|
|
251
|
+
"@scheme" : "http://www.fpml.org/coding-scheme/external/iso4217",
|
|
252
|
+
"@data" : "EUR"
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
} ]
|
|
257
|
+
} ],
|
|
258
|
+
"counterparty" : [ {
|
|
259
|
+
"role" : "Party1",
|
|
260
|
+
"partyReference" : {
|
|
261
|
+
"@ref:external" : "party1"
|
|
262
|
+
}
|
|
263
|
+
}, {
|
|
264
|
+
"role" : "Party2",
|
|
265
|
+
"partyReference" : {
|
|
266
|
+
"@ref:external" : "party2"
|
|
267
|
+
}
|
|
268
|
+
} ],
|
|
269
|
+
"tradeIdentifier" : [ {
|
|
270
|
+
"issuerReference" : {
|
|
271
|
+
"@ref:external" : "party2"
|
|
272
|
+
},
|
|
273
|
+
"assignedIdentifier" : [ {
|
|
274
|
+
"identifier" : {
|
|
275
|
+
"@scheme" : "http://www.barclays.com/swaps/trade-id",
|
|
276
|
+
"@data" : "SW2000"
|
|
277
|
+
},
|
|
278
|
+
"version" : 1
|
|
279
|
+
} ]
|
|
280
|
+
} ],
|
|
281
|
+
"tradeDate" : {
|
|
282
|
+
"@data" : "1994-12-12"
|
|
283
|
+
},
|
|
284
|
+
"party" : [ {
|
|
285
|
+
"@key:external" : "party1",
|
|
286
|
+
"partyId" : [ {
|
|
287
|
+
"identifier" : {
|
|
288
|
+
"@scheme" : "http://www.fpml.org/coding-scheme/external/iso9362",
|
|
289
|
+
"@data" : "PARTYAUS33"
|
|
290
|
+
},
|
|
291
|
+
"identifierType" : "BIC"
|
|
292
|
+
} ],
|
|
293
|
+
"name" : {
|
|
294
|
+
"@data" : "Party A"
|
|
295
|
+
}
|
|
296
|
+
}, {
|
|
297
|
+
"@key:external" : "party2",
|
|
298
|
+
"partyId" : [ {
|
|
299
|
+
"identifier" : {
|
|
300
|
+
"@scheme" : "http://www.fpml.org/coding-scheme/external/iso9362",
|
|
301
|
+
"@data" : "BARCGB2L"
|
|
302
|
+
},
|
|
303
|
+
"identifierType" : "BIC"
|
|
304
|
+
} ]
|
|
305
|
+
} ]
|
|
306
|
+
}
|
|
307
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
'''test module for ref lifecycle'''
|
|
2
|
+
from collections import UserList
|
|
3
|
+
from typing import Optional, Annotated
|
|
4
|
+
from pydantic import Field
|
|
5
|
+
import pytest
|
|
6
|
+
from rune.runtime.metadata import Reference, KeyType, UnresolvedReference
|
|
7
|
+
from rune.runtime.base_data_class import BaseDataClass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class B(BaseDataClass):
|
|
11
|
+
'''no doc'''
|
|
12
|
+
fieldB: str = Field(..., description='')
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class A(BaseDataClass):
|
|
16
|
+
'''no doc'''
|
|
17
|
+
b: Annotated[B, B.serializer(),
|
|
18
|
+
B.validator(('@key:scoped', ))] = Field(..., description='')
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Root(BaseDataClass):
|
|
22
|
+
'''no doc'''
|
|
23
|
+
typeA: Optional[Annotated[A, A.serializer(),
|
|
24
|
+
A.validator()]] = Field(None, description='')
|
|
25
|
+
bAddress: Optional[Annotated[B,
|
|
26
|
+
B.serializer(),
|
|
27
|
+
B.validator(('@ref:scoped', ))]] = Field(
|
|
28
|
+
None, description='')
|
|
29
|
+
_KEY_REF_CONSTRAINTS = {
|
|
30
|
+
'bAddress': {'@ref:scoped'}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class DeepRef(BaseDataClass):
|
|
34
|
+
'''no doc'''
|
|
35
|
+
root: Annotated[Root, Root.serializer(),
|
|
36
|
+
Root.validator()] = Field(..., description='')
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ListedReferences(BaseDataClass):
|
|
40
|
+
'''References and their targets occur in separate list fields.'''
|
|
41
|
+
references: list[Annotated[Root, Root.validator()]]
|
|
42
|
+
targets: list[Annotated[A, A.validator()]]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class NestedListedReferences(BaseDataClass):
|
|
46
|
+
'''Exercise lists below both model fields and other lists.'''
|
|
47
|
+
groups: list[Annotated[ListedReferences, ListedReferences.validator()]]
|
|
48
|
+
labels: list[str] = Field(default_factory=list)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class SequenceContainer(BaseDataClass):
|
|
52
|
+
'''Keep sequence implementations intact during validation.'''
|
|
53
|
+
entries: object
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CyclicReference(BaseDataClass):
|
|
57
|
+
'''A resolved reference can point back to its containing object.'''
|
|
58
|
+
_ALLOWED_METADATA = {'@key'}
|
|
59
|
+
target: Annotated[BaseDataClass, BaseDataClass.validator(('@ref', ))]
|
|
60
|
+
_KEY_REF_CONSTRAINTS = {'target': {'@ref'}}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@pytest.fixture
|
|
64
|
+
def listed_reference_data():
|
|
65
|
+
'''A forward reference to a target in a sibling list.'''
|
|
66
|
+
return {
|
|
67
|
+
'groups': [{
|
|
68
|
+
'references': [
|
|
69
|
+
{'bAddress': {'@ref:scoped': 'first'}},
|
|
70
|
+
{'bAddress': {'@ref:scoped': 'second'}},
|
|
71
|
+
],
|
|
72
|
+
'targets': [
|
|
73
|
+
{'b': {'@key:scoped': 'first', 'fieldB': 'first target'}},
|
|
74
|
+
{'b': {'@key:scoped': 'second', 'fieldB': 'second target'}},
|
|
75
|
+
],
|
|
76
|
+
}],
|
|
77
|
+
'labels': ['primitive list entries are ignored'],
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_list_children_share_parent_and_key_maps(listed_reference_data):
|
|
82
|
+
'''Keys from list children must be visible to sibling list entries.'''
|
|
83
|
+
model = NestedListedReferences.model_validate(listed_reference_data)
|
|
84
|
+
group = model.groups[0]
|
|
85
|
+
|
|
86
|
+
assert group.get_rune_parent() is model
|
|
87
|
+
for reference, target in zip(group.references, group.targets):
|
|
88
|
+
assert reference.get_rune_parent() is group
|
|
89
|
+
assert target.get_rune_parent() is group
|
|
90
|
+
key = target.b.get_meta('@key:scoped')
|
|
91
|
+
assert reference.get_object_by_key(key, KeyType.SCOPED) is target.b
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_deserialize_and_validate_references_in_nested_lists(listed_reference_data):
|
|
95
|
+
'''Rune deserialization resolves every list entry before validation.'''
|
|
96
|
+
model = NestedListedReferences.rune_deserialize(listed_reference_data)
|
|
97
|
+
group = model.groups[0]
|
|
98
|
+
|
|
99
|
+
for reference, target in zip(group.references, group.targets):
|
|
100
|
+
assert reference.bAddress is target.b
|
|
101
|
+
assert reference.resolve_ref_key('bAddress') == target.b.get_meta('@key:scoped')
|
|
102
|
+
assert model.validate_model() == []
|
|
103
|
+
|
|
104
|
+
model.resolve_references()
|
|
105
|
+
for reference, target in zip(group.references, group.targets):
|
|
106
|
+
assert reference.bAddress is target.b
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_resolve_list_references_respects_recurse_false(listed_reference_data):
|
|
110
|
+
'''Nonrecursive resolution leaves child references untouched.'''
|
|
111
|
+
model = NestedListedReferences.model_validate(listed_reference_data)
|
|
112
|
+
|
|
113
|
+
model.resolve_references(recurse=False)
|
|
114
|
+
|
|
115
|
+
assert all(isinstance(ref.bAddress, UnresolvedReference)
|
|
116
|
+
for ref in model.groups[0].references)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_resolve_list_references_respects_ignore_dangling(listed_reference_data):
|
|
120
|
+
'''Missing list references may be ignored or reported by the caller.'''
|
|
121
|
+
listed_reference_data['groups'][0]['references'][1]['bAddress'] = {
|
|
122
|
+
'@ref:scoped': 'missing-key'
|
|
123
|
+
}
|
|
124
|
+
model = NestedListedReferences.model_validate(listed_reference_data)
|
|
125
|
+
|
|
126
|
+
model.resolve_references(ignore_dangling=True)
|
|
127
|
+
|
|
128
|
+
group = model.groups[0]
|
|
129
|
+
assert group.references[0].bAddress is group.targets[0].b
|
|
130
|
+
assert isinstance(group.references[1].bAddress, UnresolvedReference)
|
|
131
|
+
with pytest.raises(KeyError, match='missing-key'):
|
|
132
|
+
model.resolve_references(ignore_dangling=False)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@pytest.mark.parametrize('sequence_type', [list, tuple, UserList])
|
|
136
|
+
def test_resolve_references_in_sequence_types(sequence_type):
|
|
137
|
+
'''Supported sequences provide both parent wiring and reference traversal.'''
|
|
138
|
+
reference = Root.model_validate({'bAddress': {'@ref:scoped': 'target'}})
|
|
139
|
+
target = A.model_validate({'b': {'@key:scoped': 'target', 'fieldB': 'value'}})
|
|
140
|
+
model = SequenceContainer(entries=sequence_type([reference, target, 'label', None]))
|
|
141
|
+
|
|
142
|
+
model.resolve_references()
|
|
143
|
+
|
|
144
|
+
assert reference.get_rune_parent() is model
|
|
145
|
+
assert target.get_rune_parent() is model
|
|
146
|
+
assert reference.bAddress is target.b
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_resolve_references_skips_resolved_cycles_in_lists():
|
|
150
|
+
'''Repeated resolution must not recurse through an already-bound reference.'''
|
|
151
|
+
child = CyclicReference.model_validate({'target': {'@ref': 'self'}})
|
|
152
|
+
child.target = Reference(child)
|
|
153
|
+
model = SequenceContainer(entries=[child])
|
|
154
|
+
|
|
155
|
+
model.resolve_references()
|
|
156
|
+
model.resolve_references()
|
|
157
|
+
|
|
158
|
+
assert child.target is child
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_ref_creation():
|
|
162
|
+
'''no doc'''
|
|
163
|
+
b = B(fieldB='some b content')
|
|
164
|
+
a = A(b=b)
|
|
165
|
+
root = Root(typeA=a, bAddress=Reference(a.b, 'aKey', KeyType.SCOPED))
|
|
166
|
+
# pylint: disable=no-member
|
|
167
|
+
assert id(root.typeA.b) == id(root.bAddress)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_deep_ref_creation():
|
|
171
|
+
'''no doc'''
|
|
172
|
+
b = B(fieldB='some b content')
|
|
173
|
+
a = A(b=b)
|
|
174
|
+
root = Root(typeA=a, bAddress=Reference(a.b, 'aKey2', KeyType.SCOPED))
|
|
175
|
+
deep_ref = DeepRef(root=root)
|
|
176
|
+
# pylint: disable=no-member
|
|
177
|
+
assert id(deep_ref.root.typeA.b) == id(deep_ref.root.bAddress)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_fail_wrong_key_ext():
|
|
181
|
+
'''no doc'''
|
|
182
|
+
b = B(fieldB='some b content')
|
|
183
|
+
a = A(b=b)
|
|
184
|
+
with pytest.raises(ValueError):
|
|
185
|
+
Root(typeA=a, bAddress=Reference(a.b, 'aKey', KeyType.EXTERNAL))
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_fail_wrong_key_int():
|
|
189
|
+
'''no doc'''
|
|
190
|
+
b = B(fieldB='some b content')
|
|
191
|
+
a = A(b=b)
|
|
192
|
+
with pytest.raises(ValueError):
|
|
193
|
+
Root(typeA=a, bAddress=Reference(a.b))
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_scoped_reference_metadata_and_type():
|
|
197
|
+
'''scoped refs should store key type and metadata'''
|
|
198
|
+
b = B(fieldB='some b content')
|
|
199
|
+
a = A(b=b)
|
|
200
|
+
root = Root(typeA=a, bAddress=Reference(a.b, 'aKeyScoped', KeyType.SCOPED))
|
|
201
|
+
assert b.get_meta('@key:scoped') == 'aKeyScoped'
|
|
202
|
+
assert root.__dict__['__rune_references']['bAddress'][1] == KeyType.SCOPED
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_ref_prefers_scoped_over_internal():
|
|
206
|
+
'''scoped ref should be preferred when both tags are provided'''
|
|
207
|
+
rune_dict = {
|
|
208
|
+
"bAddress": {
|
|
209
|
+
"@ref": "internalKey",
|
|
210
|
+
"@ref:scoped": "scopedKey"
|
|
211
|
+
},
|
|
212
|
+
"typeA": {
|
|
213
|
+
"b": {
|
|
214
|
+
"@key:scoped": "scopedKey",
|
|
215
|
+
"fieldB": "some b content"
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
root = Root.model_validate(rune_dict)
|
|
220
|
+
assert root.bAddress == root.typeA.b
|
|
221
|
+
assert root.__dict__['__rune_references']['bAddress'][1] == KeyType.SCOPED
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def test_invalid_multiple_ref_tags_raise():
|
|
225
|
+
'''unknown multiple ref tags should raise'''
|
|
226
|
+
rune_dict = {
|
|
227
|
+
"bAddress": {
|
|
228
|
+
"@ref:foo": "key1",
|
|
229
|
+
"@ref:bar": "key2"
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
with pytest.raises(ValueError):
|
|
233
|
+
Root.model_validate(rune_dict)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_scoped_key_not_visible_outside_scope(mocker):
|
|
237
|
+
'''scoped keys should not leak across scope instances'''
|
|
238
|
+
mocker.patch('rune.runtime.metadata.BaseMetaDataMixin._DEFAULT_SCOPE_TYPE',
|
|
239
|
+
'test_deep_keys_and_references.Root')
|
|
240
|
+
b = B(fieldB='some b content')
|
|
241
|
+
a = A(b=b)
|
|
242
|
+
Root(typeA=a, bAddress=Reference(a.b, 'aKeyScoped', KeyType.SCOPED))
|
|
243
|
+
root2 = Root.model_validate({"bAddress": {"@ref:scoped": "aKeyScoped"}})
|
|
244
|
+
with pytest.raises(KeyError):
|
|
245
|
+
root2.resolve_references(ignore_dangling=False, recurse=False)
|
|
246
|
+
|
|
247
|
+
# EOF
|
|
@@ -16,6 +16,8 @@ because its required fields are absent.
|
|
|
16
16
|
|
|
17
17
|
The fix is to have _deserialize_refs check for reference dicts BEFORE calling
|
|
18
18
|
handler, consistent with ComplexTypeMetaDataMixin.deserialize.
|
|
19
|
+
Target keys must also be retained when the deferred schema bypasses the field
|
|
20
|
+
metadata validator, so references in sibling lists can find their targets.
|
|
19
21
|
|
|
20
22
|
Note on placement: this test lives in rune-runtime even though the Phase 1/2/3
|
|
21
23
|
pattern originates in the rune-python-generator's bundle generator. The runtime
|
|
@@ -29,7 +31,7 @@ import pytest
|
|
|
29
31
|
from pydantic import Field
|
|
30
32
|
|
|
31
33
|
from rune.runtime.base_data_class import BaseDataClass
|
|
32
|
-
from rune.runtime.metadata import BaseReference, UnresolvedReference
|
|
34
|
+
from rune.runtime.metadata import BaseReference, KeyType, UnresolvedReference
|
|
33
35
|
|
|
34
36
|
|
|
35
37
|
class Issuer(BaseDataClass):
|
|
@@ -45,7 +47,7 @@ class Contract(BaseDataClass):
|
|
|
45
47
|
name: str = Field(..., description='Contract name')
|
|
46
48
|
|
|
47
49
|
_KEY_REF_CONSTRAINTS = {
|
|
48
|
-
'issuerReference': {'@key', '@key:external', '@ref', '@ref:external'}
|
|
50
|
+
'issuerReference': {'@key', '@key:external', '@ref', '@ref:external', '@ref:scoped'}
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
|
|
@@ -55,7 +57,7 @@ def _apply_deferred_annotations():
|
|
|
55
57
|
Contract.__annotations__['issuerReference'] = Annotated[
|
|
56
58
|
Optional[Issuer | BaseReference],
|
|
57
59
|
Issuer.serializer(),
|
|
58
|
-
Issuer.validator(('@key', '@key:external', '@ref', '@ref:external')),
|
|
60
|
+
Issuer.validator(('@key', '@key:external', '@ref', '@ref:external', '@ref:scoped')),
|
|
59
61
|
]
|
|
60
62
|
Contract.model_rebuild(force=True)
|
|
61
63
|
|
|
@@ -63,6 +65,31 @@ def _apply_deferred_annotations():
|
|
|
63
65
|
_apply_deferred_annotations()
|
|
64
66
|
|
|
65
67
|
|
|
68
|
+
class Portfolio(BaseDataClass):
|
|
69
|
+
'''Bare field types reproduce schemas rebuilt from deferred annotations.'''
|
|
70
|
+
contracts: list[Contract]
|
|
71
|
+
issuers: list[Issuer]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@pytest.mark.parametrize('key_type', list(KeyType))
|
|
75
|
+
def test_deferred_list_targets_retain_keys_and_resolve(key_type):
|
|
76
|
+
'''Target keys must survive even when field metadata validators are absent.'''
|
|
77
|
+
data = {
|
|
78
|
+
'contracts': [{
|
|
79
|
+
'name': 'test-contract',
|
|
80
|
+
'issuerReference': {key_type.rune_ref_tag: 'party1'},
|
|
81
|
+
}],
|
|
82
|
+
'issuers': [{key_type.rune_key_tag: 'party1', 'partyId': ['LEI-001']}],
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
portfolio = Portfolio.rune_deserialize(data)
|
|
86
|
+
|
|
87
|
+
assert portfolio.issuers[0].get_meta(key_type.key_tag) == 'party1'
|
|
88
|
+
assert portfolio.contracts[0].issuerReference is portfolio.issuers[0]
|
|
89
|
+
assert portfolio.get_object_by_key('party1', key_type) is portfolio.issuers[0]
|
|
90
|
+
assert portfolio.validate_model() == []
|
|
91
|
+
|
|
92
|
+
|
|
66
93
|
def test_ref_external_deserializes_to_unresolved_reference():
|
|
67
94
|
'''A ref dict in a deferred field should produce an UnresolvedReference,
|
|
68
95
|
not a validation error about missing required fields on the target type.'''
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
'''Full attribute validation - pydantic and constraints'''
|
|
2
|
-
import pytest
|
|
3
|
-
from pydantic import ValidationError
|
|
4
|
-
try:
|
|
5
|
-
# pylint: disable=unused-import
|
|
6
|
-
# type: ignore
|
|
7
|
-
from cdm.base.math.NonNegativeQuantity import NonNegativeQuantity
|
|
8
|
-
from cdm.base.math.UnitType import UnitType
|
|
9
|
-
NO_SER_TEST_MOD = False
|
|
10
|
-
except ImportError:
|
|
11
|
-
NO_SER_TEST_MOD = True
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@pytest.mark.skipif(NO_SER_TEST_MOD, reason='CDM package not found')
|
|
15
|
-
def test_bad_attrib_validation():
|
|
16
|
-
'''Invalid attribute assigned'''
|
|
17
|
-
unit = UnitType(currency='EUR')
|
|
18
|
-
mq = NonNegativeQuantity(value=10, unit=unit)
|
|
19
|
-
mq.frequency = 'Blah'
|
|
20
|
-
with pytest.raises(ValidationError):
|
|
21
|
-
mq.validate_model()
|
|
22
|
-
|
|
23
|
-
# EOF
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
'''test module for ref lifecycle'''
|
|
2
|
-
from typing import Optional, Annotated
|
|
3
|
-
from pydantic import Field
|
|
4
|
-
import pytest
|
|
5
|
-
from rune.runtime.metadata import Reference, KeyType
|
|
6
|
-
from rune.runtime.base_data_class import BaseDataClass
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class B(BaseDataClass):
|
|
10
|
-
'''no doc'''
|
|
11
|
-
fieldB: str = Field(..., description='')
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class A(BaseDataClass):
|
|
15
|
-
'''no doc'''
|
|
16
|
-
b: Annotated[B, B.serializer(),
|
|
17
|
-
B.validator(('@key:scoped', ))] = Field(..., description='')
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class Root(BaseDataClass):
|
|
21
|
-
'''no doc'''
|
|
22
|
-
typeA: Optional[Annotated[A, A.serializer(),
|
|
23
|
-
A.validator()]] = Field(None, description='')
|
|
24
|
-
bAddress: Optional[Annotated[B,
|
|
25
|
-
B.serializer(),
|
|
26
|
-
B.validator(('@ref:scoped', ))]] = Field(
|
|
27
|
-
None, description='')
|
|
28
|
-
_KEY_REF_CONSTRAINTS = {
|
|
29
|
-
'bAddress': {'@ref:scoped'}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
class DeepRef(BaseDataClass):
|
|
33
|
-
'''no doc'''
|
|
34
|
-
root: Annotated[Root, Root.serializer(),
|
|
35
|
-
Root.validator()] = Field(..., description='')
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def test_ref_creation():
|
|
39
|
-
'''no doc'''
|
|
40
|
-
b = B(fieldB='some b content')
|
|
41
|
-
a = A(b=b)
|
|
42
|
-
root = Root(typeA=a, bAddress=Reference(a.b, 'aKey', KeyType.SCOPED))
|
|
43
|
-
# pylint: disable=no-member
|
|
44
|
-
assert id(root.typeA.b) == id(root.bAddress)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def test_deep_ref_creation():
|
|
48
|
-
'''no doc'''
|
|
49
|
-
b = B(fieldB='some b content')
|
|
50
|
-
a = A(b=b)
|
|
51
|
-
root = Root(typeA=a, bAddress=Reference(a.b, 'aKey2', KeyType.SCOPED))
|
|
52
|
-
deep_ref = DeepRef(root=root)
|
|
53
|
-
# pylint: disable=no-member
|
|
54
|
-
assert id(deep_ref.root.typeA.b) == id(deep_ref.root.bAddress)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def test_fail_wrong_key_ext():
|
|
58
|
-
'''no doc'''
|
|
59
|
-
b = B(fieldB='some b content')
|
|
60
|
-
a = A(b=b)
|
|
61
|
-
with pytest.raises(ValueError):
|
|
62
|
-
Root(typeA=a, bAddress=Reference(a.b, 'aKey', KeyType.EXTERNAL))
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def test_fail_wrong_key_int():
|
|
66
|
-
'''no doc'''
|
|
67
|
-
b = B(fieldB='some b content')
|
|
68
|
-
a = A(b=b)
|
|
69
|
-
with pytest.raises(ValueError):
|
|
70
|
-
Root(typeA=a, bAddress=Reference(a.b))
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def test_scoped_reference_metadata_and_type():
|
|
74
|
-
'''scoped refs should store key type and metadata'''
|
|
75
|
-
b = B(fieldB='some b content')
|
|
76
|
-
a = A(b=b)
|
|
77
|
-
root = Root(typeA=a, bAddress=Reference(a.b, 'aKeyScoped', KeyType.SCOPED))
|
|
78
|
-
assert b.get_meta('@key:scoped') == 'aKeyScoped'
|
|
79
|
-
assert root.__dict__['__rune_references']['bAddress'][1] == KeyType.SCOPED
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def test_ref_prefers_scoped_over_internal():
|
|
83
|
-
'''scoped ref should be preferred when both tags are provided'''
|
|
84
|
-
rune_dict = {
|
|
85
|
-
"bAddress": {
|
|
86
|
-
"@ref": "internalKey",
|
|
87
|
-
"@ref:scoped": "scopedKey"
|
|
88
|
-
},
|
|
89
|
-
"typeA": {
|
|
90
|
-
"b": {
|
|
91
|
-
"@key:scoped": "scopedKey",
|
|
92
|
-
"fieldB": "some b content"
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
|
-
}
|
|
96
|
-
root = Root.model_validate(rune_dict)
|
|
97
|
-
assert root.bAddress == root.typeA.b
|
|
98
|
-
assert root.__dict__['__rune_references']['bAddress'][1] == KeyType.SCOPED
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def test_invalid_multiple_ref_tags_raise():
|
|
102
|
-
'''unknown multiple ref tags should raise'''
|
|
103
|
-
rune_dict = {
|
|
104
|
-
"bAddress": {
|
|
105
|
-
"@ref:foo": "key1",
|
|
106
|
-
"@ref:bar": "key2"
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
with pytest.raises(ValueError):
|
|
110
|
-
Root.model_validate(rune_dict)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def test_scoped_key_not_visible_outside_scope(mocker):
|
|
114
|
-
'''scoped keys should not leak across scope instances'''
|
|
115
|
-
mocker.patch('rune.runtime.metadata.BaseMetaDataMixin._DEFAULT_SCOPE_TYPE',
|
|
116
|
-
'test_deep_keys_and_references.Root')
|
|
117
|
-
b = B(fieldB='some b content')
|
|
118
|
-
a = A(b=b)
|
|
119
|
-
Root(typeA=a, bAddress=Reference(a.b, 'aKeyScoped', KeyType.SCOPED))
|
|
120
|
-
root2 = Root.model_validate({"bAddress": {"@ref:scoped": "aKeyScoped"}})
|
|
121
|
-
with pytest.raises(KeyError):
|
|
122
|
-
root2.resolve_references(ignore_dangling=False, recurse=False)
|
|
123
|
-
|
|
124
|
-
# EOF
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/functions/specs/cdm/base/math/RoundToNearest.py
RENAMED
|
File without changes
|
{rune_runtime-2.2.0 → rune_runtime-2.2.1}/test/functions/specs/cdm/base/math/RoundingModeEnum.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|