rune-runtime 1.0.20__py3-none-any.whl

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.
@@ -0,0 +1,5 @@
1
+ '''rune dsl runtime package'''
2
+ # PEP 396 module version attribute in PEP 440 version format
3
+ from .version import __version__
4
+
5
+ # EOF
@@ -0,0 +1,407 @@
1
+ '''Base class for all Rune type classes'''
2
+ import logging
3
+ import importlib
4
+ import copy
5
+ import json
6
+ from typing import get_args, get_origin, Any, Literal
7
+ from typing_extensions import Self
8
+ from pydantic import (BaseModel, ValidationError, ConfigDict, model_serializer,
9
+ model_validator, ModelWrapValidatorHandler)
10
+ from pydantic.main import IncEx
11
+ from rune.runtime.conditions import ConditionViolationError
12
+ from rune.runtime.conditions import get_conditions
13
+ from rune.runtime.metadata import (ComplexTypeMetaDataMixin, Reference,
14
+ UnresolvedReference, BaseMetaDataMixin,
15
+ _EnumWrapper, RUNE_OBJ_MAPS)
16
+
17
+ ROOT_CONTAINER = '__rune_root_metadata'
18
+
19
+
20
+ class BaseDataClass(BaseModel, ComplexTypeMetaDataMixin):
21
+ ''' A base class for all cdm generated classes. It is derived from
22
+ `pydantic.BaseModel` which provides type checking at object creation
23
+ for all cdm classes. It provides as well the `validate_model`,
24
+ `validate_conditions` and `validate_attribs` methods which perform the
25
+ conditions, cardinality and type checks as specified in the rune
26
+ type model. The method `validate_model` is not invoked automatically,
27
+ but is left to the user to determine when to check the validity of the
28
+ cdm model.
29
+ '''
30
+ model_config = ConfigDict(extra='ignore',
31
+ revalidate_instances='always',
32
+ arbitrary_types_allowed=True)
33
+
34
+ def __setattr__(self, name: str, value: Any) -> None:
35
+ if isinstance(value, Reference):
36
+ self._bind_property_to(name, value)
37
+ else:
38
+ # replace reference with an object
39
+ if name in self._get_rune_refs_container():
40
+ self._remove_rune_ref(name)
41
+ if isinstance(self.__dict__[name], _EnumWrapper):
42
+ self.__dict__[name] = _EnumWrapper()
43
+ # if the value is an enum, pass it to the EnumWrapper
44
+ if (isinstance(self.__dict__[name], _EnumWrapper)
45
+ and not isinstance(value, _EnumWrapper)):
46
+ value = _EnumWrapper(value)
47
+ # if the value is a "model", register as rune_parent
48
+ if isinstance(value, BaseMetaDataMixin):
49
+ value._set_rune_parent(self)
50
+ super().__setattr__(name, value)
51
+
52
+ @model_serializer(mode='wrap')
53
+ def _serialize_refs(self, serializer, info):
54
+ '''should replace objects with refs while serializing'''
55
+ res = serializer(self, info)
56
+ refs = self._get_rune_refs_container()
57
+ for property_nm, (key, ref_type) in refs.items():
58
+ res[property_nm] = {ref_type.rune_ref_tag: key}
59
+ res = self.__dict__.get(ROOT_CONTAINER, {}) | res
60
+ return res
61
+
62
+ @model_validator(mode='wrap')
63
+ @classmethod
64
+ def _deserialize_refs(cls, data: Any,
65
+ handler: ModelWrapValidatorHandler[Self]) -> Self:
66
+ '''should resolve refs after creation'''
67
+ obj = handler(data)
68
+ obj._init_rune_parent() # pylint: disable=protected-access
69
+ obj.resolve_references(ignore_dangling=True, recurse=False)
70
+ return obj
71
+
72
+ def _init_rune_parent(self):
73
+ '''sets the rune parent in all properties'''
74
+ refs = self._get_rune_refs_container()
75
+ if not self.get_rune_parent() and RUNE_OBJ_MAPS not in self.__dict__:
76
+ self.__dict__[RUNE_OBJ_MAPS] = {}
77
+
78
+ for prop_nm, obj in self.__dict__.items():
79
+ if (isinstance(obj, BaseMetaDataMixin)
80
+ and not prop_nm.startswith('__') and prop_nm not in refs):
81
+ obj._set_rune_parent(self) # pylint: disable=protected-access
82
+
83
+ def rune_serialize(
84
+ self,
85
+ *,
86
+ validate_model: bool = True,
87
+ check_rune_constraints: bool = True,
88
+ strict: bool = True,
89
+ raise_validation_errors: bool = True,
90
+ indent: int | None = None,
91
+ include: IncEx | None = None,
92
+ exclude: IncEx | None = None,
93
+ exclude_unset: bool = True,
94
+ exclude_defaults: bool = True,
95
+ exclude_none: bool = False,
96
+ round_trip: bool = False,
97
+ warnings: bool | Literal['none', 'warn', 'error'] = True,
98
+ serialize_as_any: bool = False,
99
+ ) -> str:
100
+ '''Rune conform serialization to json string. To be invoked on the model
101
+ root.
102
+
103
+ #### Args:
104
+ `validate_model (bool, optional):` Validate the model prior
105
+ serialization. It checks also all Rune type constraints.
106
+ Defaults to True.
107
+
108
+ `check_rune_constraints (bool, optional):` If `validate_model` is
109
+ set to `True`, executes all model defined Rune constraints after
110
+ deserialization. Defaults to True.
111
+
112
+ `strict (bool, optional):` Perform strict attribute validation.
113
+ Defaults to True.
114
+
115
+ `raise_validation_errors (bool, optional):` Raise an exception in
116
+ case a validation error has occurred. Defaults to True.
117
+
118
+ `indent (int | None, optional):` Indentation to use in the JSON
119
+ output. If None is passed, the output will be compact. Defaults to
120
+ None.
121
+
122
+ `include (IncEx | None, optional):` Field(s) to include in the JSON
123
+ output. Defaults to None.
124
+
125
+ `exclude (IncEx | None, optional):` Field(s) to exclude from the
126
+ JSON output. Defaults to None.
127
+
128
+ `exclude_unset (bool, optional):` Whether to exclude fields that
129
+ have not been explicitly set. Defaults to True.
130
+
131
+ `exclude_defaults (bool, optional):` Whether to exclude fields that
132
+ are set to their default value. Defaults to True.
133
+
134
+ `exclude_none (bool, optional):` Whether to exclude fields that have
135
+ a value of `None`. Defaults to False.
136
+
137
+ `round_trip (bool, optional):` If True, dumped values should be
138
+ valid as input for non-idempotent types such as Json[T]. Defaults to
139
+ False.
140
+
141
+ `warnings (bool | Literal['none', 'warn', 'error'], optional):` How
142
+ to handle serialization errors. False/"none" ignores them,
143
+ True/"warn" logs errors, "error" raises a
144
+ `PydanticSerializationError`. Defaults to True.
145
+
146
+ `serialize_as_any (bool, optional):` Whether to serialize fields
147
+ with duck-typing serialization behavior. Defaults to False.
148
+
149
+ #### Returns:
150
+ `str:` A Rune conforming JSON string representation of the model.
151
+ '''
152
+ try:
153
+ if validate_model:
154
+ self.validate_model(
155
+ check_rune_constraints=check_rune_constraints,
156
+ strict=strict,
157
+ raise_exc=raise_validation_errors)
158
+
159
+ root_meta = self.__dict__.setdefault(ROOT_CONTAINER, {})
160
+ root_meta['@type'] = self._FQRTN
161
+ root_meta['@model'] = self._FQRTN.split('.', maxsplit=1)[0]
162
+ root_meta['@version'] = self.get_model_version()
163
+
164
+ return self.model_dump_json(indent=indent,
165
+ include=include,
166
+ exclude=exclude,
167
+ exclude_unset=exclude_unset,
168
+ exclude_defaults=exclude_defaults,
169
+ exclude_none=exclude_none,
170
+ round_trip=round_trip,
171
+ warnings=warnings,
172
+ serialize_as_any=serialize_as_any)
173
+ finally:
174
+ self.__dict__.pop(ROOT_CONTAINER)
175
+
176
+ @classmethod
177
+ def rune_deserialize(cls,
178
+ rune_data: str | dict[str, Any],
179
+ validate_model: bool = True,
180
+ check_rune_constraints: bool = True,
181
+ strict: bool = False,
182
+ raise_validation_errors: bool = True) -> BaseModel:
183
+ # pylint: disable=line-too-long
184
+ '''Rune compliant deserialization
185
+
186
+ #### Args:
187
+ `rune_json (str):` A JSON string.
188
+
189
+ `validate_model (bool, optional):` Validate the model after
190
+ deserialization. It checks also all Rune type constraints. Defaults
191
+ to True.
192
+
193
+ `check_rune_constraints (bool, optional):` If `validate_model` is
194
+ set to `True`, executes all model defined Rune constraints after
195
+ deserialization. Defaults to True.
196
+
197
+ `strict (bool, optional):` Perform strict attribute validation.
198
+ Defaults to False.
199
+
200
+ `raise_validation_errors (bool, optional):` Raise an exception in
201
+ case a validation error has occurred. Defaults to True.
202
+
203
+ #### Returns:
204
+ `BaseModel:` The Rune model.
205
+ '''
206
+ if isinstance(rune_data, str):
207
+ rune_dict = json.loads(rune_data)
208
+ # NOTE: json.loads will not create the right atomic types
209
+ # (e.g. date, datetime, time etc) and the strict model validate
210
+ # will not attempt to convert them.
211
+ strict = False
212
+ elif not isinstance(rune_data, dict):
213
+ raise ValueError(f'rune_data is of type {type(rune_data)}, '
214
+ 'alas it has to be either dict or str!')
215
+ else:
216
+ rune_dict = copy.deepcopy(rune_data)
217
+ rune_dict.pop('@version', None)
218
+ rune_dict.pop('@model', None)
219
+ rune_cls = cls._type_to_cls(rune_dict)
220
+ model = rune_cls.model_validate(rune_dict, strict=strict)
221
+ model.resolve_references(ignore_dangling=False, recurse=True)
222
+ if validate_model:
223
+ model.validate_model(check_rune_constraints=check_rune_constraints,
224
+ strict=strict,
225
+ raise_exc=raise_validation_errors)
226
+ return model
227
+
228
+ def resolve_references(self, ignore_dangling=False, recurse=True):
229
+ '''resolves all attributes which are references'''
230
+ if recurse:
231
+ for prop_nm, obj in self.__dict__.items():
232
+ if (isinstance(obj, BaseDataClass)
233
+ and not prop_nm.startswith('__')):
234
+ obj.resolve_references(ignore_dangling=ignore_dangling,
235
+ recurse=recurse)
236
+
237
+ refs = []
238
+ for prop_nm, obj in self.__dict__.items():
239
+ if isinstance(obj, (UnresolvedReference, Reference)):
240
+ try:
241
+ refs.append((prop_nm, obj.get_reference(self)))
242
+ except KeyError:
243
+ if not ignore_dangling:
244
+ raise
245
+
246
+ for prop_nm, ref in refs:
247
+ self._bind_property_to(prop_nm, ref)
248
+
249
+ def validate_model(self,
250
+ check_rune_constraints=True,
251
+ recursively: bool = True,
252
+ raise_exc: bool = True,
253
+ strict: bool = True) -> list:
254
+ ''' This method performs full model validation. It will validate all
255
+ attributes and it will also invoke `validate_conditions` to check
256
+ all conditions and the cardinality of all attributes of this object.
257
+ The parameter `raise_exc` controls whether an exception should be
258
+ thrown if a validation or condition is violated or if a list with
259
+ all encountered violations should be returned instead.
260
+ '''
261
+ try:
262
+ self.disable_meta_checks()
263
+ att_errors = self.validate_attribs(raise_exc=raise_exc,
264
+ strict=strict)
265
+ if check_rune_constraints:
266
+ att_errors.extend(
267
+ self.validate_conditions(recursively=recursively,
268
+ raise_exc=raise_exc))
269
+ return att_errors
270
+ finally:
271
+ self.enable_meta_checks()
272
+
273
+ def validate_attribs(self,
274
+ raise_exc: bool = True,
275
+ strict: bool = True) -> list:
276
+ ''' This method performs attribute type validation.
277
+ The parameter `raise_exc` controls whether an exception should be
278
+ thrown if a validation or condition is violated or if a list with
279
+ all encountered violations should be returned instead.
280
+ '''
281
+ try:
282
+ self.model_validate(self, strict=strict)
283
+ except ValidationError as validation_error:
284
+ if raise_exc:
285
+ raise validation_error
286
+ return [validation_error]
287
+ return []
288
+
289
+ def validate_conditions(self,
290
+ recursively: bool = True,
291
+ raise_exc: bool = True) -> list:
292
+ ''' This method will check all conditions and the cardinality of all
293
+ attributes of this object. This includes conditions and cardinality
294
+ of properties specified in the base classes. If the parameter
295
+ `recursively` is set to `True`, it will invoke the validation on the
296
+ rune defined attributes of this object too.
297
+ The parameter `raise_exc` controls whether an exception should be
298
+ thrown if a condition is not met or if a list with all encountered
299
+ condition violations should be returned instead.
300
+ '''
301
+ self_rep = object.__repr__(self)
302
+ logging.info('Checking conditions for %s ...', self_rep)
303
+ exceptions = []
304
+ for name, condition in get_conditions(self.__class__, BaseDataClass):
305
+ logging.info('Checking condition %s for %s...', name, self_rep)
306
+ if not condition(self):
307
+ msg = f'Condition "{name}" for {repr(self)} failed!'
308
+ logging.error(msg)
309
+ exc = ConditionViolationError(msg)
310
+ if raise_exc:
311
+ raise exc
312
+ exceptions.append(exc)
313
+ else:
314
+ logging.info('Condition %s for %s satisfied.', name, self_rep)
315
+ if recursively:
316
+ for k, v in self.__dict__.items():
317
+ if k.startswith('__'): # ignore *all* private vars!
318
+ continue
319
+ logging.info('Validating conditions of property %s', k)
320
+ exceptions += _validate_conditions_recursively(
321
+ v, raise_exc=raise_exc)
322
+ err = f'with {len(exceptions)}' if exceptions else 'without'
323
+ logging.info('Done conditions checking for %s %s errors.', self_rep,
324
+ err)
325
+ return exceptions
326
+
327
+ def add_to_list_attribute(self, attr_name: str, value) -> None:
328
+ '''
329
+ Adds a value to a list attribute, ensuring the value is of an allowed
330
+ type.
331
+
332
+ Parameters:
333
+ attr_name (str): Name of the list attribute.
334
+ value: Value to add to the list.
335
+
336
+ Raises:
337
+ AttributeError: If the attribute name is not found or not a list.
338
+ TypeError: If the value type is not one of the allowed types.
339
+ '''
340
+ if not hasattr(self, attr_name):
341
+ raise AttributeError(f"Attribute {attr_name} not found.")
342
+
343
+ attr = getattr(self, attr_name)
344
+ if not isinstance(attr, list):
345
+ raise AttributeError(f"Attribute {attr_name} is not a list.")
346
+
347
+ # Get allowed types for the list elements
348
+ allowed_types = self.get_allowed_types_for_list_field(attr_name)
349
+
350
+ # Check if value is an instance of one of the allowed types
351
+ if not isinstance(value, allowed_types):
352
+ raise TypeError(f"Value must be an instance of {allowed_types}, "
353
+ f"not {type(value)}")
354
+
355
+ attr.append(value)
356
+
357
+ @classmethod
358
+ def get_allowed_types_for_list_field(cls, field_name: str):
359
+ '''
360
+ Gets the allowed types for a list field in a Pydantic model, supporting
361
+ both Union and | operator.
362
+
363
+ Parameters:
364
+ cls (type): The Pydantic model class.
365
+ field_name (str): The field name.
366
+
367
+ Returns:
368
+ tuple: A tuple of allowed types.
369
+ '''
370
+ field_type = cls.__annotations__.get(field_name)
371
+ if field_type and get_origin(field_type) is list:
372
+ list_elem_type = get_args(field_type)[0]
373
+ if get_origin(list_elem_type):
374
+ return get_args(list_elem_type)
375
+ return (list_elem_type, ) # Single type or | operator used
376
+ return ()
377
+
378
+ @classmethod
379
+ def get_model_version(cls):
380
+ ''' Attempt to obtain the Rune model version, in case of a failure,
381
+ 0.0.0 will be returned
382
+ '''
383
+ try:
384
+ module = importlib.import_module(
385
+ cls.__module__.split('.', maxsplit=1)[0])
386
+ return getattr(module, 'rune_model_version', default='0.0.0')
387
+ # pylint: disable=bare-except
388
+ except: # noqa
389
+ return '0.0.0'
390
+
391
+
392
+ def _validate_conditions_recursively(obj, raise_exc=True):
393
+ '''Helper to execute conditions recursively on a model.'''
394
+ if not obj:
395
+ return []
396
+ if isinstance(obj, BaseDataClass):
397
+ return obj.validate_conditions(
398
+ recursively=True, # type:ignore
399
+ raise_exc=raise_exc)
400
+ if isinstance(obj, (list, tuple)):
401
+ exc = []
402
+ for item in obj:
403
+ exc += _validate_conditions_recursively(item, raise_exc=raise_exc)
404
+ return exc
405
+ return []
406
+
407
+ # EOF
@@ -0,0 +1,57 @@
1
+ '''facilities for rune conditions'''
2
+ from collections import defaultdict
3
+ from typing import Any
4
+
5
+ _CONDITIONS_REGISTRY: defaultdict[str, dict[str, Any]] = defaultdict(dict)
6
+
7
+
8
+ class ConditionViolationError(ValueError):
9
+ '''Exception thrown on violation of a constraint'''
10
+
11
+
12
+ def rune_condition(condition):
13
+ '''Wrapper to register all constraint functions in the global registry'''
14
+ path_components = condition.__qualname__.split('.')
15
+ path = '.'.join([condition.__module__ or ''] + path_components[:-1])
16
+ name = path_components[-1]
17
+ _CONDITIONS_REGISTRY[path][name] = condition
18
+
19
+ return condition
20
+
21
+
22
+ def rune_local_condition(registry: dict):
23
+ '''Registers a condition function in a local registry.'''
24
+
25
+ def decorator(condition):
26
+ path_components = condition.__qualname__.split('.')
27
+ path = '.'.join([condition.__module__ or ''] + path_components)
28
+ registry[path] = condition
29
+
30
+ return condition
31
+
32
+ return decorator
33
+
34
+
35
+ def rune_execute_local_conditions(registry: dict, cond_type: str):
36
+ '''Executes all registered in a local registry.'''
37
+ for condition_path, condition_func in registry.items():
38
+ if not condition_func():
39
+ raise ConditionViolationError(
40
+ f"{cond_type} '{condition_path}' failed.")
41
+
42
+
43
+ def get_conditions(cls, base_class) -> list:
44
+ '''returns the conditions registered for the passed in class'''
45
+ res = []
46
+ index = cls.__mro__.index(base_class)
47
+ for c in reversed(cls.__mro__[:index]):
48
+ fqcn = _fqcn(c)
49
+ res += [('.'.join([fqcn, k]), v)
50
+ for k, v in _CONDITIONS_REGISTRY.get(fqcn, {}).items()]
51
+ return res
52
+
53
+
54
+ def _fqcn(cls) -> str:
55
+ return '.'.join([cls.__module__ or '', cls.__qualname__])
56
+
57
+ # EOF
@@ -0,0 +1,72 @@
1
+ '''func proxy'''
2
+ import inspect
3
+ import functools
4
+
5
+ __all__ = ['FuncProxy', 'replaceable', 'create_module_attr_guardian']
6
+
7
+
8
+ class FuncProxy:
9
+ '''A callable proxy allowing functions to be replaced at runtime'''
10
+ __slots__ = ('_func',)
11
+
12
+ def __init__(self, func):
13
+ self._func = func
14
+
15
+ def __call__(self, *args, **kwargs):
16
+ '''pass the call to the current function'''
17
+ return self._func(*args, **kwargs)
18
+
19
+ @property
20
+ def func(self):
21
+ '''current function'''
22
+ return self._func
23
+
24
+ @func.setter
25
+ def func(self, func):
26
+ '''replace the current function with a new one'''
27
+ self.__assign__(func)
28
+
29
+ def __assign__(self, func):
30
+ '''assigns the new function and checks parameter list compatibility'''
31
+ if not callable(func):
32
+ raise ValueError(f'Need a callable, but got {str(func)}')
33
+
34
+ curr_params = inspect.signature(self._func).parameters
35
+ new_params = inspect.signature(func).parameters
36
+ if curr_params.keys() != new_params.keys():
37
+ raise ValueError(
38
+ 'Replacement function parameter list do not match the current '
39
+ f'parameter list of {str(self._func)}'
40
+ )
41
+ self._func = func
42
+
43
+
44
+ def replaceable(func):
45
+ '''wrapper for a function which can be replaced at runtime'''
46
+ proxy = FuncProxy(func)
47
+
48
+ @functools.wraps(func)
49
+ def wrapper(*args, **kwargs):
50
+ return proxy(*args, **kwargs)
51
+
52
+ wrapper.__assign__ = proxy.__assign__ # type: ignore
53
+ return wrapper
54
+
55
+
56
+ def create_module_attr_guardian(module):
57
+ '''Returns a module setter class derived from the invoking module'''
58
+ # pylint: disable=too-few-public-methods
59
+ class ModuleAttrSetter(module):
60
+ ''' Redirects the assignment of an attribute to its __assign__ method
61
+ if defined, otherwise the default functionality is used and the
62
+ attribute is just replaced.
63
+ '''
64
+ def __setattr__(self, attr, val):
65
+ exists = getattr(self, attr, None)
66
+ if exists is not None and hasattr(exists, '__assign__'):
67
+ exists.__assign__(val)
68
+ else:
69
+ super().__setattr__(attr, val)
70
+ return ModuleAttrSetter
71
+
72
+ # EOF