Skip to content

elements

trestle.core.models.elements ¤

Element wrapper of an OSCAL model element.

Attributes¤

logger = logging.getLogger(__name__) module-attribute ¤

Classes¤

Element ¤

Element wrapper of an OSCAL model.

Source code in trestle/core/models/elements.py
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
class Element:
    """Element wrapper of an OSCAL model."""

    IGNORE_WRAPPER_ALIAS = '__'

    _allowed_sub_element_types: List[str] = ['Element', 'OscalBaseModel', 'list', 'None', 'dict']

    def __init__(self, elem: OscalBaseModel, wrapper_alias: str = ''):
        """Initialize an element wrapper.

        wrapper_alias is the OSCAL alias for the given elem object and used for seriazation in to_json() method.

        For example,
         - List[Catalog.Group] element should have wrapper alias 'groups'
         - Catalog element should have wrapper alias 'catalog'

        wrapper_alias is deduced for collection type object

        if wrapper_alias = IGNORE_WRAPPER_ALIAS, then it is ignored and assumed to be json-serializable during to_json()
        """
        # FIXME: There are instances where elem is a list.
        self._elem: OscalBaseModel = elem

        if wrapper_alias == '' and wrapper_alias != self.IGNORE_WRAPPER_ALIAS:
            class_name = elem.__class__.__name__
            if utils.is_collection_field_type(elem):
                class_name = self._get_singular_classname()
                if class_name is None:
                    raise TrestleError(
                        f'wrapper_alias not found for a collection type object: {elem.__class__.__name__}'
                    )
            wrapper_alias = str_utils.classname_to_alias(class_name, AliasMode.JSON)

        self._wrapper_alias: str = wrapper_alias

    def _get_singular_classname(self) -> str:
        """Get the inner class name for list or dict objects."""
        # this assumes all items in list and all values in dict are same type
        class_name = None
        root = getattr(self._elem, '__root__', None)
        if root is not None:
            type_str = root.__class__.__name__
            if type_str == 'list':
                class_name = self._elem.__root__[0].__class__.__name__
            elif type_str == 'dict':
                class_name = list(self._elem.__root__.values())[0].__class__.__name__
        return class_name

    def get(self) -> OscalBaseModel:
        """Return the model object."""
        return self._elem

    def _split_element_path(self, element_path: ElementPath):
        """Split the element path into root_model and remaing attr names."""
        path_parts = element_path.get()
        root_model = path_parts[0]
        path_parts = path_parts[1:]

        return root_model, path_parts

    def get_at(self,
               element_path: ElementPath = None,
               check_parent: bool = True) -> Union[OscalBaseModel, List[OscalBaseModel]]:
        """Get the element at the specified element path.

        it will return the sub-model object at the path. Sub-model object
        can be of type OscalBaseModel or List
        """
        if element_path is None:
            return self._elem

        # find the root-model and element path parts
        _, path_parts = self._split_element_path(element_path)

        # TODO validate that self._elem is of same type as root_model

        # initialize the starting element for search
        elm = self._elem
        if hasattr(elm, '__root__') and (isinstance(elm.__root__, dict) or isinstance(elm.__root__, list)):
            elm = elm.__root__

        # if parent exists and does not end with wildcard, use the parent as the starting element for search
        if check_parent and element_path.get_parent(
        ) is not None and element_path.get_parent().get_last() != ElementPath.WILDCARD:
            elm_at = self.get_at(element_path.get_parent())
            if elm_at is None:
                raise TrestleNotFoundError(f'Invalid parent path {element_path.get_parent()}')
            elm = elm_at

        # return the sub-element at the specified path
        for attr in path_parts:
            if elm is None:
                break

            # process for wildcard and array indexes

            if attr == ElementPath.WILDCARD:
                break
            elif attr.isnumeric():
                if isinstance(elm, list):
                    elm = elm[int(attr)]
                else:
                    # index to a non list type should return None
                    return None
            else:
                elm = elm.get_field_value_by_alias(attr)

        return elm

    def get_preceding_element(self, element_path: ElementPath) -> Optional[OscalBaseModel]:
        """Get the preceding element in the path."""
        preceding_path = element_path.get_preceding_path()
        preceding_elm: Optional[OscalBaseModel] = self.get_at(preceding_path)
        return preceding_elm

    def _get_sub_element_obj(self, sub_element):
        """Convert sub element into allowed model obj."""
        if not self.is_allowed_sub_element_type(sub_element):
            raise TrestleError(
                f'Sub element must be one of "{self.get_allowed_sub_element_types()}", found "{sub_element.__class__}"'
            )

        model_obj = sub_element
        if isinstance(sub_element, Element):
            model_obj = sub_element.get()

        return model_obj

    def set_at(self, element_path: ElementPath, sub_element: OscalBaseModel) -> 'Element':
        """Set a sub_element at the path in the current element.

        Sub element can be Element, OscalBaseModel, list or None type
        It returns the element itself so that chaining operation can be done such as
            `element.set_at(path, sub-element).get()`.
        """
        # convert the element_path to ElementPath if needed
        if isinstance(element_path, str):
            element_path = ElementPath(element_path)

        # convert sub-element to OscalBaseModel if needed
        model_obj = self._get_sub_element_obj(sub_element)

        # find the root-model and element path parts
        _, path_parts = self._split_element_path(element_path)

        # TODO validate that self._elem is of same type as root_model

        # If wildcard is present, check the input type and determine the preceding element
        if element_path.get_last() == ElementPath.WILDCARD:
            # validate the type is either list or OscalBaseModel
            if not isinstance(model_obj, list) and not isinstance(model_obj, OscalBaseModel):
                raise TrestleError(
                    f'The model object needs to be a List or OscalBaseModel for path with "{ElementPath.WILDCARD}"'
                )

            # since wildcard * is there, we need to go one level up for preceding element in the path
            preceding_elm = self.get_preceding_element(element_path.get_preceding_path())
        else:
            # get the preceding element in the path
            preceding_elm = self.get_preceding_element(element_path)

        if preceding_elm is None:
            raise TrestleError(f'Invalid sub element path {element_path} with no valid preceding element')

        # check if it can be a valid sub_element of the parent
        sub_element_name = element_path.get_element_name().replace('-', '_')
        if hasattr(preceding_elm, sub_element_name) is False:
            raise TrestleError(
                f'Element "{preceding_elm.__class__}" does not have the attribute "{sub_element_name}" '
                f'of type "{model_obj.__class__}"'
            )

        # set the sub-element
        try:
            setattr(preceding_elm, sub_element_name, model_obj)
        except ValidationError:
            sub_element_class = self.get_sub_element_class(preceding_elm, sub_element_name)
            raise TrestleError(
                f'Validation error: {sub_element_name} is expected to be "{sub_element_class}", '
                f'but found "{model_obj.__class__}"'
            )

        # returning self will allow to do 'chaining' of commands after set
        return self

    def to_yaml(self) -> str:
        """Convert into YAML string."""
        yaml = YAML(typ='safe')
        yaml.default_flow_style = False
        from io import StringIO
        string_stream = StringIO()
        yaml.dump(yaml.load(self.to_json(pretty=False)), string_stream)
        yaml_data = string_stream.getvalue()
        string_stream.close()

        return yaml_data

    def to_json(self, pretty: bool = True) -> str:
        """Convert into JSON string."""
        if self._wrapper_alias == self.IGNORE_WRAPPER_ALIAS:
            json_data = self._elem.oscal_serialize_json(pretty=pretty, wrapped=False)

        else:
            # Note before trying to edit this
            # This transient model allows self._elem not be an OscalBaseModel (e.g. a DICT or LIST)
            # typing need to be clarified.
            if isinstance(self._elem, OscalBaseModel):
                json_data = self._elem.oscal_serialize_json(pretty=pretty)
            else:
                dynamic_passer = {}
                dynamic_passer['TransientField'] = (self._elem.__class__, Field(self, alias=self._wrapper_alias))
                wrapper_model = create_model('TransientModel', __base__=OscalBaseModel, **dynamic_passer)
                wrapped_model = wrapper_model.construct(**{self._wrapper_alias: self._elem})
                json_data = wrapped_model.oscal_serialize_json(pretty=pretty, wrapped=False)
        return json_data

    @classmethod
    def get_sub_element_class(cls, parent_elm: OscalBaseModel, sub_element_name: str):
        """Get the class of the sub-element."""
        sub_element_class = parent_elm.__fields__[sub_element_name].outer_type_
        return sub_element_class

    @classmethod
    def get_allowed_sub_element_types(cls) -> List[str]:
        """Get the list of allowed sub element types."""
        return cls._allowed_sub_element_types

    @classmethod
    def is_allowed_sub_element_type(cls, elm) -> bool:
        """Check if is of allowed sub element type."""
        # FIXME: The following logic does not use the _allowed_sub_element_types being defined for the class
        if (isinstance(elm, Element) or isinstance(elm, OscalBaseModel) or isinstance(elm, list)
                or isinstance(elm, dict) or elm is None):
            return True

        return False

    def __str__(self) -> str:
        """Return string representation of element."""
        return type(self._elem).__name__

    def __eq__(self, other: object) -> bool:
        """Check that two elements are equal."""
        if not isinstance(other, Element):
            return False

        return self.get() == other.get()
