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.

Files changed (150) hide show
  1. ara_cli/__init__.py +18 -2
  2. ara_cli/__main__.py +248 -62
  3. ara_cli/ara_command_action.py +155 -86
  4. ara_cli/ara_config.py +226 -80
  5. ara_cli/ara_subcommands/__init__.py +0 -0
  6. ara_cli/ara_subcommands/autofix.py +26 -0
  7. ara_cli/ara_subcommands/chat.py +27 -0
  8. ara_cli/ara_subcommands/classifier_directory.py +16 -0
  9. ara_cli/ara_subcommands/common.py +100 -0
  10. ara_cli/ara_subcommands/create.py +75 -0
  11. ara_cli/ara_subcommands/delete.py +22 -0
  12. ara_cli/ara_subcommands/extract.py +22 -0
  13. ara_cli/ara_subcommands/fetch_templates.py +14 -0
  14. ara_cli/ara_subcommands/list.py +65 -0
  15. ara_cli/ara_subcommands/list_tags.py +25 -0
  16. ara_cli/ara_subcommands/load.py +48 -0
  17. ara_cli/ara_subcommands/prompt.py +136 -0
  18. ara_cli/ara_subcommands/read.py +47 -0
  19. ara_cli/ara_subcommands/read_status.py +20 -0
  20. ara_cli/ara_subcommands/read_user.py +20 -0
  21. ara_cli/ara_subcommands/reconnect.py +27 -0
  22. ara_cli/ara_subcommands/rename.py +22 -0
  23. ara_cli/ara_subcommands/scan.py +14 -0
  24. ara_cli/ara_subcommands/set_status.py +22 -0
  25. ara_cli/ara_subcommands/set_user.py +22 -0
  26. ara_cli/ara_subcommands/template.py +16 -0
  27. ara_cli/artefact_autofix.py +649 -68
  28. ara_cli/artefact_creator.py +8 -11
  29. ara_cli/artefact_deleter.py +2 -4
  30. ara_cli/artefact_fuzzy_search.py +22 -10
  31. ara_cli/artefact_link_updater.py +4 -4
  32. ara_cli/artefact_lister.py +29 -55
  33. ara_cli/artefact_models/artefact_data_retrieval.py +23 -0
  34. ara_cli/artefact_models/artefact_load.py +11 -3
  35. ara_cli/artefact_models/artefact_model.py +146 -39
  36. ara_cli/artefact_models/artefact_templates.py +70 -44
  37. ara_cli/artefact_models/businessgoal_artefact_model.py +23 -25
  38. ara_cli/artefact_models/epic_artefact_model.py +34 -26
  39. ara_cli/artefact_models/feature_artefact_model.py +203 -64
  40. ara_cli/artefact_models/keyfeature_artefact_model.py +21 -24
  41. ara_cli/artefact_models/serialize_helper.py +1 -1
  42. ara_cli/artefact_models/task_artefact_model.py +83 -15
  43. ara_cli/artefact_models/userstory_artefact_model.py +37 -27
  44. ara_cli/artefact_models/vision_artefact_model.py +23 -42
  45. ara_cli/artefact_reader.py +92 -91
  46. ara_cli/artefact_renamer.py +8 -4
  47. ara_cli/artefact_scan.py +66 -3
  48. ara_cli/chat.py +622 -162
  49. ara_cli/chat_agent/__init__.py +0 -0
  50. ara_cli/chat_agent/agent_communicator.py +62 -0
  51. ara_cli/chat_agent/agent_process_manager.py +211 -0
  52. ara_cli/chat_agent/agent_status_manager.py +73 -0
  53. ara_cli/chat_agent/agent_workspace_manager.py +76 -0
  54. ara_cli/commands/__init__.py +0 -0
  55. ara_cli/commands/command.py +7 -0
  56. ara_cli/commands/extract_command.py +15 -0
  57. ara_cli/commands/load_command.py +65 -0
  58. ara_cli/commands/load_image_command.py +34 -0
  59. ara_cli/commands/read_command.py +117 -0
  60. ara_cli/completers.py +144 -0
  61. ara_cli/directory_navigator.py +37 -4
  62. ara_cli/error_handler.py +134 -0
  63. ara_cli/file_classifier.py +6 -5
  64. ara_cli/file_lister.py +1 -1
  65. ara_cli/file_loaders/__init__.py +0 -0
  66. ara_cli/file_loaders/binary_file_loader.py +33 -0
  67. ara_cli/file_loaders/document_file_loader.py +34 -0
  68. ara_cli/file_loaders/document_reader.py +245 -0
  69. ara_cli/file_loaders/document_readers.py +233 -0
  70. ara_cli/file_loaders/file_loader.py +50 -0
  71. ara_cli/file_loaders/file_loaders.py +123 -0
  72. ara_cli/file_loaders/image_processor.py +89 -0
  73. ara_cli/file_loaders/markdown_reader.py +75 -0
  74. ara_cli/file_loaders/text_file_loader.py +187 -0
  75. ara_cli/global_file_lister.py +51 -0
  76. ara_cli/list_filter.py +1 -1
  77. ara_cli/output_suppressor.py +1 -1
  78. ara_cli/prompt_extractor.py +215 -88
  79. ara_cli/prompt_handler.py +521 -134
  80. ara_cli/prompt_rag.py +2 -2
  81. ara_cli/tag_extractor.py +83 -38
  82. ara_cli/template_loader.py +245 -0
  83. ara_cli/template_manager.py +18 -13
  84. ara_cli/templates/prompt-modules/commands/empty.commands.md +2 -12
  85. ara_cli/templates/prompt-modules/commands/extract_general.commands.md +12 -0
  86. ara_cli/templates/prompt-modules/commands/extract_markdown.commands.md +11 -0
  87. ara_cli/templates/prompt-modules/commands/extract_python.commands.md +13 -0
  88. ara_cli/templates/prompt-modules/commands/feature_add_or_modifiy_specified_behavior.commands.md +36 -0
  89. ara_cli/templates/prompt-modules/commands/feature_generate_initial_specified_bevahior.commands.md +53 -0
  90. ara_cli/templates/prompt-modules/commands/prompt_template_tech_stack_transformer.commands.md +95 -0
  91. ara_cli/templates/prompt-modules/commands/python_bug_fixing_code.commands.md +34 -0
  92. ara_cli/templates/prompt-modules/commands/python_generate_code.commands.md +27 -0
  93. ara_cli/templates/prompt-modules/commands/python_refactoring_code.commands.md +39 -0
  94. ara_cli/templates/prompt-modules/commands/python_step_definitions_generation_and_fixing.commands.md +40 -0
  95. ara_cli/templates/prompt-modules/commands/python_unittest_generation_and_fixing.commands.md +48 -0
  96. ara_cli/update_config_prompt.py +9 -3
  97. ara_cli/version.py +1 -1
  98. ara_cli-0.1.10.8.dist-info/METADATA +241 -0
  99. ara_cli-0.1.10.8.dist-info/RECORD +193 -0
  100. tests/test_ara_command_action.py +73 -59
  101. tests/test_ara_config.py +341 -36
  102. tests/test_artefact_autofix.py +1060 -0
  103. tests/test_artefact_link_updater.py +3 -3
  104. tests/test_artefact_lister.py +52 -132
  105. tests/test_artefact_renamer.py +2 -2
  106. tests/test_artefact_scan.py +327 -33
  107. tests/test_chat.py +2063 -498
  108. tests/test_file_classifier.py +24 -1
  109. tests/test_file_creator.py +3 -5
  110. tests/test_file_lister.py +1 -1
  111. tests/test_global_file_lister.py +131 -0
  112. tests/test_list_filter.py +2 -2
  113. tests/test_prompt_handler.py +746 -0
  114. tests/test_tag_extractor.py +19 -13
  115. tests/test_template_loader.py +192 -0
  116. tests/test_template_manager.py +5 -4
  117. tests/test_update_config_prompt.py +2 -2
  118. ara_cli/ara_command_parser.py +0 -327
  119. ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +0 -27
  120. ara_cli/templates/prompt-modules/blueprints/task_todo_list_implement_feature_BDD_way.blueprint.md +0 -30
  121. ara_cli/templates/prompt-modules/commands/artefact_classification.commands.md +0 -9
  122. ara_cli/templates/prompt-modules/commands/artefact_extension.commands.md +0 -17
  123. ara_cli/templates/prompt-modules/commands/artefact_formulation.commands.md +0 -14
  124. ara_cli/templates/prompt-modules/commands/behave_step_generation.commands.md +0 -102
  125. ara_cli/templates/prompt-modules/commands/code_generation_complex.commands.md +0 -20
  126. ara_cli/templates/prompt-modules/commands/code_generation_simple.commands.md +0 -13
  127. ara_cli/templates/prompt-modules/commands/error_fixing.commands.md +0 -20
  128. ara_cli/templates/prompt-modules/commands/feature_file_update.commands.md +0 -18
  129. ara_cli/templates/prompt-modules/commands/feature_formulation.commands.md +0 -43
  130. ara_cli/templates/prompt-modules/commands/js_code_generation_simple.commands.md +0 -13
  131. ara_cli/templates/prompt-modules/commands/refactoring.commands.md +0 -15
  132. ara_cli/templates/prompt-modules/commands/refactoring_analysis.commands.md +0 -9
  133. ara_cli/templates/prompt-modules/commands/reverse_engineer_feature_file.commands.md +0 -15
  134. ara_cli/templates/prompt-modules/commands/reverse_engineer_program_flow.commands.md +0 -19
  135. ara_cli/templates/template.businessgoal +0 -10
  136. ara_cli/templates/template.capability +0 -10
  137. ara_cli/templates/template.epic +0 -15
  138. ara_cli/templates/template.example +0 -6
  139. ara_cli/templates/template.feature +0 -26
  140. ara_cli/templates/template.issue +0 -14
  141. ara_cli/templates/template.keyfeature +0 -15
  142. ara_cli/templates/template.task +0 -6
  143. ara_cli/templates/template.userstory +0 -17
  144. ara_cli/templates/template.vision +0 -14
  145. ara_cli-0.1.9.69.dist-info/METADATA +0 -16
  146. ara_cli-0.1.9.69.dist-info/RECORD +0 -158
  147. tests/test_ara_autofix.py +0 -219
  148. {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/WHEEL +0 -0
  149. {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/entry_points.txt +0 -0
  150. {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
- in_order_to = None
51
- as_a = None
52
- i_want = None
53
-
54
- in_order_to_prefix = "In order to "
55
- as_a_prefix = "As a "
56
- as_a_prefix_alt = "As an "
57
- i_want_prefix = "I want "
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 (not in_order_to or not as_a or not i_want):
61
- line = lines[index].strip()
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(for_prefix):
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
- if index < len(lines) and lines[index].startswith(for_prefix):
102
- for_ = lines[index][len(for_prefix):]
103
- index = index + 1
104
- if index < len(lines) and lines[index].startswith(who_prefix):
105
- who = lines[index][len(who_prefix):]
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):
@@ -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 read_artefact(artefact_name, classifier) -> tuple[str, dict[str, str]]:
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
- print("Invalid classifier provided. Please provide a valid classifier.")
16
- return None, None
16
+ raise ValueError("Invalid classifier provided. Please provide a valid classifier.")
17
17
 
18
- file_classifier = FileClassifier(os)
19
- classified_file_info = file_classifier.classify_files()
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 read_single_artefact(artefact_name, classifier, classified_file_info=None) -> Artefact:
40
- if not Classifier.is_valid_classifier(classifier):
41
- print("Invalid classifier provided. Please provide a valid classifier.")
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
- if not classified_file_info:
44
- file_classifier = FileClassifier(os)
45
- classified_file_info = file_classifier.classify_files()
46
- artefact_info_of_classifier = classified_file_info.get(classifier, [])
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
- with open(artefact_info["file_path"], 'r') as file:
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
- # else:
107
- # Include file path if deserialization fails
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={}, classified_artefacts=None):
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
- if not isinstance(artefact, Artefact):
129
- continue
105
+ ArtefactReader._process_artefact(
106
+ artefact, artefact_name, classifier, filtered_artefacts
107
+ )
130
108
 
131
- try:
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
- if file_classifier not in filtered_artefacts:
140
- filtered_artefacts[file_classifier] = []
141
- filtered_artefacts[file_classifier].append(artefact)
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
- except AttributeError as e:
144
- continue
145
-
146
- return ArtefactReader.merge_dicts(artefacts_by_classifier, filtered_artefacts)
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
- if classifier not in artefacts_by_classifier:
161
- artefacts_by_classifier[classifier] = []
142
+ ArtefactReader._ensure_classifier_key(classifier, artefacts_by_classifier)
162
143
 
163
- artefact = next(filter(
164
- lambda x: x.title == artefact_name, classified_artefacts[classifier]
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.contribution
175
- if parent and parent.artefact_name and parent.classifier:
176
- parent_name = parent.artefact_name
177
- parent_classifier = parent.classifier
178
-
179
- parent_classifier_artefacts = classified_artefacts[parent_classifier]
180
- all_artefact_names = [x.title for x in parent_classifier_artefacts]
181
-
182
- if parent_name not in all_artefact_names:
183
- if parent_name is not None:
184
- suggest_close_name_matches_for_parent(
185
- artefact_name,
186
- all_artefact_names,
187
- parent_name
188
- )
189
- print()
190
- return
191
-
192
- ArtefactReader.step_through_value_chain(
193
- artefact_name=parent_name,
194
- classifier=parent_classifier,
195
- artefacts_by_classifier=artefacts_by_classifier,
196
- classified_artefacts=classified_artefacts
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
  )
@@ -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
- raise FileExistsError(f"The new directory name {new_dir_path} already exists.")
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 check_file(file_path, artefact_class):
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: