ara-cli 0.1.13.3__py3-none-any.whl → 0.1.14.0__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.
- ara_cli/__init__.py +1 -1
- ara_cli/ara_command_action.py +162 -112
- ara_cli/ara_config.py +1 -1
- ara_cli/ara_subcommands/convert.py +66 -2
- ara_cli/ara_subcommands/prompt.py +266 -106
- ara_cli/artefact_autofix.py +2 -2
- ara_cli/artefact_converter.py +152 -53
- ara_cli/artefact_creator.py +41 -17
- ara_cli/artefact_lister.py +3 -3
- ara_cli/artefact_models/artefact_model.py +1 -1
- ara_cli/artefact_models/artefact_templates.py +0 -9
- ara_cli/artefact_models/feature_artefact_model.py +8 -8
- ara_cli/artefact_reader.py +62 -43
- ara_cli/artefact_scan.py +39 -17
- ara_cli/chat.py +23 -15
- ara_cli/children_contribution_updater.py +737 -0
- ara_cli/classifier.py +34 -0
- ara_cli/commands/load_command.py +4 -3
- ara_cli/commands/load_image_command.py +1 -1
- ara_cli/commands/read_command.py +23 -27
- ara_cli/completers.py +24 -0
- ara_cli/error_handler.py +26 -11
- ara_cli/file_loaders/document_reader.py +0 -178
- ara_cli/file_loaders/factories/__init__.py +0 -0
- ara_cli/file_loaders/factories/document_reader_factory.py +32 -0
- ara_cli/file_loaders/factories/file_loader_factory.py +27 -0
- ara_cli/file_loaders/file_loader.py +1 -30
- ara_cli/file_loaders/loaders/__init__.py +0 -0
- ara_cli/file_loaders/{document_file_loader.py → loaders/document_file_loader.py} +1 -1
- ara_cli/file_loaders/loaders/text_file_loader.py +47 -0
- ara_cli/file_loaders/readers/__init__.py +0 -0
- ara_cli/file_loaders/readers/docx_reader.py +49 -0
- ara_cli/file_loaders/readers/excel_reader.py +27 -0
- ara_cli/file_loaders/{markdown_reader.py → readers/markdown_reader.py} +1 -1
- ara_cli/file_loaders/readers/odt_reader.py +59 -0
- ara_cli/file_loaders/readers/pdf_reader.py +54 -0
- ara_cli/file_loaders/readers/pptx_reader.py +104 -0
- ara_cli/file_loaders/tools/__init__.py +0 -0
- ara_cli/output_suppressor.py +53 -0
- ara_cli/prompt_handler.py +123 -17
- ara_cli/tag_extractor.py +8 -7
- ara_cli/version.py +1 -1
- {ara_cli-0.1.13.3.dist-info → ara_cli-0.1.14.0.dist-info}/METADATA +18 -12
- {ara_cli-0.1.13.3.dist-info → ara_cli-0.1.14.0.dist-info}/RECORD +58 -45
- {ara_cli-0.1.13.3.dist-info → ara_cli-0.1.14.0.dist-info}/WHEEL +1 -1
- tests/test_artefact_converter.py +1 -46
- tests/test_artefact_lister.py +11 -8
- tests/test_chat.py +4 -4
- tests/test_chat_givens_images.py +1 -1
- tests/test_children_contribution_updater.py +98 -0
- tests/test_document_loader_office.py +267 -0
- tests/test_prompt_handler.py +416 -214
- tests/test_setup_default_chat_prompt_mode.py +198 -0
- tests/test_tag_extractor.py +95 -49
- ara_cli/file_loaders/document_readers.py +0 -233
- ara_cli/file_loaders/file_loaders.py +0 -123
- ara_cli/file_loaders/text_file_loader.py +0 -187
- /ara_cli/file_loaders/{binary_file_loader.py → loaders/binary_file_loader.py} +0 -0
- /ara_cli/file_loaders/{image_processor.py → tools/image_processor.py} +0 -0
- {ara_cli-0.1.13.3.dist-info → ara_cli-0.1.14.0.dist-info}/entry_points.txt +0 -0
- {ara_cli-0.1.13.3.dist-info → ara_cli-0.1.14.0.dist-info}/top_level.txt +0 -0
ara_cli/artefact_creator.py
CHANGED
|
@@ -27,11 +27,21 @@ class ArtefactCreator:
|
|
|
27
27
|
raise ValueError("classifier must not be None or empty!")
|
|
28
28
|
|
|
29
29
|
# Standard prompt log artefact
|
|
30
|
-
self._copy_template_file(
|
|
30
|
+
self._copy_template_file(
|
|
31
|
+
dir_path,
|
|
32
|
+
template_path,
|
|
33
|
+
f"template.{classifier}.prompt_log.md",
|
|
34
|
+
f"{classifier}.prompt_log.md",
|
|
35
|
+
)
|
|
31
36
|
|
|
32
37
|
# Additional prompt log artefact for 'feature' classifier
|
|
33
|
-
if classifier ==
|
|
34
|
-
self._copy_template_file(
|
|
38
|
+
if classifier == "feature":
|
|
39
|
+
self._copy_template_file(
|
|
40
|
+
dir_path,
|
|
41
|
+
template_path,
|
|
42
|
+
"template.steps.prompt_log.md",
|
|
43
|
+
"steps.prompt_log.md",
|
|
44
|
+
)
|
|
35
45
|
|
|
36
46
|
def _copy_template_file(self, dir_path, template_path, source_name, dest_name):
|
|
37
47
|
source = Path(template_path) / source_name
|
|
@@ -41,7 +51,9 @@ class ArtefactCreator:
|
|
|
41
51
|
raise FileNotFoundError(f"Source file {source} not found!")
|
|
42
52
|
|
|
43
53
|
if not destination.parent.exists():
|
|
44
|
-
raise NotADirectoryError(
|
|
54
|
+
raise NotADirectoryError(
|
|
55
|
+
f"Destination directory {destination.parent} does not exist!"
|
|
56
|
+
)
|
|
45
57
|
|
|
46
58
|
copyfile(source, destination)
|
|
47
59
|
|
|
@@ -59,7 +71,9 @@ class ArtefactCreator:
|
|
|
59
71
|
|
|
60
72
|
def handle_existing_files(self, file_exists):
|
|
61
73
|
if file_exists:
|
|
62
|
-
user_choice = input(
|
|
74
|
+
user_choice = input(
|
|
75
|
+
"File already exists. Do you want to overwrite the existing file and directory? (y/N): "
|
|
76
|
+
)
|
|
63
77
|
if user_choice.lower() != "y":
|
|
64
78
|
print("No changes were made to the existing file and directory.")
|
|
65
79
|
return False
|
|
@@ -68,14 +82,20 @@ class ArtefactCreator:
|
|
|
68
82
|
def validate_template(self, template_path, classifier):
|
|
69
83
|
template_name = f"template.{classifier}"
|
|
70
84
|
if not self.template_exists(template_path, template_name):
|
|
71
|
-
raise FileNotFoundError(
|
|
85
|
+
raise FileNotFoundError(
|
|
86
|
+
f"Template file '{template_name}' not found in the specified template path."
|
|
87
|
+
)
|
|
72
88
|
|
|
73
|
-
def set_artefact_parent(
|
|
74
|
-
|
|
89
|
+
def set_artefact_parent(
|
|
90
|
+
self, artefact, parent_classifier, parent_file_name
|
|
91
|
+
) -> Artefact:
|
|
92
|
+
classified_artefacts = ArtefactReader(self.file_system).read_artefacts()
|
|
75
93
|
if parent_classifier not in classified_artefacts:
|
|
76
94
|
return artefact
|
|
77
95
|
artefact_list = classified_artefacts[parent_classifier]
|
|
78
|
-
matching_artefacts = list(
|
|
96
|
+
matching_artefacts = list(
|
|
97
|
+
filter(lambda a: a.file_name == parent_file_name, artefact_list)
|
|
98
|
+
)
|
|
79
99
|
if not matching_artefacts:
|
|
80
100
|
artefact_names_list = [a.file_name for a in artefact_list]
|
|
81
101
|
suggest_close_name_matches(parent_file_name, artefact_names_list)
|
|
@@ -83,17 +103,23 @@ class ArtefactCreator:
|
|
|
83
103
|
artefact._parent = matching_artefacts[0]
|
|
84
104
|
return artefact
|
|
85
105
|
|
|
86
|
-
def run(
|
|
106
|
+
def run(
|
|
107
|
+
self, filename, classifier, parent_classifier=None, parent_name=None, rule=None
|
|
108
|
+
):
|
|
87
109
|
# make sure this function is always called from the ara top level directory
|
|
88
110
|
original_directory = os.getcwd()
|
|
89
111
|
navigator = DirectoryNavigator()
|
|
90
112
|
navigator.navigate_to_target()
|
|
91
113
|
|
|
92
114
|
if not Classifier.is_valid_classifier(classifier):
|
|
93
|
-
raise ValueError(
|
|
115
|
+
raise ValueError(
|
|
116
|
+
"Invalid classifier provided. Please provide a valid classifier."
|
|
117
|
+
)
|
|
94
118
|
|
|
95
119
|
sub_directory = Classifier.get_sub_directory(classifier)
|
|
96
|
-
file_path = self.file_system.path.join(
|
|
120
|
+
file_path = self.file_system.path.join(
|
|
121
|
+
sub_directory, f"{filename}.{classifier}"
|
|
122
|
+
)
|
|
97
123
|
dir_path = self.file_system.path.join(sub_directory, f"{filename}.data")
|
|
98
124
|
|
|
99
125
|
file_exists = self.file_system.path.exists(file_path)
|
|
@@ -105,9 +131,7 @@ class ArtefactCreator:
|
|
|
105
131
|
|
|
106
132
|
if parent_classifier and parent_name:
|
|
107
133
|
artefact.set_contribution(
|
|
108
|
-
artefact_name=parent_name,
|
|
109
|
-
classifier=parent_classifier,
|
|
110
|
-
rule=rule
|
|
134
|
+
artefact_name=parent_name, classifier=parent_classifier, rule=rule
|
|
111
135
|
)
|
|
112
136
|
else:
|
|
113
137
|
artefact.set_contribution(None, None, None)
|
|
@@ -115,7 +139,7 @@ class ArtefactCreator:
|
|
|
115
139
|
artefact_content = artefact.serialize()
|
|
116
140
|
rmtree(dir_path, ignore_errors=True)
|
|
117
141
|
os.makedirs(dir_path, exist_ok=True)
|
|
118
|
-
with open(file_path,
|
|
142
|
+
with open(file_path, "w", encoding="utf-8") as artefact_file:
|
|
119
143
|
artefact_file.write(artefact_content)
|
|
120
144
|
|
|
121
145
|
relative_file_path = os.path.relpath(file_path, original_directory)
|
|
@@ -138,6 +162,6 @@ class ArtefactCreator:
|
|
|
138
162
|
with open(file_path, "r", encoding="utf-8") as file:
|
|
139
163
|
for line in file:
|
|
140
164
|
if line.strip().startswith(title):
|
|
141
|
-
return line.split(
|
|
165
|
+
return line.split(":")[1].strip()
|
|
142
166
|
|
|
143
167
|
raise ValueError(f"Title not found in the parent file {file_path}")
|
ara_cli/artefact_lister.py
CHANGED
|
@@ -28,7 +28,7 @@ class ArtefactLister:
|
|
|
28
28
|
def list_files(
|
|
29
29
|
self, tags=None, navigate_to_target=False, list_filter: ListFilter | None = None
|
|
30
30
|
):
|
|
31
|
-
artefact_list = ArtefactReader.read_artefacts(tags=tags)
|
|
31
|
+
artefact_list = ArtefactReader(self.file_system).read_artefacts(tags=tags)
|
|
32
32
|
artefact_list = self.filter_artefacts(artefact_list, list_filter)
|
|
33
33
|
|
|
34
34
|
filtered_artefact_list = {
|
|
@@ -54,7 +54,7 @@ class ArtefactLister:
|
|
|
54
54
|
)
|
|
55
55
|
|
|
56
56
|
artefacts_by_classifier = {classifier: []}
|
|
57
|
-
ArtefactReader.step_through_value_chain(
|
|
57
|
+
ArtefactReader(self.file_system).step_through_value_chain(
|
|
58
58
|
artefact_name=artefact_name,
|
|
59
59
|
classifier=classifier,
|
|
60
60
|
artefacts_by_classifier=artefacts_by_classifier,
|
|
@@ -79,7 +79,7 @@ class ArtefactLister:
|
|
|
79
79
|
artefact_name, [info["title"] for info in artefact_info]
|
|
80
80
|
)
|
|
81
81
|
|
|
82
|
-
child_artefacts = ArtefactReader.find_children(
|
|
82
|
+
child_artefacts = ArtefactReader(self.file_system).find_children(
|
|
83
83
|
artefact_name=artefact_name, classifier=classifier
|
|
84
84
|
)
|
|
85
85
|
|
|
@@ -185,7 +185,7 @@ class Artefact(BaseModel, ABC):
|
|
|
185
185
|
description="Optional list of tags (0-many)",
|
|
186
186
|
)
|
|
187
187
|
author: Optional[str] = Field(
|
|
188
|
-
default=
|
|
188
|
+
default=None,
|
|
189
189
|
description="Author of the artefact, must be a single entry of the form 'creator_<someone>'."
|
|
190
190
|
)
|
|
191
191
|
title: str = Field(
|
|
@@ -30,7 +30,6 @@ def _default_vision(title: str, use_default_contribution: bool) -> VisionArtefac
|
|
|
30
30
|
)
|
|
31
31
|
return VisionArtefact(
|
|
32
32
|
tags=[],
|
|
33
|
-
author="creator_unknown",
|
|
34
33
|
title=title,
|
|
35
34
|
description="<further optional description to understand the vision, markdown capable text formatting>",
|
|
36
35
|
intent=intent,
|
|
@@ -46,7 +45,6 @@ def _default_businessgoal(title: str, use_default_contribution: bool) -> Busines
|
|
|
46
45
|
)
|
|
47
46
|
return BusinessgoalArtefact(
|
|
48
47
|
tags=[],
|
|
49
|
-
author="creator_unknown",
|
|
50
48
|
title=title,
|
|
51
49
|
description="<further optional description to understand the businessgoal, markdown capable text formatting>",
|
|
52
50
|
intent=intent,
|
|
@@ -60,7 +58,6 @@ def _default_capability(title: str, use_default_contribution: bool) -> Capabilit
|
|
|
60
58
|
)
|
|
61
59
|
return CapabilityArtefact(
|
|
62
60
|
tags=[],
|
|
63
|
-
author="creator_unknown",
|
|
64
61
|
title=title,
|
|
65
62
|
description="<further optional description to understand the capability, markdown capable text formatting>",
|
|
66
63
|
intent=intent,
|
|
@@ -81,7 +78,6 @@ def _default_epic(title: str, use_default_contribution: bool) -> EpicArtefact:
|
|
|
81
78
|
]
|
|
82
79
|
return EpicArtefact(
|
|
83
80
|
tags=[],
|
|
84
|
-
author="creator_unknown",
|
|
85
81
|
title=title,
|
|
86
82
|
description="<further optional description to understand the epic, markdown capable text formatting>",
|
|
87
83
|
intent=intent,
|
|
@@ -103,7 +99,6 @@ def _default_userstory(title: str, use_default_contribution: bool) -> UserstoryA
|
|
|
103
99
|
]
|
|
104
100
|
return UserstoryArtefact(
|
|
105
101
|
tags=[],
|
|
106
|
-
author="creator_unknown",
|
|
107
102
|
title=title,
|
|
108
103
|
description="<further optional description to understand the userstory, markdown capable text formatting>",
|
|
109
104
|
intent=intent,
|
|
@@ -116,7 +111,6 @@ def _default_userstory(title: str, use_default_contribution: bool) -> UserstoryA
|
|
|
116
111
|
def _default_example(title: str, use_default_contribution: bool) -> ExampleArtefact:
|
|
117
112
|
return ExampleArtefact(
|
|
118
113
|
tags=[],
|
|
119
|
-
author="creator_unknown",
|
|
120
114
|
title=title,
|
|
121
115
|
description="<further optional description to understand the example, markdown capable text formatting>",
|
|
122
116
|
contribution=default_contribution() if use_default_contribution else None
|
|
@@ -137,7 +131,6 @@ def _default_keyfeature(title: str, use_default_contribution: bool) -> Keyfeatur
|
|
|
137
131
|
AND some other result is to be expected>"""
|
|
138
132
|
return KeyfeatureArtefact(
|
|
139
133
|
tags=[],
|
|
140
|
-
author="creator_unknown",
|
|
141
134
|
title=title,
|
|
142
135
|
description=description,
|
|
143
136
|
intent=intent,
|
|
@@ -193,7 +186,6 @@ def _default_feature(title: str, use_default_contribution: bool) -> FeatureArtef
|
|
|
193
186
|
|
|
194
187
|
return FeatureArtefact(
|
|
195
188
|
tags=[],
|
|
196
|
-
author="creator_unknown",
|
|
197
189
|
title=title,
|
|
198
190
|
description=description,
|
|
199
191
|
intent=intent,
|
|
@@ -224,7 +216,6 @@ def _default_issue(title: str, use_default_contribution: bool) -> IssueArtefact:
|
|
|
224
216
|
|
|
225
217
|
return IssueArtefact(
|
|
226
218
|
tags=[],
|
|
227
|
-
author="creator_unknown",
|
|
228
219
|
title=title,
|
|
229
220
|
description=description,
|
|
230
221
|
additional_description=additional_description,
|
|
@@ -149,10 +149,10 @@ class Scenario(BaseModel):
|
|
|
149
149
|
return steps
|
|
150
150
|
|
|
151
151
|
@model_validator(mode='after')
|
|
152
|
-
def check_no_placeholders(
|
|
152
|
+
def check_no_placeholders(self) -> 'Scenario':
|
|
153
153
|
"""Ensure regular scenarios don't contain placeholders that should be in scenario outlines."""
|
|
154
154
|
placeholders = set()
|
|
155
|
-
for step in
|
|
155
|
+
for step in self.steps:
|
|
156
156
|
# Skip validation if step contains docstring placeholders (during parsing)
|
|
157
157
|
if '__DOCSTRING_PLACEHOLDER_' in step:
|
|
158
158
|
continue
|
|
@@ -170,7 +170,7 @@ class Scenario(BaseModel):
|
|
|
170
170
|
f"Scenario Contains Placeholders ({placeholder_list}) but is not a Scenario Outline. "
|
|
171
171
|
f"Use 'Scenario Outline:' instead of 'Scenario:' and provide an Examples table."
|
|
172
172
|
)
|
|
173
|
-
return
|
|
173
|
+
return self
|
|
174
174
|
|
|
175
175
|
@classmethod
|
|
176
176
|
def from_lines(cls, lines: List[str], start_idx: int) -> Tuple['Scenario', int]:
|
|
@@ -219,18 +219,18 @@ class ScenarioOutline(BaseModel):
|
|
|
219
219
|
return v
|
|
220
220
|
|
|
221
221
|
@model_validator(mode='after')
|
|
222
|
-
def check_placeholders(
|
|
222
|
+
def check_placeholders(self) -> 'ScenarioOutline':
|
|
223
223
|
"""Ensure all placeholders in steps have corresponding values in examples."""
|
|
224
224
|
placeholders = set()
|
|
225
|
-
for step in
|
|
225
|
+
for step in self.steps:
|
|
226
226
|
found = re.findall(r'<([^>]+)>', step)
|
|
227
227
|
placeholders.update(found)
|
|
228
|
-
for example in
|
|
228
|
+
for example in self.examples:
|
|
229
229
|
missing = placeholders - set(example.values.keys())
|
|
230
230
|
if missing:
|
|
231
231
|
raise ValueError(
|
|
232
232
|
f"Example is missing values for placeholders: {missing}")
|
|
233
|
-
return
|
|
233
|
+
return self
|
|
234
234
|
|
|
235
235
|
@classmethod
|
|
236
236
|
def from_lines(cls, lines: List[str], start_idx: int) -> Tuple['ScenarioOutline', int]:
|
|
@@ -298,7 +298,7 @@ class FeatureArtefact(Artefact):
|
|
|
298
298
|
def validate_artefact_type(cls, v):
|
|
299
299
|
if v != ArtefactType.feature:
|
|
300
300
|
raise ValueError(
|
|
301
|
-
f"FeatureArtefact must have artefact_type of '{ArtefactType.feature}', not '{v}'")
|
|
301
|
+
f"FeatureArtefact must have artefact_type of '{ArtefactType.feature.value}', not '{v}'")
|
|
302
302
|
return v
|
|
303
303
|
|
|
304
304
|
@classmethod
|
ara_cli/artefact_reader.py
CHANGED
|
@@ -3,20 +3,29 @@ from ara_cli.classifier import Classifier
|
|
|
3
3
|
from ara_cli.file_classifier import FileClassifier
|
|
4
4
|
from ara_cli.artefact_models.artefact_model import Artefact
|
|
5
5
|
from ara_cli.artefact_models.artefact_load import artefact_from_content
|
|
6
|
-
from ara_cli.artefact_fuzzy_search import
|
|
6
|
+
from ara_cli.artefact_fuzzy_search import (
|
|
7
|
+
suggest_close_name_matches_for_parent,
|
|
8
|
+
suggest_close_name_matches,
|
|
9
|
+
)
|
|
7
10
|
from typing import Dict, List
|
|
8
11
|
import os
|
|
9
12
|
import re
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
class ArtefactReader:
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
def __init__(self, file_system=None):
|
|
17
|
+
self.file_system = file_system or os
|
|
18
|
+
|
|
19
|
+
def read_artefact_data(
|
|
20
|
+
self, artefact_name, classifier, classified_file_info=None
|
|
21
|
+
) -> tuple[str, dict[str, str]]:
|
|
15
22
|
if not Classifier.is_valid_classifier(classifier):
|
|
16
|
-
raise ValueError(
|
|
23
|
+
raise ValueError(
|
|
24
|
+
"Invalid classifier provided. Please provide a valid classifier."
|
|
25
|
+
)
|
|
17
26
|
|
|
18
27
|
if not classified_file_info:
|
|
19
|
-
file_classifier = FileClassifier(
|
|
28
|
+
file_classifier = FileClassifier(self.file_system)
|
|
20
29
|
classified_file_info = file_classifier.classify_files()
|
|
21
30
|
artefact_info_of_classifier = classified_file_info.get(classifier, [])
|
|
22
31
|
|
|
@@ -24,21 +33,21 @@ class ArtefactReader:
|
|
|
24
33
|
file_path = artefact_info["file_path"]
|
|
25
34
|
artefact_title = artefact_info["title"]
|
|
26
35
|
if artefact_title == artefact_name:
|
|
27
|
-
with open(file_path,
|
|
36
|
+
with open(file_path, "r", encoding="utf-8") as file:
|
|
28
37
|
content = file.read()
|
|
29
38
|
return content, artefact_info
|
|
30
39
|
|
|
31
40
|
all_artefact_names = [info["title"] for info in artefact_info_of_classifier]
|
|
32
|
-
suggest_close_name_matches(
|
|
33
|
-
artefact_name,
|
|
34
|
-
all_artefact_names
|
|
35
|
-
)
|
|
41
|
+
suggest_close_name_matches(artefact_name, all_artefact_names)
|
|
36
42
|
|
|
37
43
|
return None, None
|
|
38
44
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
def read_artefact(
|
|
46
|
+
self, artefact_name, classifier, classified_file_info=None
|
|
47
|
+
) -> Artefact:
|
|
48
|
+
content, artefact_info = self.read_artefact_data(
|
|
49
|
+
artefact_name, classifier, classified_file_info
|
|
50
|
+
)
|
|
42
51
|
if not content or not artefact_info:
|
|
43
52
|
return None
|
|
44
53
|
file_path = artefact_info["file_path"]
|
|
@@ -49,9 +58,11 @@ class ArtefactReader:
|
|
|
49
58
|
@staticmethod
|
|
50
59
|
def extract_parent_tree(artefact_content):
|
|
51
60
|
artefact_titles = Classifier.artefact_titles()
|
|
52
|
-
title_segment =
|
|
61
|
+
title_segment = "|".join(artefact_titles)
|
|
53
62
|
|
|
54
|
-
regex_pattern =
|
|
63
|
+
regex_pattern = (
|
|
64
|
+
rf"(?:Contributes to|Illustrates)\s*:*\s*(.*)\s+({title_segment}).*"
|
|
65
|
+
)
|
|
55
66
|
regex = re.compile(regex_pattern)
|
|
56
67
|
match = re.search(regex, artefact_content)
|
|
57
68
|
if not match:
|
|
@@ -72,33 +83,40 @@ class ArtefactReader:
|
|
|
72
83
|
merged[key].extend(artefacts)
|
|
73
84
|
return dict(merged)
|
|
74
85
|
|
|
75
|
-
|
|
76
|
-
|
|
86
|
+
def read_artefacts(
|
|
87
|
+
self, classified_artefacts=None, tags=None
|
|
88
|
+
) -> Dict[str, List[Artefact]]:
|
|
77
89
|
|
|
78
90
|
if classified_artefacts is None:
|
|
79
|
-
file_classifier = FileClassifier(file_system)
|
|
91
|
+
file_classifier = FileClassifier(self.file_system)
|
|
80
92
|
classified_artefacts = file_classifier.classify_files()
|
|
81
93
|
|
|
82
|
-
artefacts = {artefact_type: []
|
|
83
|
-
for artefact_type in classified_artefacts.keys()}
|
|
94
|
+
artefacts = {artefact_type: [] for artefact_type in classified_artefacts.keys()}
|
|
84
95
|
for artefact_type, artefact_info_dicts in classified_artefacts.items():
|
|
85
96
|
for artefact_info in artefact_info_dicts:
|
|
86
97
|
title = artefact_info["title"]
|
|
87
98
|
try:
|
|
88
|
-
artefact =
|
|
99
|
+
artefact = self.read_artefact(
|
|
100
|
+
title, artefact_type, classified_artefacts
|
|
101
|
+
)
|
|
89
102
|
artefacts[artefact_type].append(artefact)
|
|
90
103
|
except Exception as e:
|
|
91
104
|
error_handler.report_error(e, f"reading {artefact_type} '{title}'")
|
|
92
105
|
continue
|
|
93
106
|
return artefacts
|
|
94
107
|
|
|
95
|
-
|
|
96
|
-
|
|
108
|
+
def find_children(
|
|
109
|
+
self,
|
|
110
|
+
artefact_name,
|
|
111
|
+
classifier,
|
|
112
|
+
artefacts_by_classifier=None,
|
|
113
|
+
classified_artefacts=None,
|
|
114
|
+
):
|
|
97
115
|
artefacts_by_classifier = artefacts_by_classifier or {}
|
|
98
116
|
filtered_artefacts = {k: [] for k in artefacts_by_classifier.keys()}
|
|
99
117
|
|
|
100
118
|
if classified_artefacts is None:
|
|
101
|
-
classified_artefacts =
|
|
119
|
+
classified_artefacts = self.read_artefacts()
|
|
102
120
|
|
|
103
121
|
for artefact_classifier, artefacts in classified_artefacts.items():
|
|
104
122
|
for artefact in artefacts:
|
|
@@ -112,38 +130,37 @@ class ArtefactReader:
|
|
|
112
130
|
def _process_artefact(artefact, artefact_name, classifier, filtered_artefacts):
|
|
113
131
|
if not isinstance(artefact, Artefact):
|
|
114
132
|
return
|
|
115
|
-
contribution = getattr(artefact,
|
|
133
|
+
contribution = getattr(artefact, "contribution", None)
|
|
116
134
|
if not contribution:
|
|
117
135
|
return
|
|
118
|
-
if getattr(contribution,
|
|
136
|
+
if getattr(contribution, "artefact_name", None) != artefact_name:
|
|
119
137
|
return
|
|
120
|
-
if getattr(contribution,
|
|
138
|
+
if getattr(contribution, "classifier", None) != classifier:
|
|
121
139
|
return
|
|
122
140
|
|
|
123
|
-
file_classifier = getattr(artefact,
|
|
141
|
+
file_classifier = getattr(artefact, "_file_path", "").split(".")[-1]
|
|
124
142
|
if file_classifier not in filtered_artefacts:
|
|
125
143
|
filtered_artefacts[file_classifier] = []
|
|
126
144
|
filtered_artefacts[file_classifier].append(artefact)
|
|
127
145
|
|
|
128
|
-
@staticmethod
|
|
129
146
|
def step_through_value_chain(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
147
|
+
self,
|
|
148
|
+
artefact_name,
|
|
149
|
+
classifier,
|
|
150
|
+
artefacts_by_classifier=None,
|
|
151
|
+
classified_artefacts: dict[str, list["Artefact"]] | None = None,
|
|
134
152
|
):
|
|
135
153
|
from ara_cli.artefact_models.artefact_load import artefact_from_content
|
|
136
154
|
|
|
137
155
|
artefacts_by_classifier = artefacts_by_classifier or {}
|
|
138
156
|
|
|
139
157
|
if classified_artefacts is None:
|
|
140
|
-
classified_artefacts =
|
|
158
|
+
classified_artefacts = self.read_artefacts()
|
|
141
159
|
|
|
142
160
|
ArtefactReader._ensure_classifier_key(classifier, artefacts_by_classifier)
|
|
143
161
|
|
|
144
162
|
artefact = ArtefactReader._find_artefact_by_name(
|
|
145
|
-
artefact_name,
|
|
146
|
-
classified_artefacts.get(classifier, [])
|
|
163
|
+
artefact_name, classified_artefacts.get(classifier, [])
|
|
147
164
|
)
|
|
148
165
|
|
|
149
166
|
if not artefact or artefact in artefacts_by_classifier[classifier]:
|
|
@@ -151,7 +168,7 @@ class ArtefactReader:
|
|
|
151
168
|
|
|
152
169
|
artefacts_by_classifier[classifier].append(artefact)
|
|
153
170
|
|
|
154
|
-
parent = getattr(artefact,
|
|
171
|
+
parent = getattr(artefact, "contribution", None)
|
|
155
172
|
if not ArtefactReader._has_valid_parent(parent):
|
|
156
173
|
return
|
|
157
174
|
|
|
@@ -168,11 +185,11 @@ class ArtefactReader:
|
|
|
168
185
|
print()
|
|
169
186
|
return
|
|
170
187
|
|
|
171
|
-
|
|
188
|
+
self.step_through_value_chain(
|
|
172
189
|
artefact_name=parent_name,
|
|
173
190
|
classifier=parent_classifier,
|
|
174
191
|
artefacts_by_classifier=artefacts_by_classifier,
|
|
175
|
-
classified_artefacts=classified_artefacts
|
|
192
|
+
classified_artefacts=classified_artefacts,
|
|
176
193
|
)
|
|
177
194
|
|
|
178
195
|
@staticmethod
|
|
@@ -186,13 +203,15 @@ class ArtefactReader:
|
|
|
186
203
|
|
|
187
204
|
@staticmethod
|
|
188
205
|
def _has_valid_parent(parent):
|
|
189
|
-
return
|
|
206
|
+
return (
|
|
207
|
+
parent
|
|
208
|
+
and getattr(parent, "artefact_name", None)
|
|
209
|
+
and getattr(parent, "classifier", None)
|
|
210
|
+
)
|
|
190
211
|
|
|
191
212
|
@staticmethod
|
|
192
213
|
def _suggest_parent_name_match(artefact_name, all_artefact_names, parent_name):
|
|
193
214
|
if parent_name is not None:
|
|
194
215
|
suggest_close_name_matches_for_parent(
|
|
195
|
-
artefact_name,
|
|
196
|
-
all_artefact_names,
|
|
197
|
-
parent_name
|
|
216
|
+
artefact_name, all_artefact_names, parent_name
|
|
198
217
|
)
|
ara_cli/artefact_scan.py
CHANGED
|
@@ -4,12 +4,16 @@ import os
|
|
|
4
4
|
|
|
5
5
|
def is_contribution_valid(contribution, classified_artefact_info) -> bool:
|
|
6
6
|
from ara_cli.artefact_fuzzy_search import extract_artefact_names_of_classifier
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
if (
|
|
9
|
+
not contribution
|
|
10
|
+
or not contribution.artefact_name
|
|
11
|
+
or not contribution.classifier
|
|
12
|
+
):
|
|
8
13
|
return True
|
|
9
14
|
|
|
10
15
|
all_artefact_names = extract_artefact_names_of_classifier(
|
|
11
|
-
classified_files=classified_artefact_info,
|
|
12
|
-
classifier=contribution.classifier
|
|
16
|
+
classified_files=classified_artefact_info, classifier=contribution.classifier
|
|
13
17
|
)
|
|
14
18
|
if contribution.artefact_name not in all_artefact_names:
|
|
15
19
|
return False
|
|
@@ -19,12 +23,18 @@ def is_contribution_valid(contribution, classified_artefact_info) -> bool:
|
|
|
19
23
|
def is_rule_valid(contribution, classified_artefact_info) -> bool:
|
|
20
24
|
from ara_cli.artefact_reader import ArtefactReader
|
|
21
25
|
|
|
22
|
-
if
|
|
26
|
+
if (
|
|
27
|
+
not contribution
|
|
28
|
+
or not contribution.artefact_name
|
|
29
|
+
or not contribution.classifier
|
|
30
|
+
):
|
|
23
31
|
return True
|
|
24
32
|
rule = contribution.rule
|
|
25
33
|
if not rule:
|
|
26
34
|
return True
|
|
27
|
-
parent = ArtefactReader.read_artefact(
|
|
35
|
+
parent = ArtefactReader().read_artefact(
|
|
36
|
+
contribution.artefact_name, contribution.classifier
|
|
37
|
+
)
|
|
28
38
|
if not parent:
|
|
29
39
|
return True
|
|
30
40
|
rules = parent.rules
|
|
@@ -33,20 +43,26 @@ def is_rule_valid(contribution, classified_artefact_info) -> bool:
|
|
|
33
43
|
return True
|
|
34
44
|
|
|
35
45
|
|
|
36
|
-
def check_contribution(
|
|
46
|
+
def check_contribution(
|
|
47
|
+
contribution, classified_artefact_info, file_path
|
|
48
|
+
) -> tuple[bool, str]:
|
|
37
49
|
if not contribution:
|
|
38
50
|
return True, None
|
|
39
51
|
|
|
40
52
|
if not is_contribution_valid(contribution, classified_artefact_info):
|
|
41
|
-
reason = (
|
|
42
|
-
|
|
43
|
-
|
|
53
|
+
reason = (
|
|
54
|
+
f"Invalid Contribution Reference: The contribution references "
|
|
55
|
+
f"'{contribution.classifier}' artefact '{contribution.artefact_name}' "
|
|
56
|
+
f"which does not exist."
|
|
57
|
+
)
|
|
44
58
|
return False, reason
|
|
45
59
|
|
|
46
60
|
if not is_rule_valid(contribution, classified_artefact_info):
|
|
47
|
-
reason = (
|
|
48
|
-
|
|
49
|
-
|
|
61
|
+
reason = (
|
|
62
|
+
f"Rule Mismatch: The contribution references "
|
|
63
|
+
f"rule '{contribution.rule}' which the parent "
|
|
64
|
+
f"{contribution.classifier} '{contribution.artefact_name}' does not have."
|
|
65
|
+
)
|
|
50
66
|
return False, reason
|
|
51
67
|
return True, None
|
|
52
68
|
|
|
@@ -73,13 +89,17 @@ def check_file(file_path, artefact_class, classified_artefact_info=None):
|
|
|
73
89
|
|
|
74
90
|
# Check title and file name matching
|
|
75
91
|
if artefact_instance.title != file_name_without_ext:
|
|
76
|
-
reason = (
|
|
77
|
-
|
|
92
|
+
reason = (
|
|
93
|
+
f"Filename-Title Mismatch: The file name '{file_name_without_ext}' "
|
|
94
|
+
f"does not match the artefact title '{artefact_instance.title}'."
|
|
95
|
+
)
|
|
78
96
|
return False, reason
|
|
79
97
|
|
|
80
98
|
contribution = artefact_instance.contribution
|
|
81
99
|
|
|
82
|
-
contribution_valid, reason = check_contribution(
|
|
100
|
+
contribution_valid, reason = check_contribution(
|
|
101
|
+
contribution, classified_artefact_info, file_path
|
|
102
|
+
)
|
|
83
103
|
if not contribution_valid:
|
|
84
104
|
return False, reason
|
|
85
105
|
|
|
@@ -100,7 +120,9 @@ def find_invalid_files(classified_artefact_info, classifier):
|
|
|
100
120
|
continue
|
|
101
121
|
if ".data" in artefact_info["file_path"]:
|
|
102
122
|
continue
|
|
103
|
-
is_valid, reason = check_file(
|
|
123
|
+
is_valid, reason = check_file(
|
|
124
|
+
artefact_info["file_path"], artefact_class, classified_artefact_info
|
|
125
|
+
)
|
|
104
126
|
if not is_valid:
|
|
105
127
|
invalid_files.append((artefact_info["file_path"], reason))
|
|
106
128
|
return invalid_files
|
|
@@ -122,4 +144,4 @@ def show_results(invalid_artefacts):
|
|
|
122
144
|
report.write("\n")
|
|
123
145
|
if not has_issues:
|
|
124
146
|
print("All files are good!")
|
|
125
|
-
report.write("No problems found.\n")
|
|
147
|
+
report.write("No problems found.\n")
|