Attributes¤
IGNORE_WRAPPER_ALIAS = '__' class-attribute instance-attribute ¤
Functions¤
__eq__(other) ¤

Check that two elements are equal.

Source code in trestle/core/models/elements.py
608
609
610
611
612
613
def __eq__(self, other: object) -> bool:
    """Check that two elements are equal."""
    if not isinstance(other, Element):
        return False

    return self.get() == other.get()
__init__(elem, wrapper_alias='') ¤

Initialize an element wrapper.

wrapper_alias is the OSCAL alias for the given elem object and used for seriazation in to_json() method.

For example, - List[Catalog.Group] element should have wrapper alias 'groups' - Catalog element should have wrapper alias 'catalog'

wrapper_alias is deduced for collection type object

if wrapper_alias = IGNORE_WRAPPER_ALIAS, then it is ignored and assumed to be json-serializable during to_json()

Source code in trestle/core/models/elements.py
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
def __init__(self, elem: OscalBaseModel, wrapper_alias: str = ''):
    """Initialize an element wrapper.

    wrapper_alias is the OSCAL alias for the given elem object and used for seriazation in to_json() method.

    For example,
     - List[Catalog.Group] element should have wrapper alias 'groups'
     - Catalog element should have wrapper alias 'catalog'

    wrapper_alias is deduced for collection type object

    if wrapper_alias = IGNORE_WRAPPER_ALIAS, then it is ignored and assumed to be json-serializable during to_json()
    """
    # FIXME: There are instances where elem is a list.
    self._elem: OscalBaseModel = elem

    if wrapper_alias == '' and wrapper_alias != self.IGNORE_WRAPPER_ALIAS:
        class_name = elem.__class__.__name__
        if utils.is_collection_field_type(elem):
            class_name = self._get_singular_classname()
            if class_name is None:
                raise TrestleError(
                    f'wrapper_alias not found for a collection type object: {elem.__class__.__name__}'
                )
        wrapper_alias = str_utils.classname_to_alias(class_name, AliasMode.JSON)

    self._wrapper_alias: str = wrapper_alias
__str__() ¤

Return string representation of element.

Source code in trestle/core/models/elements.py
604
605
606
def __str__(self) -> str:
    """Return string representation of element."""
    return type(self._elem).__name__
get() ¤

Return the model object.

Source code in trestle/core/models/elements.py
415
416
417
def get(self) -> OscalBaseModel:
    """Return the model object."""
    return self._elem
get_allowed_sub_element_types() classmethod ¤

Get the list of allowed sub element types.

Source code in trestle/core/models/elements.py
589
590
591
592
@classmethod
def get_allowed_sub_element_types(cls) -> List[str]:
    """Get the list of allowed sub element types."""
    return cls._allowed_sub_element_types
get_at(element_path=None, check_parent=True) ¤

Get the element at the specified element path.

it will return the sub-model object at the path. Sub-model object can be of type OscalBaseModel or List

Source code in trestle/core/models/elements.py
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
def get_at(self,
           element_path: ElementPath = None,
           check_parent: bool = True) -> Union[OscalBaseModel, List[OscalBaseModel]]:
    """Get the element at the specified element path.

    it will return the sub-model object at the path. Sub-model object
    can be of type OscalBaseModel or List
    """
    if element_path is None:
        return self._elem

    # find the root-model and element path parts
    _, path_parts = self._split_element_path(element_path)

    # TODO validate that self._elem is of same type as root_model

    # initialize the starting element for search
    elm = self._elem
    if hasattr(elm, '__root__') and (isinstance(elm.__root__, dict) or isinstance(elm.__root__, list)):
        elm = elm.__root__

    # if parent exists and does not end with wildcard, use the parent as the starting element for search
    if check_parent and element_path.get_parent(
    ) is not None and element_path.get_parent().get_last() != ElementPath.WILDCARD:
        elm_at = self.get_at(element_path.get_parent())
        if elm_at is None:
            raise TrestleNotFoundError(f'Invalid parent path {element_path.get_parent()}')
        elm = elm_at

    # return the sub-element at the specified path
    for attr in path_parts:
        if elm is None:
            break

        # process for wildcard and array indexes

        if attr == ElementPath.WILDCARD:
            break
        elif attr.isnumeric():
            if isinstance(elm, list):
                elm = elm[int(attr)]
            else:
                # index to a non list type should return None
                return None
        else:
            elm = elm.get_field_value_by_alias(attr)

    return elm
get_preceding_element(element_path) ¤

Get the preceding element in the path.

Source code in trestle/core/models/elements.py
476
477
478
479
480
def get_preceding_element(self, element_path: ElementPath) -> Optional[OscalBaseModel]:
    """Get the preceding element in the path."""
    preceding_path = element_path.get_preceding_path()
    preceding_elm: Optional[OscalBaseModel] = self.get_at(preceding_path)
    return preceding_elm
get_sub_element_class(parent_elm, sub_element_name) classmethod ¤

Get the class of the sub-element.

Source code in trestle/core/models/elements.py
583
584
585
586
587
@classmethod
def get_sub_element_class(cls, parent_elm: OscalBaseModel, sub_element_name: str):
    """Get the class of the sub-element."""
    sub_element_class = parent_elm.__fields__[sub_element_name].outer_type_
    return sub_element_class
is_allowed_sub_element_type(elm) classmethod ¤

Check if is of allowed sub element type.

Source code in trestle/core/models/elements.py
594
595
596
597
598
599
600
601
602
@classmethod
def is_allowed_sub_element_type(cls, elm) -> bool:
    """Check if is of allowed sub element type."""
    # FIXME: The following logic does not use the _allowed_sub_element_types being defined for the class
    if (isinstance(elm, Element) or isinstance(elm, OscalBaseModel) or isinstance(elm, list)
            or isinstance(elm, dict) or elm is None):
        return True

    return False
set_at(element_path, sub_element) ¤

Set a sub_element at the path in the current element.

Sub element can be Element, OscalBaseModel, list or None type It returns the element itself so that chaining operation can be done such as element.set_at(path, sub-element).get().

Source code in trestle/core/models/elements.py
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
def set_at(self, element_path: ElementPath, sub_element: OscalBaseModel) -> 'Element':
    """Set a sub_element at the path in the current element.

    Sub element can be Element, OscalBaseModel, list or None type
    It returns the element itself so that chaining operation can be done such as
        `element.set_at(path, sub-element).get()`.
    """
    # convert the element_path to ElementPath if needed
    if isinstance(element_path, str):
        element_path = ElementPath(element_path)

    # convert sub-element to OscalBaseModel if needed
    model_obj = self._get_sub_element_obj(sub_element)

    # find the root-model and element path parts
    _, path_parts = self._split_element_path(element_path)

    # TODO validate that self._elem is of same type as root_model

    # If wildcard is present, check the input type and determine the preceding element
    if element_path.get_last() == ElementPath.WILDCARD:
        # validate the type is either list or OscalBaseModel
        if not isinstance(model_obj, list) and not isinstance(model_obj, OscalBaseModel):
            raise TrestleError(
                f'The model object needs to be a List or OscalBaseModel for path with "{ElementPath.WILDCARD}"'
            )

        # since wildcard * is there, we need to go one level up for preceding element in the path
        preceding_elm = self.get_preceding_element(element_path.get_preceding_path())
    else:
        # get the preceding element in the path
        preceding_elm = self.get_preceding_element(element_path)

    if preceding_elm is None:
        raise TrestleError(f'Invalid sub element path {element_path} with no valid preceding element')

    # check if it can be a valid sub_element of the parent
    sub_element_name = element_path.get_element_name().replace('-', '_')
    if hasattr(preceding_elm, sub_element_name) is False:
        raise TrestleError(
            f'Element "{preceding_elm.__class__}" does not have the attribute "{sub_element_name}" '
            f'of type "{model_obj.__class__}"'
        )

    # set the sub-element
    try:
        setattr(preceding_elm, sub_element_name, model_obj)
    except ValidationError:
        sub_element_class = self.get_sub_element_class(preceding_elm, sub_element_name)
        raise TrestleError(
            f'Validation error: {sub_element_name} is expected to be "{sub_element_class}", '
            f'but found "{model_obj.__class__}"'
        )

    # returning self will allow to do 'chaining' of commands after set
    return self
to_json(pretty=True) ¤

Convert into JSON string.

Source code in trestle/core/models/elements.py
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
def to_json(self, pretty: bool = True) -> str:
    """Convert into JSON string."""
    if self._wrapper_alias == self.IGNORE_WRAPPER_ALIAS:
        json_data = self._elem.oscal_serialize_json(pretty=pretty, wrapped=False)

    else:
        # Note before trying to edit this
        # This transient model allows self._elem not be an OscalBaseModel (e.g. a DICT or LIST)
        # typing need to be clarified.
        if isinstance(self._elem, OscalBaseModel):
            json_data = self._elem.oscal_serialize_json(pretty=pretty)
        else:
            dynamic_passer = {}
            dynamic_passer['TransientField'] = (self._elem.__class__, Field(self, alias=self._wrapper_alias))
            wrapper_model = create_model('TransientModel', __base__=OscalBaseModel, **dynamic_passer)
            wrapped_model = wrapper_model.construct(**{self._wrapper_alias: self._elem})
            json_data = wrapped_model.oscal_serialize_json(pretty=pretty, wrapped=False)
    return json_data
to_yaml() ¤

Convert into YAML string.

Source code in trestle/core/models/elements.py
552
553
554
555
556
557
558
559
560
561
562
def to_yaml(self) -> str:
    """Convert into YAML string."""
    yaml = YAML(typ='safe')
    yaml.default_flow_style = False
    from io import StringIO
    string_stream = StringIO()
    yaml.dump(yaml.load(self.to_json(pretty=False)), string_stream)
    yaml_data = string_stream.getvalue()
    string_stream.close()

    return yaml_data

ElementPath ¤

Element path wrapper of an element.

This only allows a single wildcard '*' at the end to denote elements of an array or dict

Source code in trestle/core/models/elements.py
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
class ElementPath:
    """Element path wrapper of an element.

    This only allows a single wildcard '*' at the end to denote elements of an array or dict
    """

    PATH_SEPARATOR: str = const.ALIAS_PATH_SEPARATOR

    WILDCARD: str = '*'

    def __init__(self, element_path: str, parent_path: Optional['ElementPath'] = None) -> None:
        """Initialize an element wrapper.

        It assumes the element path contains oscal field alias with hyphens only
        """
        self._parent_path = parent_path

        self._path: List[str] = self._parse(element_path)

        # Initialize private variables for lazy processing and caching
        self._element_name: Optional[str] = None
        self._preceding_path: Optional['ElementPath'] = None

    def _parse(self, element_path: str) -> List[str]:
        """Parse the element path and validate."""
        parts: List[str] = element_path.split(self.PATH_SEPARATOR)

        for part in parts:
            if part == '':
                raise TrestleError(
                    f'Invalid path "{element_path}" because there are empty path parts between "{self.PATH_SEPARATOR}" '
                    'or in the beginning'
                )

        if parts[0] == self.WILDCARD:
            raise TrestleError(f'Invalid path {element_path} with wildcard.')
        return parts

    def get(self) -> List[str]:
        """Return the path parts as a list."""
        return self._path

    def get_type(self, root_model: Optional[Type[Any]] = None, use_parent: bool = False) -> Type[Any]:
        """Get the type of an element.

        If possible the model type will be derived from one of the top level models,
        otherwise a 'root model' can be passed for situations where this is not possible.

        This type path should *NOT* have wild cards in it. It *may* have* indices.
        Valid Examples:
            catalog.metadata
            catalog.groups
            catalog.groups.group
            catalog
            catalog.groups.0

        Args:
            root_model: An OscalBaseModel Type from which to base the approach on.
            use_parent: Whether or not to normalise the full path across parent ElementPaths, default to not.

        Returns:
            The type of the model whether or not it is an OscalBaseModel or not.
        """
        effective_path: List[str]
        if use_parent:
            effective_path = self.get_full_path_parts()
        else:
            effective_path = self._path

        if not root_model:
            # lookup root model from top level oscal models or fail
            prev_model = self._top_level_type_lookup(effective_path[0])
        else:
            prev_model = root_model
        if len(effective_path) == 1:
            return prev_model
        # variables
        # for current_element_str in effective_path[1:]:
        for current_element_str in effective_path[1:]:
            # Determine if the parent model is a collection.
            if utils.is_collection_field_type(prev_model):
                inner_model = utils.get_inner_type(prev_model)
                inner_class_name = classname_to_alias(inner_model.__name__, AliasMode.JSON)
                # Assert that the current name fits an expected form.
                # Valid choices here are *, integer (for arrays) and the inner model alias
                if (inner_class_name == current_element_str or current_element_str == self.WILDCARD
                        or current_element_str.isnumeric()):
                    prev_model = inner_model

                else:
                    raise TrestleError('Unexpected key in element path when finding type.')

            else:
                # Indices, * are not allowed on non-collection types
                if current_element_str == self.WILDCARD:
                    raise TrestleError(
                        'Wild card in unexpected position when trying to find class type.'
                        + ' Element path type lookup can only occur where a single type can be identified.'
                    )
                prev_model = prev_model.alias_to_field_map()[current_element_str].outer_type_
        return prev_model

    def get_obm_wrapped_type(self,
                             root_model: Optional[Type[Any]] = None,
                             use_parent: bool = False) -> Type[OscalBaseModel]:
        """Get the type of the element. Wraps the collection type in an OscalBaseModel as a __root__ element.

        This should principally be used for validating content.

        Args:
            root_model: An OscalBaseModel Type from which to base the approach on.
            use_parent: Whether or not to normalise the full path across parent ElementPaths, default to not.

        Returns:
            The type of the model whether wrapped or not as an OscalBaseModel.
        """
        base_type = self.get_type(root_model, use_parent)
        # Get an outer model type.
        origin_type = utils.get_origin(base_type)

        if origin_type in [list, dict]:
            # OSCAL does not support collections of collections directly. We should not hit this scenario
            collection_name = self.get_last()
            if collection_name == self.WILDCARD:
                logger.critical('Unexpected error in type system when inferring type from element path.')
                logger.critical('Please report this issue.')
                raise TrestleError('Unknown error inferring type from element path.')
            # Final path must be the alias

            new_base_type = create_model(
                str_utils.alias_to_classname(collection_name, AliasMode.JSON),
                __base__=OscalBaseModel,
                __root__=(base_type, ...)
            )
            return new_base_type
        return base_type

    def _top_level_type_lookup(self, element_str: str) -> Type[common_types.TopLevelOscalModel]:
        """From an individual element tag, induce the type of the model.

        Args:
            element_str: individual element as text such as 'catalog' or 'profile'

        Returns:
            Top level object model such as catalog, profile etc.
        """
        # Even though awkward use chain of models.
        if element_str not in const.MODEL_TYPE_LIST:
            raise TrestleError(f'{element_str} is not a top level model (e.g. catalog, profile)')
        model_package = const.MODEL_TYPE_TO_MODEL_MODULE[element_str]
        object_type, _ = ModelUtils.get_root_model(model_package)
        object_type = cast(Type[common_types.TopLevelOscalModel], object_type)
        return object_type

    def is_multipart(self) -> bool:
        """Assert whether or not an element path is multiple parts.

        Originally element paths had to have multiple paths.
        This provides a check for higher level code that still has that requirement.

        Single part:
            catalog
            control
            assessment-results

        Multipart:
            catalog.metadata
            catalog.controls.control
        """
        return len(self._path) > 1

    def to_string(self) -> str:
        """Return the path parts as a dot-separated string."""
        return self.PATH_SEPARATOR.join(self.get())

    def get_parent(self) -> 'ElementPath':
        """Return the parent path.

        It can be None or a valid ElementPath
        """
        return self._parent_path

    def get_first(self) -> str:
        """Return the first part of the path."""
        return self._path[0]

    def get_last(self) -> str:
        """Return the last part of the path."""
        return self._path[-1]

    def get_full(self) -> str:
        """Return the full path including parent path parts as a dot separated str."""
        all_parts = self.get_full_path_parts()
        return self.PATH_SEPARATOR.join(all_parts)

    def get_element_name(self) -> str:
        """Return the element alias name from the path.

        Essentailly this the last part of the element path
        """
        # if it is available then return otherwise compute
        if self._element_name is None:
            element_name = self.get_last()
            if element_name == self.WILDCARD:
                element_name = self._path[-2]

            self._element_name = element_name

        return self._element_name

    def get_full_path_parts(self) -> List[str]:
        """Get full path parts to the element including parent path parts as a list."""
        path_parts = []
        if self.get_parent() is not None:
            parent_path_parts = self.get_parent().get_full_path_parts()
            path_parts.extend(parent_path_parts)
            path_parts.extend(self.get()[1:])  # don't use the first part
        else:
            path_parts.extend(self.get())

        return path_parts

    def get_preceding_path(self) -> 'ElementPath':
        """Return the element path to the preceding element in the path."""
        # if it is available then return otherwise compute
        if self._preceding_path is None:
            path_parts = self.get_full_path_parts()

            if len(path_parts) > 1:
                prec_path_parts = path_parts[:-1]
                self._preceding_path = ElementPath(self.PATH_SEPARATOR.join(prec_path_parts))

        return self._preceding_path

    def find_last_file_in_path(self, content_type: FileContentType, model_dir: pathlib.Path) -> pathlib.Path:
        """Find the last (nearest) existing file in the element path leading to this element."""
        # model dir is the top level dir for this model, e.g. catalogs/mycat
        path = model_dir
        extension = FileContentType.to_file_extension(content_type)
        good_model: pathlib.Path = None
        for element in self._path:
            if element == '*':
                break
            model_file = (path / element).with_suffix(extension)
            if not model_file.exists():
                break
            path = path / element
            good_model = model_file
        return good_model

    def make_absolute(self, model_dir: pathlib.Path, reference_dir: pathlib.Path):
        """Make the parts absolute from the top model dir."""
        # Match the current relative element path to the model directory and reference directory
        # If the element path is partial and doesn't connect to the top of the model,
        # need to deduce absolute element path from the model_dir and the reference directory
        # that corresponds to the root of the element path

        # if first element is a model type it is already absolute
        if self._path[0] not in const.MODEL_TYPE_LIST:
            rel_path = list(reference_dir.relative_to(model_dir).parts)
            rel_path.extend(self._path)
            self._path = rel_path

    def make_relative(self, model_relative_path: pathlib.Path) -> int:
        """Make the parts relative to the model path."""
        # The element path should currently be absolute
        # The model relative path should be relative to the top leve of the model
        # Change the element path to be relative to the model being loaded
        # Returns 0 on success and 1 on failur
        rel_path_parts = model_relative_path.parts[:-1]
        n_rel_parts = len(rel_path_parts)
        # the element path can't start above the model path
        if n_rel_parts >= len(self._path):
            return 1
        # confirm the leading parts match
        for ii in range(n_rel_parts):
            if rel_path_parts[ii] != self._path[ii]:
                return 1
        # chop off the leading parts of the absolute element path
        self._path = self._path[n_rel_parts:]
        return 0

    def to_file_path(self, content_type: FileContentType = None, root_dir: str = '') -> pathlib.Path:
        """Convert to a file or directory path for the element path.

        if content_type is not passed, it will return a path for directory
        """
        path_parts = self.get()

        # skip wildcard
        if path_parts[-1] == ElementPath.WILDCARD:
            path_parts = path_parts[:-1]

        if root_dir != '':
            path_parts[0] = root_dir

        path_str = '/'.join(path_parts)

        # add file extension if required
        # this will be omitted if it is a dir path
        if content_type is not None:
            file_extension = FileContentType.to_file_extension(content_type)
            path_str = path_str + file_extension

        # prepare the path
        file_path: pathlib.Path = pathlib.Path(f'./{path_str}')

        return file_path

    def to_root_path(self, content_type: FileContentType = None) -> pathlib.Path:
        """Convert to a file path for the element root."""
        path_str = f'./{self.get_first()}'
        if content_type is not None:
            file_extension = FileContentType.to_file_extension(content_type)
            path_str = path_str + file_extension

        file_path: pathlib.Path = pathlib.Path(path_str)
        return file_path

    def __str__(self) -> str:
        """Return string representation of element path."""
        return self.to_string()

    def __eq__(self, other) -> bool:
        """Override equality method."""
        if not isinstance(other, ElementPath):
            return False

        return self.get() == other.get()
Attributes¤
PATH_SEPARATOR: str = const.ALIAS_PATH_SEPARATOR class-attribute instance-attribute ¤
WILDCARD: str = '*' class-attribute instance-attribute ¤
Functions¤
__eq__(other) ¤

Override equality method.

Source code in trestle/core/models/elements.py
359
360
361
362
363
364
def __eq__(self, other) -> bool:
    """Override equality method."""
    if not isinstance(other, ElementPath):
        return False

    return self.get() == other.get()
__init__(element_path, parent_path=None) ¤

Initialize an element wrapper.

It assumes the element path contains oscal field alias with hyphens only

Source code in trestle/core/models/elements.py
46
47
48
49
50
51
52
53
54
55
56
57
def __init__(self, element_path: str, parent_path: Optional['ElementPath'] = None) -> None:
    """Initialize an element wrapper.

    It assumes the element path contains oscal field alias with hyphens only
    """
    self._parent_path = parent_path

    self._path: List[str] = self._parse(element_path)

    # Initialize private variables for lazy processing and caching
    self._element_name: Optional[str] = None
    self._preceding_path: Optional['ElementPath'] = None
__str__() ¤

Return string representation of element path.

Source code in trestle/core/models/elements.py
355
356
357
def __str__(self) -> str:
    """Return string representation of element path."""
    return self.to_string()
find_last_file_in_path(content_type, model_dir) ¤

Find the last (nearest) existing file in the element path leading to this element.

Source code in trestle/core/models/elements.py
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
def find_last_file_in_path(self, content_type: FileContentType, model_dir: pathlib.Path) -> pathlib.Path:
    """Find the last (nearest) existing file in the element path leading to this element."""
    # model dir is the top level dir for this model, e.g. catalogs/mycat
    path = model_dir
    extension = FileContentType.to_file_extension(content_type)
    good_model: pathlib.Path = None
    for element in self._path:
        if element == '*':
            break
        model_file = (path / element).with_suffix(extension)
        if not model_file.exists():
            break
        path = path / element
        good_model = model_file
    return good_model
get() ¤

Return the path parts as a list.

Source code in trestle/core/models/elements.py
74
75
76
def get(self) -> List[str]:
    """Return the path parts as a list."""
    return self._path
get_element_name() ¤

Return the element alias name from the path.

Essentailly this the last part of the element path

Source code in trestle/core/models/elements.py
231
232
233
234
235
236
237
238
239
240
241
242
243
244
def get_element_name(self) -> str:
    """Return the element alias name from the path.

    Essentailly this the last part of the element path
    """
    # if it is available then return otherwise compute
    if self._element_name is None:
        element_name = self.get_last()
        if element_name == self.WILDCARD:
            element_name = self._path[-2]

        self._element_name = element_name

    return self._element_name
