ara-cli 0.1.9.69__py3-none-any.whl → 0.1.10.8__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.
Potentially problematic release.
This version of ara-cli might be problematic. Click here for more details.
- ara_cli/__init__.py +18 -2
- ara_cli/__main__.py +248 -62
- ara_cli/ara_command_action.py +155 -86
- ara_cli/ara_config.py +226 -80
- ara_cli/ara_subcommands/__init__.py +0 -0
- ara_cli/ara_subcommands/autofix.py +26 -0
- ara_cli/ara_subcommands/chat.py +27 -0
- ara_cli/ara_subcommands/classifier_directory.py +16 -0
- ara_cli/ara_subcommands/common.py +100 -0
- ara_cli/ara_subcommands/create.py +75 -0
- ara_cli/ara_subcommands/delete.py +22 -0
- ara_cli/ara_subcommands/extract.py +22 -0
- ara_cli/ara_subcommands/fetch_templates.py +14 -0
- ara_cli/ara_subcommands/list.py +65 -0
- ara_cli/ara_subcommands/list_tags.py +25 -0
- ara_cli/ara_subcommands/load.py +48 -0
- ara_cli/ara_subcommands/prompt.py +136 -0
- ara_cli/ara_subcommands/read.py +47 -0
- ara_cli/ara_subcommands/read_status.py +20 -0
- ara_cli/ara_subcommands/read_user.py +20 -0
- ara_cli/ara_subcommands/reconnect.py +27 -0
- ara_cli/ara_subcommands/rename.py +22 -0
- ara_cli/ara_subcommands/scan.py +14 -0
- ara_cli/ara_subcommands/set_status.py +22 -0
- ara_cli/ara_subcommands/set_user.py +22 -0
- ara_cli/ara_subcommands/template.py +16 -0
- ara_cli/artefact_autofix.py +649 -68
- ara_cli/artefact_creator.py +8 -11
- ara_cli/artefact_deleter.py +2 -4
- ara_cli/artefact_fuzzy_search.py +22 -10
- ara_cli/artefact_link_updater.py +4 -4
- ara_cli/artefact_lister.py +29 -55
- ara_cli/artefact_models/artefact_data_retrieval.py +23 -0
- ara_cli/artefact_models/artefact_load.py +11 -3
- ara_cli/artefact_models/artefact_model.py +146 -39
- ara_cli/artefact_models/artefact_templates.py +70 -44
- ara_cli/artefact_models/businessgoal_artefact_model.py +23 -25
- ara_cli/artefact_models/epic_artefact_model.py +34 -26
- ara_cli/artefact_models/feature_artefact_model.py +203 -64
- ara_cli/artefact_models/keyfeature_artefact_model.py +21 -24
- ara_cli/artefact_models/serialize_helper.py +1 -1
- ara_cli/artefact_models/task_artefact_model.py +83 -15
- ara_cli/artefact_models/userstory_artefact_model.py +37 -27
- ara_cli/artefact_models/vision_artefact_model.py +23 -42
- ara_cli/artefact_reader.py +92 -91
- ara_cli/artefact_renamer.py +8 -4
- ara_cli/artefact_scan.py +66 -3
- ara_cli/chat.py +622 -162
- ara_cli/chat_agent/__init__.py +0 -0
- ara_cli/chat_agent/agent_communicator.py +62 -0
- ara_cli/chat_agent/agent_process_manager.py +211 -0
- ara_cli/chat_agent/agent_status_manager.py +73 -0
- ara_cli/chat_agent/agent_workspace_manager.py +76 -0
- ara_cli/commands/__init__.py +0 -0
- ara_cli/commands/command.py +7 -0
- ara_cli/commands/extract_command.py +15 -0
- ara_cli/commands/load_command.py +65 -0
- ara_cli/commands/load_image_command.py +34 -0
- ara_cli/commands/read_command.py +117 -0
- ara_cli/completers.py +144 -0
- ara_cli/directory_navigator.py +37 -4
- ara_cli/error_handler.py +134 -0
- ara_cli/file_classifier.py +6 -5
- ara_cli/file_lister.py +1 -1
- ara_cli/file_loaders/__init__.py +0 -0
- ara_cli/file_loaders/binary_file_loader.py +33 -0
- ara_cli/file_loaders/document_file_loader.py +34 -0
- ara_cli/file_loaders/document_reader.py +245 -0
- ara_cli/file_loaders/document_readers.py +233 -0
- ara_cli/file_loaders/file_loader.py +50 -0
- ara_cli/file_loaders/file_loaders.py +123 -0
- ara_cli/file_loaders/image_processor.py +89 -0
- ara_cli/file_loaders/markdown_reader.py +75 -0
- ara_cli/file_loaders/text_file_loader.py +187 -0
- ara_cli/global_file_lister.py +51 -0
- ara_cli/list_filter.py +1 -1
- ara_cli/output_suppressor.py +1 -1
- ara_cli/prompt_extractor.py +215 -88
- ara_cli/prompt_handler.py +521 -134
- ara_cli/prompt_rag.py +2 -2
- ara_cli/tag_extractor.py +83 -38
- ara_cli/template_loader.py +245 -0
- ara_cli/template_manager.py +18 -13
- ara_cli/templates/prompt-modules/commands/empty.commands.md +2 -12
- ara_cli/templates/prompt-modules/commands/extract_general.commands.md +12 -0
- ara_cli/templates/prompt-modules/commands/extract_markdown.commands.md +11 -0
- ara_cli/templates/prompt-modules/commands/extract_python.commands.md +13 -0
- ara_cli/templates/prompt-modules/commands/feature_add_or_modifiy_specified_behavior.commands.md +36 -0
- ara_cli/templates/prompt-modules/commands/feature_generate_initial_specified_bevahior.commands.md +53 -0
- ara_cli/templates/prompt-modules/commands/prompt_template_tech_stack_transformer.commands.md +95 -0
- ara_cli/templates/prompt-modules/commands/python_bug_fixing_code.commands.md +34 -0
- ara_cli/templates/prompt-modules/commands/python_generate_code.commands.md +27 -0
- ara_cli/templates/prompt-modules/commands/python_refactoring_code.commands.md +39 -0
- ara_cli/templates/prompt-modules/commands/python_step_definitions_generation_and_fixing.commands.md +40 -0
- ara_cli/templates/prompt-modules/commands/python_unittest_generation_and_fixing.commands.md +48 -0
- ara_cli/update_config_prompt.py +9 -3
- ara_cli/version.py +1 -1
- ara_cli-0.1.10.8.dist-info/METADATA +241 -0
- ara_cli-0.1.10.8.dist-info/RECORD +193 -0
- tests/test_ara_command_action.py +73 -59
- tests/test_ara_config.py +341 -36
- tests/test_artefact_autofix.py +1060 -0
- tests/test_artefact_link_updater.py +3 -3
- tests/test_artefact_lister.py +52 -132
- tests/test_artefact_renamer.py +2 -2
- tests/test_artefact_scan.py +327 -33
- tests/test_chat.py +2063 -498
- tests/test_file_classifier.py +24 -1
- tests/test_file_creator.py +3 -5
- tests/test_file_lister.py +1 -1
- tests/test_global_file_lister.py +131 -0
- tests/test_list_filter.py +2 -2
- tests/test_prompt_handler.py +746 -0
- tests/test_tag_extractor.py +19 -13
- tests/test_template_loader.py +192 -0
- tests/test_template_manager.py +5 -4
- tests/test_update_config_prompt.py +2 -2
- ara_cli/ara_command_parser.py +0 -327
- ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +0 -27
- ara_cli/templates/prompt-modules/blueprints/task_todo_list_implement_feature_BDD_way.blueprint.md +0 -30
- ara_cli/templates/prompt-modules/commands/artefact_classification.commands.md +0 -9
- ara_cli/templates/prompt-modules/commands/artefact_extension.commands.md +0 -17
- ara_cli/templates/prompt-modules/commands/artefact_formulation.commands.md +0 -14
- ara_cli/templates/prompt-modules/commands/behave_step_generation.commands.md +0 -102
- ara_cli/templates/prompt-modules/commands/code_generation_complex.commands.md +0 -20
- ara_cli/templates/prompt-modules/commands/code_generation_simple.commands.md +0 -13
- ara_cli/templates/prompt-modules/commands/error_fixing.commands.md +0 -20
- ara_cli/templates/prompt-modules/commands/feature_file_update.commands.md +0 -18
- ara_cli/templates/prompt-modules/commands/feature_formulation.commands.md +0 -43
- ara_cli/templates/prompt-modules/commands/js_code_generation_simple.commands.md +0 -13
- ara_cli/templates/prompt-modules/commands/refactoring.commands.md +0 -15
- ara_cli/templates/prompt-modules/commands/refactoring_analysis.commands.md +0 -9
- ara_cli/templates/prompt-modules/commands/reverse_engineer_feature_file.commands.md +0 -15
- ara_cli/templates/prompt-modules/commands/reverse_engineer_program_flow.commands.md +0 -19
- ara_cli/templates/template.businessgoal +0 -10
- ara_cli/templates/template.capability +0 -10
- ara_cli/templates/template.epic +0 -15
- ara_cli/templates/template.example +0 -6
- ara_cli/templates/template.feature +0 -26
- ara_cli/templates/template.issue +0 -14
- ara_cli/templates/template.keyfeature +0 -15
- ara_cli/templates/template.task +0 -6
- ara_cli/templates/template.userstory +0 -17
- ara_cli/templates/template.vision +0 -14
- ara_cli-0.1.9.69.dist-info/METADATA +0 -16
- ara_cli-0.1.9.69.dist-info/RECORD +0 -158
- tests/test_ara_autofix.py +0 -219
- {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/WHEEL +0 -0
- {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/entry_points.txt +0 -0
- {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/top_level.txt +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from ara_cli.artefact_models.artefact_model import Artefact, ArtefactType, Intent
|
|
2
|
-
from pydantic import Field, field_validator
|
|
2
|
+
from pydantic import Field, field_validator, model_validator
|
|
3
3
|
from typing import List, Tuple
|
|
4
4
|
|
|
5
5
|
|
|
@@ -47,39 +47,37 @@ class UserstoryIntent(Intent):
|
|
|
47
47
|
|
|
48
48
|
@classmethod
|
|
49
49
|
def deserialize_from_lines(cls, lines: List[str], start_index: int = 0) -> 'UserstoryIntent':
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
50
|
+
prefixes = [
|
|
51
|
+
("In order to ", "in_order_to"),
|
|
52
|
+
("As a ", "as_a"),
|
|
53
|
+
("As an ", "as_a"),
|
|
54
|
+
("I want ", "i_want"),
|
|
55
|
+
]
|
|
56
|
+
found = {"in_order_to": None, "as_a": None, "i_want": None}
|
|
57
|
+
|
|
58
|
+
def match_and_store(line):
|
|
59
|
+
for prefix, field in prefixes:
|
|
60
|
+
if line.startswith(prefix) and found[field] is None:
|
|
61
|
+
found[field] = line[len(prefix):].strip()
|
|
62
|
+
return True
|
|
63
|
+
return False
|
|
58
64
|
|
|
59
65
|
index = start_index
|
|
60
|
-
while index < len(lines) and (
|
|
61
|
-
|
|
62
|
-
if line.startswith(in_order_to_prefix) and not in_order_to:
|
|
63
|
-
in_order_to = line[len(in_order_to_prefix):].strip()
|
|
64
|
-
elif line.startswith(as_a_prefix) and not as_a:
|
|
65
|
-
as_a = line[len(as_a_prefix):].strip()
|
|
66
|
-
elif line.startswith(as_a_prefix_alt) and not as_a:
|
|
67
|
-
as_a = line[len(as_a_prefix_alt):].strip()
|
|
68
|
-
elif line.startswith(i_want_prefix) and not i_want:
|
|
69
|
-
i_want = line[len(i_want_prefix):].strip()
|
|
66
|
+
while index < len(lines) and any(v is None for v in found.values()):
|
|
67
|
+
match_and_store(lines[index].strip())
|
|
70
68
|
index += 1
|
|
71
69
|
|
|
72
|
-
if not in_order_to:
|
|
70
|
+
if not found["in_order_to"]:
|
|
73
71
|
raise ValueError("Could not find 'In order to' line")
|
|
74
|
-
if not as_a:
|
|
72
|
+
if not found["as_a"]:
|
|
75
73
|
raise ValueError("Could not find 'As a' line")
|
|
76
|
-
if not i_want:
|
|
74
|
+
if not found["i_want"]:
|
|
77
75
|
raise ValueError("Could not find 'I want' line")
|
|
78
76
|
|
|
79
77
|
return cls(
|
|
80
|
-
in_order_to=in_order_to,
|
|
81
|
-
as_a=as_a,
|
|
82
|
-
i_want=i_want
|
|
78
|
+
in_order_to=found["in_order_to"],
|
|
79
|
+
as_a=found["as_a"],
|
|
80
|
+
i_want=found["i_want"]
|
|
83
81
|
)
|
|
84
82
|
|
|
85
83
|
|
|
@@ -94,6 +92,18 @@ class UserstoryArtefact(Artefact):
|
|
|
94
92
|
default_factory=list,
|
|
95
93
|
description="Rules the userstory defines. It is recommended to create rules to clarify the desired outcome")
|
|
96
94
|
|
|
95
|
+
@model_validator(mode='after')
|
|
96
|
+
def check_for_misplaced_content(self) -> 'UserstoryArtefact':
|
|
97
|
+
if self.description:
|
|
98
|
+
desc_lines = self.description.split('\n')
|
|
99
|
+
for line in desc_lines:
|
|
100
|
+
stripped_line = line.strip()
|
|
101
|
+
if stripped_line.startswith("Rule:"):
|
|
102
|
+
raise ValueError("Found 'Rule:' inside description. Rules must be defined before the 'Description:' section.")
|
|
103
|
+
if stripped_line.startswith("Estimate:"):
|
|
104
|
+
raise ValueError("Found 'Estimate:' inside description. Estimate must be defined before the 'Description:' section.")
|
|
105
|
+
return self
|
|
106
|
+
|
|
97
107
|
@field_validator('artefact_type')
|
|
98
108
|
def validate_artefact_type(cls, v):
|
|
99
109
|
if v != ArtefactType.userstory:
|
|
@@ -173,7 +183,7 @@ class UserstoryArtefact(Artefact):
|
|
|
173
183
|
rules = self._serialize_rules()
|
|
174
184
|
|
|
175
185
|
lines = []
|
|
176
|
-
if self.tags
|
|
186
|
+
if tags: # Changed from self.tags to tags to include all tag types
|
|
177
187
|
lines.append(tags)
|
|
178
188
|
lines.append(title)
|
|
179
189
|
lines.append("")
|
|
@@ -190,4 +200,4 @@ class UserstoryArtefact(Artefact):
|
|
|
190
200
|
lines.append(description)
|
|
191
201
|
lines.append("")
|
|
192
202
|
|
|
193
|
-
return '\n'.join(lines)
|
|
203
|
+
return '\n'.join(lines)
|
|
@@ -76,54 +76,35 @@ class VisionIntent(Intent):
|
|
|
76
76
|
|
|
77
77
|
@classmethod
|
|
78
78
|
def deserialize_from_lines(cls, lines: List[str], start_index: int = 0) -> 'VisionIntent':
|
|
79
|
+
prefixes = [
|
|
80
|
+
("For ", "for_"),
|
|
81
|
+
("Who ", "who"),
|
|
82
|
+
("The ", "the"),
|
|
83
|
+
("That ", "that"),
|
|
84
|
+
("Unlike ", "unlike"),
|
|
85
|
+
("Our product ", "our_product"),
|
|
86
|
+
]
|
|
87
|
+
found = {field: "" for _, field in prefixes}
|
|
88
|
+
|
|
89
|
+
# Find the first "For " line, if it exists
|
|
79
90
|
intent_start_index = start_index
|
|
80
|
-
|
|
81
|
-
for_ = ""
|
|
82
|
-
who = ""
|
|
83
|
-
the = ""
|
|
84
|
-
that = ""
|
|
85
|
-
unlike = ""
|
|
86
|
-
our_product = ""
|
|
87
|
-
|
|
88
|
-
for_prefix = "For "
|
|
89
|
-
who_prefix = "Who "
|
|
90
|
-
the_prefix = "The "
|
|
91
|
-
that_prefix = "That "
|
|
92
|
-
unlike_prefix = "Unlike "
|
|
93
|
-
our_product_prefix = "Our product "
|
|
94
|
-
|
|
95
91
|
for i in range(start_index, len(lines)):
|
|
96
|
-
if lines[i].startswith(
|
|
92
|
+
if lines[i].startswith("For "):
|
|
97
93
|
intent_start_index = i
|
|
98
94
|
break
|
|
99
95
|
|
|
96
|
+
def match_and_store(line):
|
|
97
|
+
for prefix, field in prefixes:
|
|
98
|
+
if line.startswith(prefix) and not found[field]:
|
|
99
|
+
found[field] = line[len(prefix):].strip()
|
|
100
|
+
return
|
|
101
|
+
|
|
100
102
|
index = intent_start_index
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
index
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
index = index + 1
|
|
107
|
-
if index < len(lines) and lines[index].startswith(the_prefix):
|
|
108
|
-
the = lines[index][len(the_prefix):]
|
|
109
|
-
index = index + 1
|
|
110
|
-
if index < len(lines) and lines[index].startswith(that_prefix):
|
|
111
|
-
that = lines[index][len(that_prefix):]
|
|
112
|
-
index = index + 1
|
|
113
|
-
if index < len(lines) and lines[index].startswith(unlike_prefix):
|
|
114
|
-
unlike = lines[index][len(unlike_prefix):]
|
|
115
|
-
index = index + 1
|
|
116
|
-
if index < len(lines) and lines[index].startswith(our_product_prefix):
|
|
117
|
-
our_product = lines[index][len(our_product_prefix):]
|
|
118
|
-
|
|
119
|
-
return cls(
|
|
120
|
-
for_=for_,
|
|
121
|
-
who=who,
|
|
122
|
-
the=the,
|
|
123
|
-
that=that,
|
|
124
|
-
unlike=unlike,
|
|
125
|
-
our_product=our_product,
|
|
126
|
-
)
|
|
103
|
+
while index < len(lines) and any(not v for v in found.values()):
|
|
104
|
+
match_and_store(lines[index])
|
|
105
|
+
index += 1
|
|
106
|
+
|
|
107
|
+
return cls(**found)
|
|
127
108
|
|
|
128
109
|
|
|
129
110
|
class VisionArtefact(Artefact):
|
ara_cli/artefact_reader.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from . import error_handler
|
|
1
2
|
from ara_cli.classifier import Classifier
|
|
2
3
|
from ara_cli.file_classifier import FileClassifier
|
|
3
4
|
from ara_cli.artefact_models.artefact_model import Artefact
|
|
@@ -10,20 +11,20 @@ import re
|
|
|
10
11
|
|
|
11
12
|
class ArtefactReader:
|
|
12
13
|
@staticmethod
|
|
13
|
-
def
|
|
14
|
+
def read_artefact_data(artefact_name, classifier, classified_file_info = None) -> tuple[str, dict[str, str]]:
|
|
14
15
|
if not Classifier.is_valid_classifier(classifier):
|
|
15
|
-
|
|
16
|
-
return None, None
|
|
16
|
+
raise ValueError("Invalid classifier provided. Please provide a valid classifier.")
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
if not classified_file_info:
|
|
19
|
+
file_classifier = FileClassifier(os)
|
|
20
|
+
classified_file_info = file_classifier.classify_files()
|
|
20
21
|
artefact_info_of_classifier = classified_file_info.get(classifier, [])
|
|
21
22
|
|
|
22
23
|
for artefact_info in artefact_info_of_classifier:
|
|
23
24
|
file_path = artefact_info["file_path"]
|
|
24
25
|
artefact_title = artefact_info["title"]
|
|
25
26
|
if artefact_title == artefact_name:
|
|
26
|
-
with open(file_path, 'r') as file:
|
|
27
|
+
with open(file_path, 'r', encoding='utf-8') as file:
|
|
27
28
|
content = file.read()
|
|
28
29
|
return content, artefact_info
|
|
29
30
|
|
|
@@ -36,25 +37,14 @@ class ArtefactReader:
|
|
|
36
37
|
return None, None
|
|
37
38
|
|
|
38
39
|
@staticmethod
|
|
39
|
-
def
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
def read_artefact(artefact_name, classifier, classified_file_info=None) -> Artefact:
|
|
41
|
+
content, artefact_info = ArtefactReader.read_artefact_data(artefact_name, classifier, classified_file_info)
|
|
42
|
+
if not content or not artefact_info:
|
|
42
43
|
return None
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
for artefact_info in artefact_info_of_classifier:
|
|
49
|
-
file_path = artefact_info["file_path"]
|
|
50
|
-
artefact_title = artefact_info["title"]
|
|
51
|
-
if artefact_title == artefact_name:
|
|
52
|
-
with open(file_path, 'r') as file:
|
|
53
|
-
content = file.read()
|
|
54
|
-
artefact = artefact_from_content(content)
|
|
55
|
-
artefact._file_path = file_path
|
|
56
|
-
return artefact
|
|
57
|
-
return None
|
|
44
|
+
file_path = artefact_info["file_path"]
|
|
45
|
+
artefact = artefact_from_content(content)
|
|
46
|
+
artefact._file_path = file_path
|
|
47
|
+
return artefact
|
|
58
48
|
|
|
59
49
|
@staticmethod
|
|
60
50
|
def extract_parent_tree(artefact_content):
|
|
@@ -84,7 +74,6 @@ class ArtefactReader:
|
|
|
84
74
|
|
|
85
75
|
@staticmethod
|
|
86
76
|
def read_artefacts(classified_artefacts=None, file_system=os, tags=None) -> Dict[str, List[Artefact]]:
|
|
87
|
-
from ara_cli.artefact_models.artefact_load import artefact_from_content
|
|
88
77
|
|
|
89
78
|
if classified_artefacts is None:
|
|
90
79
|
file_classifier = FileClassifier(file_system)
|
|
@@ -94,29 +83,17 @@ class ArtefactReader:
|
|
|
94
83
|
for artefact_type in classified_artefacts.keys()}
|
|
95
84
|
for artefact_type, artefact_info_dicts in classified_artefacts.items():
|
|
96
85
|
for artefact_info in artefact_info_dicts:
|
|
86
|
+
title = artefact_info["title"]
|
|
97
87
|
try:
|
|
98
|
-
|
|
99
|
-
content = file.read()
|
|
100
|
-
artefact = artefact_from_content(content)
|
|
101
|
-
if not artefact:
|
|
102
|
-
continue
|
|
103
|
-
# Store the full file path in the artefact
|
|
104
|
-
artefact._file_path = artefact_info["file_path"]
|
|
88
|
+
artefact = ArtefactReader.read_artefact(title, artefact_type, classified_artefacts)
|
|
105
89
|
artefacts[artefact_type].append(artefact)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
# FIXME: LOOK INTO IT
|
|
109
|
-
# artefacts[artefact_type].append(file_path)
|
|
110
|
-
except Exception:
|
|
111
|
-
# TODO: catch only specific exceptions
|
|
112
|
-
# TODO: implament error message for deserialization or "ara scan" or "ara autofix"
|
|
113
|
-
# print(f"Warning: Could not deserialize artefact at {artefact_info}: {e}")
|
|
114
|
-
# artefacts[artefact_type].append(file_path)
|
|
90
|
+
except Exception as e:
|
|
91
|
+
error_handler.report_error(e, f"reading {artefact_type} '{title}'")
|
|
115
92
|
continue
|
|
116
93
|
return artefacts
|
|
117
94
|
|
|
118
95
|
@staticmethod
|
|
119
|
-
def find_children(artefact_name, classifier, artefacts_by_classifier=
|
|
96
|
+
def find_children(artefact_name, classifier, artefacts_by_classifier=None, classified_artefacts=None):
|
|
120
97
|
artefacts_by_classifier = artefacts_by_classifier or {}
|
|
121
98
|
filtered_artefacts = {k: [] for k in artefacts_by_classifier.keys()}
|
|
122
99
|
|
|
@@ -125,73 +102,97 @@ class ArtefactReader:
|
|
|
125
102
|
|
|
126
103
|
for artefact_classifier, artefacts in classified_artefacts.items():
|
|
127
104
|
for artefact in artefacts:
|
|
128
|
-
|
|
129
|
-
|
|
105
|
+
ArtefactReader._process_artefact(
|
|
106
|
+
artefact, artefact_name, classifier, filtered_artefacts
|
|
107
|
+
)
|
|
130
108
|
|
|
131
|
-
|
|
132
|
-
contribution = artefact.contribution
|
|
133
|
-
if (contribution and
|
|
134
|
-
contribution.artefact_name == artefact_name and
|
|
135
|
-
contribution.classifier == classifier):
|
|
136
|
-
|
|
137
|
-
file_classifier = artefact._file_path.split('.')[-1]
|
|
109
|
+
return ArtefactReader.merge_dicts(artefacts_by_classifier, filtered_artefacts)
|
|
138
110
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
111
|
+
@staticmethod
|
|
112
|
+
def _process_artefact(artefact, artefact_name, classifier, filtered_artefacts):
|
|
113
|
+
if not isinstance(artefact, Artefact):
|
|
114
|
+
return
|
|
115
|
+
contribution = getattr(artefact, 'contribution', None)
|
|
116
|
+
if not contribution:
|
|
117
|
+
return
|
|
118
|
+
if getattr(contribution, 'artefact_name', None) != artefact_name:
|
|
119
|
+
return
|
|
120
|
+
if getattr(contribution, 'classifier', None) != classifier:
|
|
121
|
+
return
|
|
142
122
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
123
|
+
file_classifier = getattr(artefact, '_file_path', '').split('.')[-1]
|
|
124
|
+
if file_classifier not in filtered_artefacts:
|
|
125
|
+
filtered_artefacts[file_classifier] = []
|
|
126
|
+
filtered_artefacts[file_classifier].append(artefact)
|
|
147
127
|
|
|
148
128
|
@staticmethod
|
|
149
129
|
def step_through_value_chain(
|
|
150
130
|
artefact_name,
|
|
151
131
|
classifier,
|
|
152
|
-
artefacts_by_classifier=
|
|
153
|
-
classified_artefacts: dict[str, list[Artefact]] | None = None
|
|
132
|
+
artefacts_by_classifier=None,
|
|
133
|
+
classified_artefacts: dict[str, list['Artefact']] | None = None
|
|
154
134
|
):
|
|
155
135
|
from ara_cli.artefact_models.artefact_load import artefact_from_content
|
|
156
136
|
|
|
137
|
+
artefacts_by_classifier = artefacts_by_classifier or {}
|
|
138
|
+
|
|
157
139
|
if classified_artefacts is None:
|
|
158
140
|
classified_artefacts = ArtefactReader.read_artefacts()
|
|
159
141
|
|
|
160
|
-
|
|
161
|
-
artefacts_by_classifier[classifier] = []
|
|
142
|
+
ArtefactReader._ensure_classifier_key(classifier, artefacts_by_classifier)
|
|
162
143
|
|
|
163
|
-
artefact =
|
|
164
|
-
|
|
165
|
-
|
|
144
|
+
artefact = ArtefactReader._find_artefact_by_name(
|
|
145
|
+
artefact_name,
|
|
146
|
+
classified_artefacts.get(classifier, [])
|
|
147
|
+
)
|
|
166
148
|
|
|
167
|
-
if not artefact:
|
|
168
|
-
return
|
|
169
|
-
if artefact in artefacts_by_classifier[classifier]:
|
|
149
|
+
if not artefact or artefact in artefacts_by_classifier[classifier]:
|
|
170
150
|
return
|
|
171
151
|
|
|
172
152
|
artefacts_by_classifier[classifier].append(artefact)
|
|
173
153
|
|
|
174
|
-
parent = artefact
|
|
175
|
-
if
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
154
|
+
parent = getattr(artefact, 'contribution', None)
|
|
155
|
+
if not ArtefactReader._has_valid_parent(parent):
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
parent_name = parent.artefact_name
|
|
159
|
+
parent_classifier = parent.classifier
|
|
160
|
+
|
|
161
|
+
parent_classifier_artefacts = classified_artefacts.get(parent_classifier, [])
|
|
162
|
+
all_artefact_names = [x.title for x in parent_classifier_artefacts]
|
|
163
|
+
|
|
164
|
+
if parent_name not in all_artefact_names:
|
|
165
|
+
ArtefactReader._suggest_parent_name_match(
|
|
166
|
+
artefact_name, all_artefact_names, parent_name
|
|
167
|
+
)
|
|
168
|
+
print()
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
ArtefactReader.step_through_value_chain(
|
|
172
|
+
artefact_name=parent_name,
|
|
173
|
+
classifier=parent_classifier,
|
|
174
|
+
artefacts_by_classifier=artefacts_by_classifier,
|
|
175
|
+
classified_artefacts=classified_artefacts
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def _ensure_classifier_key(classifier, artefacts_by_classifier):
|
|
180
|
+
if classifier not in artefacts_by_classifier:
|
|
181
|
+
artefacts_by_classifier[classifier] = []
|
|
182
|
+
|
|
183
|
+
@staticmethod
|
|
184
|
+
def _find_artefact_by_name(artefact_name, artefacts):
|
|
185
|
+
return next((x for x in artefacts if x.title == artefact_name), None)
|
|
186
|
+
|
|
187
|
+
@staticmethod
|
|
188
|
+
def _has_valid_parent(parent):
|
|
189
|
+
return parent and getattr(parent, 'artefact_name', None) and getattr(parent, 'classifier', None)
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
def _suggest_parent_name_match(artefact_name, all_artefact_names, parent_name):
|
|
193
|
+
if parent_name is not None:
|
|
194
|
+
suggest_close_name_matches_for_parent(
|
|
195
|
+
artefact_name,
|
|
196
|
+
all_artefact_names,
|
|
197
|
+
parent_name
|
|
197
198
|
)
|
ara_cli/artefact_renamer.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import os
|
|
2
1
|
from functools import lru_cache
|
|
3
2
|
from ara_cli.classifier import Classifier
|
|
4
3
|
from ara_cli.artefact_link_updater import ArtefactLinkUpdater
|
|
5
4
|
from ara_cli.template_manager import DirectoryNavigator
|
|
5
|
+
import os
|
|
6
6
|
import re
|
|
7
|
+
import warnings
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class ArtefactRenamer:
|
|
@@ -22,6 +23,8 @@ class ArtefactRenamer:
|
|
|
22
23
|
return re.compile(pattern)
|
|
23
24
|
|
|
24
25
|
def rename(self, old_name, new_name, classifier):
|
|
26
|
+
import shutil
|
|
27
|
+
|
|
25
28
|
original_directory = self.navigate_to_target()
|
|
26
29
|
|
|
27
30
|
if not new_name:
|
|
@@ -47,7 +50,8 @@ class ArtefactRenamer:
|
|
|
47
50
|
if self.file_system.path.exists(new_file_path):
|
|
48
51
|
raise FileExistsError(f"The new file name {new_file_path} already exists.")
|
|
49
52
|
if self.file_system.path.exists(new_dir_path):
|
|
50
|
-
|
|
53
|
+
warnings.warn(f"The new directory name {new_dir_path} already exists. It will be replaced by the artefact's data directory or removed entirely.", UserWarning)
|
|
54
|
+
shutil.rmtree(new_dir_path)
|
|
51
55
|
|
|
52
56
|
# Perform the renaming of the file and directory
|
|
53
57
|
self.file_system.rename(old_file_path, new_file_path)
|
|
@@ -75,7 +79,7 @@ class ArtefactRenamer:
|
|
|
75
79
|
raise ValueError(f"Invalid classifier: {classifier}")
|
|
76
80
|
|
|
77
81
|
# Read the file content
|
|
78
|
-
with open(artefact_path, 'r') as file:
|
|
82
|
+
with open(artefact_path, 'r', encoding='utf-8') as file:
|
|
79
83
|
content = file.read()
|
|
80
84
|
|
|
81
85
|
# Find the old title line
|
|
@@ -89,5 +93,5 @@ class ArtefactRenamer:
|
|
|
89
93
|
new_content = content.replace(old_title_line, new_title_line, 1)
|
|
90
94
|
|
|
91
95
|
# Write the updated content back to the file
|
|
92
|
-
with open(artefact_path, 'w') as file:
|
|
96
|
+
with open(artefact_path, 'w', encoding='utf-8') as file:
|
|
93
97
|
file.write(new_content)
|
ara_cli/artefact_scan.py
CHANGED
|
@@ -2,24 +2,87 @@ from textwrap import indent
|
|
|
2
2
|
import os
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
def
|
|
5
|
+
def is_contribution_valid(contribution, classified_artefact_info) -> bool:
|
|
6
|
+
from ara_cli.artefact_fuzzy_search import extract_artefact_names_of_classifier
|
|
7
|
+
if not contribution or not contribution.artefact_name or not contribution.classifier:
|
|
8
|
+
return True
|
|
9
|
+
|
|
10
|
+
all_artefact_names = extract_artefact_names_of_classifier(
|
|
11
|
+
classified_files=classified_artefact_info,
|
|
12
|
+
classifier=contribution.classifier
|
|
13
|
+
)
|
|
14
|
+
if contribution.artefact_name not in all_artefact_names:
|
|
15
|
+
return False
|
|
16
|
+
return True
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_rule_valid(contribution, classified_artefact_info) -> bool:
|
|
20
|
+
from ara_cli.artefact_reader import ArtefactReader
|
|
21
|
+
|
|
22
|
+
if not contribution or not contribution.artefact_name or not contribution.classifier:
|
|
23
|
+
return True
|
|
24
|
+
rule = contribution.rule
|
|
25
|
+
if not rule:
|
|
26
|
+
return True
|
|
27
|
+
parent = ArtefactReader.read_artefact(contribution.artefact_name, contribution.classifier)
|
|
28
|
+
if not parent:
|
|
29
|
+
return True
|
|
30
|
+
rules = parent.rules
|
|
31
|
+
if not rules or rule not in rules:
|
|
32
|
+
return False
|
|
33
|
+
return True
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def check_contribution(contribution, classified_artefact_info, file_path) -> tuple[bool, str]:
|
|
37
|
+
if not contribution:
|
|
38
|
+
return True, None
|
|
39
|
+
|
|
40
|
+
if not is_contribution_valid(contribution, classified_artefact_info):
|
|
41
|
+
reason = (f"Invalid Contribution Reference: The contribution references "
|
|
42
|
+
f"'{contribution.classifier}' artefact '{contribution.artefact_name}' "
|
|
43
|
+
f"which does not exist.")
|
|
44
|
+
return False, reason
|
|
45
|
+
|
|
46
|
+
if not is_rule_valid(contribution, classified_artefact_info):
|
|
47
|
+
reason = (f"Rule Mismatch: The contribution references "
|
|
48
|
+
f"rule '{contribution.rule}' which the parent "
|
|
49
|
+
f"{contribution.classifier} '{contribution.artefact_name}' does not have.")
|
|
50
|
+
return False, reason
|
|
51
|
+
return True, None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def check_file(file_path, artefact_class, classified_artefact_info=None):
|
|
6
55
|
from pydantic import ValidationError
|
|
56
|
+
from ara_cli.file_classifier import FileClassifier
|
|
57
|
+
|
|
7
58
|
try:
|
|
8
59
|
with open(file_path, "r", encoding="utf-8") as f:
|
|
9
60
|
content = f.read()
|
|
10
61
|
except OSError as e:
|
|
11
62
|
return False, f"File error: {e}"
|
|
63
|
+
|
|
64
|
+
if not classified_artefact_info:
|
|
65
|
+
file_classifier = FileClassifier(os)
|
|
66
|
+
classified_artefact_info = file_classifier.classify_files()
|
|
67
|
+
|
|
12
68
|
try:
|
|
13
69
|
artefact_instance = artefact_class.deserialize(content)
|
|
14
70
|
|
|
15
71
|
base_name = os.path.basename(file_path)
|
|
16
72
|
file_name_without_ext, _ = os.path.splitext(base_name)
|
|
17
73
|
|
|
74
|
+
# Check title and file name matching
|
|
18
75
|
if artefact_instance.title != file_name_without_ext:
|
|
19
76
|
reason = (f"Filename-Title Mismatch: The file name '{file_name_without_ext}' "
|
|
20
77
|
f"does not match the artefact title '{artefact_instance.title}'.")
|
|
21
78
|
return False, reason
|
|
22
79
|
|
|
80
|
+
contribution = artefact_instance.contribution
|
|
81
|
+
|
|
82
|
+
contribution_valid, reason = check_contribution(contribution, classified_artefact_info, file_path)
|
|
83
|
+
if not contribution_valid:
|
|
84
|
+
return False, reason
|
|
85
|
+
|
|
23
86
|
return True, None
|
|
24
87
|
except (ValidationError, ValueError, AssertionError) as e:
|
|
25
88
|
return False, str(e)
|
|
@@ -37,7 +100,7 @@ def find_invalid_files(classified_artefact_info, classifier):
|
|
|
37
100
|
continue
|
|
38
101
|
if ".data" in artefact_info["file_path"]:
|
|
39
102
|
continue
|
|
40
|
-
is_valid, reason = check_file(artefact_info["file_path"], artefact_class)
|
|
103
|
+
is_valid, reason = check_file(artefact_info["file_path"], artefact_class, classified_artefact_info)
|
|
41
104
|
if not is_valid:
|
|
42
105
|
invalid_files.append((artefact_info["file_path"], reason))
|
|
43
106
|
return invalid_files
|
|
@@ -45,7 +108,7 @@ def find_invalid_files(classified_artefact_info, classifier):
|
|
|
45
108
|
|
|
46
109
|
def show_results(invalid_artefacts):
|
|
47
110
|
has_issues = False
|
|
48
|
-
with open("incompatible_artefacts_report.md", "w") as report:
|
|
111
|
+
with open("incompatible_artefacts_report.md", "w", encoding="utf-8") as report:
|
|
49
112
|
report.write("# Artefact Check Report\n\n")
|
|
50
113
|
for classifier, files in invalid_artefacts.items():
|
|
51
114
|
if files:
|