Skip to content

cmd_utils

trestle.core.commands.common.cmd_utils ¤

Trestle command related utilities.

Classes¤

Functions¤

clear_folder(folder_path) ¤

Clear all contents of the specified folder.

Source code in trestle/core/commands/common/cmd_utils.py
41
42
43
44
45
46
47
48
def clear_folder(folder_path: pathlib.Path) -> None:
    """Clear all contents of the specified folder."""
    if not folder_path.exists() or not folder_path.is_dir():
        return
    try:
        shutil.rmtree(folder_path)
    except OSError as e:  # pragma: no cover
        raise TrestleError(f'Error deleting contents of {folder_path}: {e}')

model_type_is_too_granular(model_type) ¤

Is an model_type too fine to split.

Source code in trestle/core/commands/common/cmd_utils.py
30
31
32
33
34
35
36
37
38
def model_type_is_too_granular(model_type: Type[Any]) -> bool:
    """Is an model_type too fine to split."""
    if type_utils.is_collection_field_type(model_type):
        return False
    if hasattr(model_type, '__fields__') and '__root__' in model_type.__fields__:
        return True
    if model_type.__name__ in ['str', 'ConstrainedStrValue', 'int', 'float', 'datetime']:
        return True
    return False

parse_chain(model_obj, path_parts, relative_path=None) ¤

Parse the model chain starting from the beginning.

Parameters:

Name Type Description Default
model_obj Union[OscalBaseModel, None]

Model to use for inspecting available elements, if available or none

required
path_parts List[str]

list of string paths to parse including wildcards

required
relative_path Optional[Path]

Optional relative path (w.r.t trestle workspace root directory)

None

Returns:

Type Description
List[ElementPath]

List of ElementPath

Source code in trestle/core/commands/common/cmd_utils.py
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
def parse_chain(
    model_obj: Union[OscalBaseModel, None],
    path_parts: List[str],
    relative_path: Optional[pathlib.Path] = None
) -> List[ElementPath]:
    """Parse the model chain starting from the beginning.

    Args:
        model_obj: Model to use for inspecting available elements, if available or none
        path_parts: list of string paths to parse including wildcards
        relative_path: Optional relative path (w.r.t trestle workspace root directory)

    Returns:
        List of ElementPath
    """
    element_paths: List[ElementPath] = []
    sub_model = model_obj
    have_model_to_parse = model_obj is not None

    prev_element_path = None
    latest_path = None
    parent_model = path_parts[0]
    i = 1
    while i < len(path_parts):
        p = path_parts[i]

        # if hit wildcard create element path up to this point
        if p == ElementPath.WILDCARD and len(element_paths) > 0:
            # append wildcard to the latest element path
            latest_path = element_paths.pop()
            if latest_path.get_last() == ElementPath.WILDCARD:
                raise TrestleError(f'Invalid element path with consecutive {ElementPath.WILDCARD}')

            latest_path_str = ElementPath.PATH_SEPARATOR.join([latest_path.to_string(), p])
            element_path = ElementPath(latest_path_str, latest_path.get_parent())
        else:
            # create and append element_path
            # at this point sub_model may be a list of items
            # new element path is needed only if any of the items contains the desired part
            if p != ElementPath.WILDCARD:
                new_attrib = str_utils.dash_to_underscore(p)
                if isinstance(sub_model, list):
                    for item in sub_model:
                        # go into the list and find one with requested part
                        sub_item = getattr(item, new_attrib, None)
                        if sub_item is not None:
                            sub_model = sub_item
                            break
                else:
                    sub_model = getattr(sub_model, new_attrib, None)
            if have_model_to_parse and sub_model is None:
                return element_paths
            p = ElementPath.PATH_SEPARATOR.join([parent_model, p])
            element_path = ElementPath(p, parent_path=prev_element_path)

        # If the path has wildcard and there are more parts later,
        # get the parent model for the alias path
        # If path has wildcard and it does not refer to a list, then there can be nothing after *
        if element_path.get_last() == ElementPath.WILDCARD:
            full_path_str = ElementPath.PATH_SEPARATOR.join(element_path.get_full_path_parts()[:-1])
            parent_model = ModelUtils.get_singular_alias(full_path_str, relative_path)
            # Does wildcard mean we need to inspect the sub_model to determine what can be split off from it?
            # If it has __root__ it may mean it contains a list of objects and should be split as a list
            if isinstance(sub_model, OscalBaseModel):
                root = getattr(sub_model, '__root__', None)
                if root is None or not isinstance(root, list):
                    # Cannot have parts beyond * if it isn't a list
                    if i < len(path_parts) - 1:
                        raise TrestleError(
                            f'Cannot split beyond * when the wildcard does not refer to a list.  Path: {path_parts}'
                        )
                    for key in sub_model.__fields__.keys():
                        # only create element path is item is present in the sub_model
                        if getattr(sub_model, key, None) is None:
                            continue
                        new_alias = str_utils.underscore_to_dash(key)
                        new_path = full_path_str + '.' + new_alias
                        if not split_is_too_fine(new_path, model_obj):
                            # to add parts of an element, need to add two links
                            # prev_element_path may be None, for example catalog.*
                            if prev_element_path is not None:
                                element_paths.append(prev_element_path)
                            element_paths.append(ElementPath(parent_model + '.' + new_alias, latest_path))
                    # Since wildcard is last in the chain when splitting an oscal model we are done
                    return element_paths
        else:
            parent_model = element_path.get_element_name()

        # store values for next cycle
        prev_element_path = element_path
        element_paths.append(element_path)
        i += 1
    return element_paths

parse_element_arg(model_obj, element_arg, relative_path=None) ¤