get_first() ¤

Return the first part of the path.

Source code in trestle/core/models/elements.py
218
219
220
def get_first(self) -> str:
    """Return the first part of the path."""
    return self._path[0]
get_full() ¤

Return the full path including parent path parts as a dot separated str.

Source code in trestle/core/models/elements.py
226
227
228
229
def get_full(self) -> str:
    """Return the full path including parent path parts as a dot separated str."""
    all_parts = self.get_full_path_parts()
    return self.PATH_SEPARATOR.join(all_parts)
get_full_path_parts() ¤

Get full path parts to the element including parent path parts as a list.

Source code in trestle/core/models/elements.py
246
247
248
249
250
251
252
253
254
255
256
def get_full_path_parts(self) -> List[str]:
    """Get full path parts to the element including parent path parts as a list."""
    path_parts = []
    if self.get_parent() is not None:
        parent_path_parts = self.get_parent().get_full_path_parts()
        path_parts.extend(parent_path_parts)
        path_parts.extend(self.get()[1:])  # don't use the first part
    else:
        path_parts.extend(self.get())

    return path_parts
get_last() ¤

Return the last part of the path.

Source code in trestle/core/models/elements.py
222
223
224
def get_last(self) -> str:
    """Return the last part of the path."""
    return self._path[-1]
get_obm_wrapped_type(root_model=None, use_parent=False) ¤

Get the type of the element. Wraps the collection type in an OscalBaseModel as a root element.

This should principally be used for validating content.

Parameters:

Name Type Description Default
root_model Optional[Type[Any]]

An OscalBaseModel Type from which to base the approach on.

None
use_parent bool

Whether or not to normalise the full path across parent ElementPaths, default to not.

False

Returns:

Type Description
Type[OscalBaseModel]

The type of the model whether wrapped or not as an OscalBaseModel.

Source code in trestle/core/models/elements.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
def get_obm_wrapped_type(self,
                         root_model: Optional[Type[Any]] = None,
                         use_parent: bool = False) -> Type[OscalBaseModel]:
    """Get the type of the element. Wraps the collection type in an OscalBaseModel as a __root__ element.

    This should principally be used for validating content.

    Args:
        root_model: An OscalBaseModel Type from which to base the approach on.
        use_parent: Whether or not to normalise the full path across parent ElementPaths, default to not.

    Returns:
        The type of the model whether wrapped or not as an OscalBaseModel.
    """
    base_type = self.get_type(root_model, use_parent)
    # Get an outer model type.
    origin_type = utils.get_origin(base_type)

    if origin_type in [list, dict]:
        # OSCAL does not support collections of collections directly. We should not hit this scenario
        collection_name = self.get_last()
        if collection_name == self.WILDCARD:
            logger.critical('Unexpected error in type system when inferring type from element path.')
            logger.critical('Please report this issue.')
            raise TrestleError('Unknown error inferring type from element path.')
        # Final path must be the alias

        new_base_type = create_model(
            str_utils.alias_to_classname(collection_name, AliasMode.JSON),
            __base__=OscalBaseModel,
            __root__=(base_type, ...)
        )
        return new_base_type
    return base_type
get_parent() ¤

Return the parent path.

It can be None or a valid ElementPath

Source code in trestle/core/models/elements.py
211
212
213
214
215
216
def get_parent(self) -> 'ElementPath':
    """Return the parent path.

    It can be None or a valid ElementPath
    """
    return self._parent_path
get_preceding_path() ¤

Return the element path to the preceding element in the path.

Source code in trestle/core/models/elements.py
258
259
260
261
262
263
264
265
266
267
268
def get_preceding_path(self) -> 'ElementPath':
    """Return the element path to the preceding element in the path."""
    # if it is available then return otherwise compute
    if self._preceding_path is None:
        path_parts = self.get_full_path_parts()

        if len(path_parts) > 1:
            prec_path_parts = path_parts[:-1]
            self._preceding_path = ElementPath(self.PATH_SEPARATOR.join(prec_path_parts))

    return self._preceding_path
get_type(root_model=None, use_parent=False) ¤

Get the type of an element.

If possible the model type will be derived from one of the top level models, otherwise a 'root model' can be passed for situations where this is not possible.

This type path should NOT have wild cards in it. It may have* indices. Valid Examples: catalog.metadata catalog.groups catalog.groups.group catalog catalog.groups.0

Parameters:

Name Type Description Default
root_model Optional[Type[Any]]

An OscalBaseModel Type from which to base the approach on.

None
use_parent bool

Whether or not to normalise the full path across parent ElementPaths, default to not.

False

Returns:

Type Description
Type[Any]

The type of the model whether or not it is an OscalBaseModel or not.

Source code in trestle/core/models/elements.py
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
def get_type(self, root_model: Optional[Type[Any]] = None, use_parent: bool = False) -> Type[Any]:
    """Get the type of an element.

    If possible the model type will be derived from one of the top level models,
    otherwise a 'root model' can be passed for situations where this is not possible.

    This type path should *NOT* have wild cards in it. It *may* have* indices.
    Valid Examples:
        catalog.metadata
        catalog.groups
        catalog.groups.group
        catalog
        catalog.groups.0

    Args:
        root_model: An OscalBaseModel Type from which to base the approach on.
        use_parent: Whether or not to normalise the full path across parent ElementPaths, default to not.

    Returns:
        The type of the model whether or not it is an OscalBaseModel or not.
    """
    effective_path: List[str]
    if use_parent:
        effective_path = self.get_full_path_parts()
    else:
        effective_path = self._path

    if not root_model:
        # lookup root model from top level oscal models or fail
        prev_model = self._top_level_type_lookup(effective_path[0])
    else:
        prev_model = root_model
    if len(effective_path) == 1:
        return prev_model
    # variables
    # for current_element_str in effective_path[1:]:
    for current_element_str in effective_path[1:]:
        # Determine if the parent model is a collection.
        if utils.is_collection_field_type(prev_model):
            inner_model = utils.get_inner_type(prev_model)
            inner_class_name = classname_to_alias(inner_model.__name__, AliasMode.JSON)
            # Assert that the current name fits an expected form.
            # Valid choices here are *, integer (for arrays) and the inner model alias
            if (inner_class_name == current_element_str or current_element_str == self.WILDCARD
                    or current_element_str.isnumeric()):
                prev_model = inner_model

            else:
                raise TrestleError('Unexpected key in element path when finding type.')

        else:
            # Indices, * are not allowed on non-collection types
            if current_element_str == self.WILDCARD:
                raise TrestleError(
                    'Wild card in unexpected position when trying to find class type.'
                    + ' Element path type lookup can only occur where a single type can be identified.'
                )
            prev_model = prev_model.alias_to_field_map()[current_element_str].outer_type_
    return prev_model
is_multipart() ¤

Assert whether or not an element path is multiple parts.

Originally element paths had to have multiple paths. This provides a check for higher level code that still has that requirement.

Single part

catalog control assessment-results

Multipart

catalog.metadata catalog.controls.control

Source code in trestle/core/models/elements.py
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
def is_multipart(self) -> bool:
    """Assert whether or not an element path is multiple parts.

    Originally element paths had to have multiple paths.
    This provides a check for higher level code that still has that requirement.

    Single part:
        catalog
        control
        assessment-results

    Multipart:
        catalog.metadata
        catalog.controls.control
    """
    return len(self._path) > 1
make_absolute(model_dir, reference_dir) ¤

Make the parts absolute from the top model dir.

Source code in trestle/core/models/elements.py
286
287
288
289
290
291
292
293
294
295
296
297
def make_absolute(self, model_dir: pathlib.Path, reference_dir: pathlib.Path):
    """Make the parts absolute from the top model dir."""
    # Match the current relative element path to the model directory and reference directory
    # If the element path is partial and doesn't connect to the top of the model,
    # need to deduce absolute element path from the model_dir and the reference directory
    # that corresponds to the root of the element path

    # if first element is a model type it is already absolute
    if self._path[0] not in const.MODEL_TYPE_LIST:
        rel_path = list(reference_dir.relative_to(model_dir).parts)
        rel_path.extend(self._path)
        self._path = rel_path
make_relative(model_relative_path) ¤

Make the parts relative to the model path.

Source code in trestle/core/models/elements.py
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
def make_relative(self, model_relative_path: pathlib.Path) -> int:
    """Make the parts relative to the model path."""
    # The element path should currently be absolute
    # The model relative path should be relative to the top leve of the model
    # Change the element path to be relative to the model being loaded
    # Returns 0 on success and 1 on failur
    rel_path_parts = model_relative_path.parts[:-1]
    n_rel_parts = len(rel_path_parts)
    # the element path can't start above the model path
    if n_rel_parts >= len(self._path):
        return 1
    # confirm the leading parts match
    for ii in range(n_rel_parts):
        if rel_path_parts[ii] != self._path[ii]:
            return 1
    # chop off the leading parts of the absolute element path
    self._path = self._path[n_rel_parts:]
    return 0
to_file_path(content_type=None, root_dir='') ¤

Convert to a file or directory path for the element path.

if content_type is not passed, it will return a path for directory

Source code in trestle/core/models/elements.py
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
def to_file_path(self, content_type: FileContentType = None, root_dir: str = '') -> pathlib.Path:
    """Convert to a file or directory path for the element path.

    if content_type is not passed, it will return a path for directory
    """
    path_parts = self.get()

    # skip wildcard
    if path_parts[-1] == ElementPath.WILDCARD:
        path_parts = path_parts[:-1]

    if root_dir != '':
        path_parts[0] = root_dir

    path_str = '/'.join(path_parts)

    # add file extension if required
    # this will be omitted if it is a dir path
    if content_type is not None:
        file_extension = FileContentType.to_file_extension(content_type)
        path_str = path_str + file_extension

    # prepare the path
    file_path: pathlib.Path = pathlib.Path(f'./{path_str}')

    return file_path
to_root_path(content_type=None) ¤

Convert to a file path for the element root.

Source code in trestle/core/models/elements.py
345
346
347
348
349
350
351
352
353
def to_root_path(self, content_type: FileContentType = None) -> pathlib.Path:
    """Convert to a file path for the element root."""
    path_str = f'./{self.get_first()}'
    if content_type is not None:
        file_extension = FileContentType.to_file_extension(content_type)
        path_str = path_str + file_extension

    file_path: pathlib.Path = pathlib.Path(path_str)
    return file_path
to_string() ¤

Return the path parts as a dot-separated string.

Source code in trestle/core/models/elements.py
207
208
209
def to_string(self) -> str:
    """Return the path parts as a dot-separated string."""
    return self.PATH_SEPARATOR.join(self.get())

Functions¤

handler: python