Parse an element arg string into a list of ElementPath.

Parameters:

Name Type Description Default
model_obj Union[OscalBaseModel, None]

The OscalBaseModel being inspected to determine available elements that can be split

required
element_arg str

Single element path, as a string.

required
relative_path Optional[Path]

Optional relative path (from trestle root) used to validate element args are valid.

None

Returns: The requested parsed list of ElementPath for use in split

Source code in trestle/core/commands/common/cmd_utils.py
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
def parse_element_arg(
    model_obj: Union[OscalBaseModel, None],
    element_arg: str,
    relative_path: Optional[pathlib.Path] = None
) -> List[ElementPath]:
    """Parse an element arg string into a list of ElementPath.

    Args:
        model_obj: The OscalBaseModel being inspected to determine available elements that can be split
        element_arg: Single element path, as a string.
        relative_path: Optional relative path (from trestle root) used to validate element args are valid.
    Returns:
        The requested parsed list of ElementPath for use in split
    """
    element_arg = element_arg.strip()

    if element_arg == '*':
        raise TrestleError('Invalid element path containing only a single wildcard.')

    if element_arg == '':
        raise TrestleError('Invalid element path is empty string.')

    # search for wildcards and create paths with its parent path
    path_parts = element_arg.split(ElementPath.PATH_SEPARATOR)
    if len(path_parts) <= 1:
        raise TrestleError(f'Invalid element path "{element_arg}" with only one element and no wildcard')

    element_paths = parse_chain(model_obj, path_parts, relative_path)

    if len(element_paths) <= 0:
        # don't complain if nothing to split
        pass

    return element_paths

parse_element_args(model, element_args, relative_path=None) ¤

Parse element args into a list of ElementPath.

The element paths are either simple links of two elements, or two elements followed by *. The * represents either a list of the items in that element, or a splitting of that element into its parts. The only parts split off are the non-trivial ones determined by the granularity check.

contextual_mode specifies if the path is a valid project model path or not. For example, if we are processing a metadata.parties.*, we need to know which metadata we are processing. If we pass contextual_mode=true, we can infer the root model by inspecting the file directory

If contextual_mode=False, then the path must include the full path, e.g. catalog.metadata.parties. instead of just metadata.parties.

When the * represents splitting a model rather than a list, the model is inspected for what parts are available, and for each new part two element paths are created, one for the parent to the current element, and another from the current element to the child.

A path may have multiple *'s, but only the final one can represent splitting a model.

Parameters:

Name Type Description Default
model Union[OscalBaseModel, None]

The OscalBaseModel being inspected to determine available elements that can be split

required
element_args List[str]

List of str representing links in the chain of element paths to be parsed

required
relative_path Optional[Path]

Optional relative path (from trestle root) used to validate element args are valid.

None

Returns: The requested parsed list of ElementPath for use in split

Source code in trestle/core/commands/common/cmd_utils.py
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
def parse_element_args(
    model: Union[OscalBaseModel, None],
    element_args: List[str],
    relative_path: Optional[pathlib.Path] = None
) -> List[ElementPath]:
    """Parse element args into a list of ElementPath.

    The element paths are either simple links of two elements, or two elements followed by *.
    The * represents either a list of the items in that element, or a splitting of that element into its parts.
    The only parts split off are the non-trivial ones determined by the granularity check.

    contextual_mode specifies if the path is a valid project model path or not. For example,
    if we are processing a metadata.parties.*, we need to know which metadata we are processing. If we pass
    contextual_mode=true, we can infer the root model by inspecting the file directory

    If contextual_mode=False, then the path must include the full path, e.g. catalog.metadata.parties.* instead of just
    metadata.parties.*

    When the * represents splitting a model rather than a list, the model is inspected for what parts are available,
    and for each new part two element paths are created, one for the parent to the current element, and another from
    the current element to the child.

    A path may have multiple *'s, but only the final one can represent splitting a model.

    Args:
        model: The OscalBaseModel being inspected to determine available elements that can be split
        element_args: List of str representing links in the chain of element paths to be parsed
        relative_path: Optional relative path (from trestle root) used to validate element args are valid.
    Returns:
        The requested parsed list of ElementPath for use in split
    """
    # collect all paths
    element_paths: List[ElementPath] = []
    for element_arg in element_args:
        paths = parse_element_arg(model, element_arg, relative_path)
        element_paths.extend(paths)

    return element_paths

split_is_too_fine(split_paths, model_obj) ¤

Determine if the element path list goes too fine, e.g. individual strings.

Source code in trestle/core/commands/common/cmd_utils.py
51
52
53
54
55
56
57
58
def split_is_too_fine(split_paths: str, model_obj: OscalBaseModel) -> bool:
    """Determine if the element path list goes too fine, e.g. individual strings."""
    for split_path in split_paths.split(','):
        # find model type one level above if finishing with '.*'
        model_type = ElementPath(split_path.rstrip('.*')).get_type(type(model_obj))
        if model_type_is_too_granular(model_type):
            return True
    return False

to_model_file_name(model_obj, file_prefix, content_type) ¤

Return the file name for the item.

Source code in trestle/core/commands/common/cmd_utils.py
232
233
234
235
236
237
def to_model_file_name(model_obj: OscalBaseModel, file_prefix: str, content_type: FileContentType) -> str:
    """Return the file name for the item."""
    file_ext = FileContentType.to_file_extension(content_type)
    model_type = classname_to_alias(type(model_obj).__name__, AliasMode.JSON)
    file_name = f'{file_prefix}{const.IDX_SEP}{model_type}{file_ext}'
    return file_name

handler: python