ara-cli 0.1.9.77__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 (122) hide show
  1. ara_cli/__init__.py +18 -2
  2. ara_cli/__main__.py +245 -66
  3. ara_cli/ara_command_action.py +128 -63
  4. ara_cli/ara_config.py +201 -177
  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 +214 -28
  28. ara_cli/artefact_creator.py +5 -8
  29. ara_cli/artefact_deleter.py +2 -4
  30. ara_cli/artefact_fuzzy_search.py +13 -6
  31. ara_cli/artefact_lister.py +29 -55
  32. ara_cli/artefact_models/artefact_data_retrieval.py +23 -0
  33. ara_cli/artefact_models/artefact_model.py +106 -25
  34. ara_cli/artefact_models/artefact_templates.py +23 -13
  35. ara_cli/artefact_models/epic_artefact_model.py +11 -2
  36. ara_cli/artefact_models/feature_artefact_model.py +56 -1
  37. ara_cli/artefact_models/userstory_artefact_model.py +15 -3
  38. ara_cli/artefact_reader.py +4 -5
  39. ara_cli/artefact_renamer.py +6 -2
  40. ara_cli/artefact_scan.py +2 -2
  41. ara_cli/chat.py +594 -219
  42. ara_cli/chat_agent/__init__.py +0 -0
  43. ara_cli/chat_agent/agent_communicator.py +62 -0
  44. ara_cli/chat_agent/agent_process_manager.py +211 -0
  45. ara_cli/chat_agent/agent_status_manager.py +73 -0
  46. ara_cli/chat_agent/agent_workspace_manager.py +76 -0
  47. ara_cli/commands/__init__.py +0 -0
  48. ara_cli/commands/command.py +7 -0
  49. ara_cli/commands/extract_command.py +15 -0
  50. ara_cli/commands/load_command.py +65 -0
  51. ara_cli/commands/load_image_command.py +34 -0
  52. ara_cli/commands/read_command.py +117 -0
  53. ara_cli/completers.py +144 -0
  54. ara_cli/directory_navigator.py +37 -4
  55. ara_cli/error_handler.py +134 -0
  56. ara_cli/file_classifier.py +3 -2
  57. ara_cli/file_loaders/__init__.py +0 -0
  58. ara_cli/file_loaders/binary_file_loader.py +33 -0
  59. ara_cli/file_loaders/document_file_loader.py +34 -0
  60. ara_cli/file_loaders/document_reader.py +245 -0
  61. ara_cli/file_loaders/document_readers.py +233 -0
  62. ara_cli/file_loaders/file_loader.py +50 -0
  63. ara_cli/file_loaders/file_loaders.py +123 -0
  64. ara_cli/file_loaders/image_processor.py +89 -0
  65. ara_cli/file_loaders/markdown_reader.py +75 -0
  66. ara_cli/file_loaders/text_file_loader.py +187 -0
  67. ara_cli/global_file_lister.py +51 -0
  68. ara_cli/prompt_extractor.py +214 -87
  69. ara_cli/prompt_handler.py +508 -146
  70. ara_cli/tag_extractor.py +54 -24
  71. ara_cli/template_loader.py +245 -0
  72. ara_cli/template_manager.py +14 -4
  73. ara_cli/templates/prompt-modules/commands/empty.commands.md +2 -12
  74. ara_cli/templates/prompt-modules/commands/extract_general.commands.md +12 -0
  75. ara_cli/templates/prompt-modules/commands/extract_markdown.commands.md +11 -0
  76. ara_cli/templates/prompt-modules/commands/extract_python.commands.md +13 -0
  77. ara_cli/templates/prompt-modules/commands/feature_add_or_modifiy_specified_behavior.commands.md +36 -0
  78. ara_cli/templates/prompt-modules/commands/feature_generate_initial_specified_bevahior.commands.md +53 -0
  79. ara_cli/templates/prompt-modules/commands/prompt_template_tech_stack_transformer.commands.md +95 -0
  80. ara_cli/templates/prompt-modules/commands/python_bug_fixing_code.commands.md +34 -0
  81. ara_cli/templates/prompt-modules/commands/python_generate_code.commands.md +27 -0
  82. ara_cli/templates/prompt-modules/commands/python_refactoring_code.commands.md +39 -0
  83. ara_cli/templates/prompt-modules/commands/python_step_definitions_generation_and_fixing.commands.md +40 -0
  84. ara_cli/templates/prompt-modules/commands/python_unittest_generation_and_fixing.commands.md +48 -0
  85. ara_cli/update_config_prompt.py +7 -1
  86. ara_cli/version.py +1 -1
  87. ara_cli-0.1.10.8.dist-info/METADATA +241 -0
  88. {ara_cli-0.1.9.77.dist-info → ara_cli-0.1.10.8.dist-info}/RECORD +104 -59
  89. tests/test_ara_command_action.py +66 -52
  90. tests/test_ara_config.py +200 -279
  91. tests/test_artefact_autofix.py +361 -5
  92. tests/test_artefact_lister.py +52 -132
  93. tests/test_artefact_scan.py +1 -1
  94. tests/test_chat.py +2009 -603
  95. tests/test_file_classifier.py +23 -0
  96. tests/test_file_creator.py +3 -5
  97. tests/test_global_file_lister.py +131 -0
  98. tests/test_prompt_handler.py +746 -0
  99. tests/test_tag_extractor.py +19 -13
  100. tests/test_template_loader.py +192 -0
  101. tests/test_template_manager.py +5 -4
  102. ara_cli/ara_command_parser.py +0 -536
  103. ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +0 -27
  104. ara_cli/templates/prompt-modules/blueprints/task_todo_list_implement_feature_BDD_way.blueprint.md +0 -30
  105. ara_cli/templates/prompt-modules/commands/artefact_classification.commands.md +0 -9
  106. ara_cli/templates/prompt-modules/commands/artefact_extension.commands.md +0 -17
  107. ara_cli/templates/prompt-modules/commands/artefact_formulation.commands.md +0 -14
  108. ara_cli/templates/prompt-modules/commands/behave_step_generation.commands.md +0 -102
  109. ara_cli/templates/prompt-modules/commands/code_generation_complex.commands.md +0 -20
  110. ara_cli/templates/prompt-modules/commands/code_generation_simple.commands.md +0 -13
  111. ara_cli/templates/prompt-modules/commands/error_fixing.commands.md +0 -20
  112. ara_cli/templates/prompt-modules/commands/feature_file_update.commands.md +0 -18
  113. ara_cli/templates/prompt-modules/commands/feature_formulation.commands.md +0 -43
  114. ara_cli/templates/prompt-modules/commands/js_code_generation_simple.commands.md +0 -13
  115. ara_cli/templates/prompt-modules/commands/refactoring.commands.md +0 -15
  116. ara_cli/templates/prompt-modules/commands/refactoring_analysis.commands.md +0 -9
  117. ara_cli/templates/prompt-modules/commands/reverse_engineer_feature_file.commands.md +0 -15
  118. ara_cli/templates/prompt-modules/commands/reverse_engineer_program_flow.commands.md +0 -19
  119. ara_cli-0.1.9.77.dist-info/METADATA +0 -18
  120. {ara_cli-0.1.9.77.dist-info → ara_cli-0.1.10.8.dist-info}/WHEEL +0 -0
  121. {ara_cli-0.1.9.77.dist-info → ara_cli-0.1.10.8.dist-info}/entry_points.txt +0 -0
  122. {ara_cli-0.1.9.77.dist-info → ara_cli-0.1.10.8.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,4 @@
1
+ from ara_cli.error_handler import AraError
1
2
  from ara_cli.artefact_scan import check_file
2
3
  from ara_cli.artefact_fuzzy_search import (
3
4
  find_closest_name_matches,
@@ -10,6 +11,7 @@ from ara_cli.artefact_models.artefact_model import Artefact
10
11
  from typing import Optional, Dict, List, Tuple
11
12
  import difflib
12
13
  import os
14
+ import re
13
15
 
14
16
 
15
17
  def populate_classified_artefact_info(
@@ -104,8 +106,9 @@ def determine_artefact_type_and_class(classifier):
104
106
 
105
107
  artefact_class = artefact_type_mapping.get(artefact_type)
106
108
  if not artefact_class:
107
- print(f"No artefact class found for {artefact_type}")
108
- return None, None
109
+ raise AraError(f"No artefact class found for {artefact_type}")
110
+ # print(f"No artefact class found for {artefact_type}")
111
+ # return None, None
109
112
 
110
113
  return artefact_type, artefact_class
111
114
 
@@ -144,11 +147,11 @@ def run_agent(prompt, artefact_class):
144
147
  # anthropic:claude-4-sonnet-20250514
145
148
  agent = Agent(
146
149
  model="anthropic:claude-4-sonnet-20250514",
147
- result_type=artefact_class,
150
+ output_type=artefact_class,
148
151
  instrument=True,
149
152
  )
150
153
  result = agent.run_sync(prompt)
151
- return result.data
154
+ return result.output
152
155
 
153
156
 
154
157
  def write_corrected_artefact(file_path, corrected_text):
@@ -196,36 +199,52 @@ def ask_for_correct_contribution(
196
199
  return name, classifier
197
200
 
198
201
 
199
- def ask_for_contribution_choice(
200
- choices, artefact_info: Optional[tuple[str, str]] = None
201
- ) -> Optional[str]:
202
- artefact_name, artefact_classifier = (
203
- artefact_info if artefact_info else (None, None)
204
- )
202
+ def ask_for_contribution_choice(choices: List[str], artefact_info: Optional[tuple[str, str]] = None) -> Optional[str]:
203
+ artefact_name, artefact_classifier = artefact_info if artefact_info else (None, None)
205
204
  message = "Found multiple close matches for the contribution"
206
205
  if artefact_name and artefact_classifier:
207
206
  message += f" of the {artefact_classifier} '{artefact_name}'"
208
- print(f"{message}.")
209
- for i, contribution in enumerate(choices):
210
- print(f"{i + 1}: {contribution}")
211
- choice_number = input(
212
- "Please choose the artefact to use for contribution (enter number): "
213
- )
207
+ message += "."
208
+ return get_user_choice(choices, message)
209
+
210
+
211
+ def _has_valid_contribution(artefact: Artefact) -> bool:
212
+ contribution = artefact.contribution
213
+ return contribution and contribution.artefact_name and contribution.classifier
214
+
215
+
216
+ def get_user_choice(choices: List[str], message: str) -> Optional[str]:
217
+ """
218
+ Generic function to present user with a list of choices and return their selection.
219
+
220
+ Args:
221
+ choices: A list of strings representing the choices to display.
222
+ message: A message to display before listing the choices.
223
+
224
+ Returns:
225
+ The chosen item from the list or None if the input was invalid.
226
+ """
227
+ print(message)
228
+ for i, choice in enumerate(choices):
229
+ print(f"{i + 1}: {choice}")
230
+
231
+ choice_number = input("Please enter your choice (number): ")
232
+
214
233
  try:
215
234
  choice_index = int(choice_number) - 1
216
235
  if choice_index < 0 or choice_index >= len(choices):
217
- print("Invalid choice. Aborting contribution choice.")
236
+ print("Invalid choice. Aborting operation.")
218
237
  return None
219
- choice = choices[choice_index]
238
+ return choices[choice_index]
220
239
  except ValueError:
221
- print("Invalid input. Aborting contribution choice.")
240
+ print("Invalid input. Aborting operation.")
222
241
  return None
223
- return choice
224
242
 
225
243
 
226
- def _has_valid_contribution(artefact: Artefact) -> bool:
227
- contribution = artefact.contribution
228
- return contribution and contribution.artefact_name and contribution.classifier
244
+ def ask_for_rule_choice(matches: List[str]) -> Optional[str]:
245
+ """Asks the user for a choice between multiple rule matches"""
246
+ message = "Multiple rule matches found:"
247
+ return get_user_choice(matches, message)
229
248
 
230
249
 
231
250
  def _update_rule(
@@ -249,6 +268,9 @@ def _update_rule(
249
268
  return
250
269
  if not closest_rule_match:
251
270
  return
271
+ if len(closest_rule_match) > 1:
272
+ artefact.contribution.rule = ask_for_rule_choice(closest_rule_match)
273
+ return
252
274
  artefact.contribution.rule = closest_rule_match[0]
253
275
 
254
276
 
@@ -353,6 +375,131 @@ def set_closest_contribution(
353
375
  return artefact, True
354
376
 
355
377
 
378
+ def fix_scenario_placeholder_mismatch(
379
+ file_path: str, artefact_text: str, artefact_class, **kwargs
380
+ ) -> str:
381
+ """
382
+ Converts a regular Scenario with placeholders to a Scenario Outline.
383
+ This is a deterministic fix that detects placeholders and converts the format.
384
+ """
385
+ lines = artefact_text.splitlines()
386
+ new_lines = []
387
+ i = 0
388
+
389
+ while i < len(lines):
390
+ line = lines[i]
391
+ stripped_line = line.strip()
392
+
393
+ if stripped_line.startswith('Scenario:'):
394
+ scenario_lines, next_index = _extract_scenario_block(lines, i)
395
+ processed_lines = _process_scenario_block(scenario_lines)
396
+ new_lines.extend(processed_lines)
397
+ i = next_index
398
+ else:
399
+ new_lines.append(line)
400
+ i += 1
401
+
402
+ return "\n".join(new_lines)
403
+
404
+
405
+ def _extract_scenario_block(lines: list, start_index: int) -> tuple[list, int]:
406
+ """Extract all lines belonging to a scenario block."""
407
+ scenario_lines = [lines[start_index]]
408
+ j = start_index + 1
409
+
410
+ while j < len(lines):
411
+ next_line = lines[j].strip()
412
+ if _is_scenario_boundary(next_line):
413
+ break
414
+ scenario_lines.append(lines[j])
415
+ j += 1
416
+
417
+ return scenario_lines, j
418
+
419
+
420
+ def _is_scenario_boundary(line: str) -> bool:
421
+ """Check if a line marks the boundary of a scenario block."""
422
+ boundaries = ['Scenario:', 'Scenario Outline:', 'Background:', 'Feature:']
423
+ return any(line.startswith(boundary) for boundary in boundaries)
424
+
425
+
426
+ def _process_scenario_block(scenario_lines: list) -> list:
427
+ """Process a scenario block and convert to outline if placeholders are found."""
428
+ if not scenario_lines:
429
+ return scenario_lines
430
+
431
+ first_line = scenario_lines[0]
432
+ indentation = _get_line_indentation(first_line)
433
+ placeholders = _extract_placeholders_from_scenario(scenario_lines[1:])
434
+
435
+ if not placeholders:
436
+ return scenario_lines
437
+
438
+ return _convert_to_scenario_outline(scenario_lines, placeholders, indentation)
439
+
440
+
441
+ def _get_line_indentation(line: str) -> str:
442
+ """Get the indentation of a line."""
443
+ return line[:len(line) - len(line.lstrip())]
444
+
445
+
446
+ def _extract_placeholders_from_scenario(step_lines: list) -> set:
447
+ """Extract placeholders from scenario step lines, ignoring docstrings."""
448
+ placeholders = set()
449
+ in_docstring = False
450
+
451
+ for line in step_lines:
452
+ step_line = line.strip()
453
+ if not step_line:
454
+ continue
455
+
456
+ in_docstring = _update_docstring_state(step_line, in_docstring)
457
+
458
+ if not in_docstring and '"""' not in step_line:
459
+ found = re.findall(r'<([^>]+)>', step_line)
460
+ placeholders.update(found)
461
+
462
+ return placeholders
463
+
464
+
465
+ def _update_docstring_state(line: str, current_state: bool) -> bool:
466
+ """Update the docstring state based on the current line."""
467
+ if '"""' in line:
468
+ return not current_state
469
+ return current_state
470
+
471
+
472
+ def _convert_to_scenario_outline(scenario_lines: list, placeholders: set, indentation: str) -> list:
473
+ """Convert scenario lines to scenario outline format with examples table."""
474
+ first_line = scenario_lines[0]
475
+ title = first_line.strip()[len('Scenario:'):].strip()
476
+
477
+ new_lines = [f"{indentation}Scenario Outline: {title}"]
478
+ new_lines.extend(scenario_lines[1:])
479
+ new_lines.append("")
480
+
481
+ examples_lines = _create_examples_table(placeholders, indentation)
482
+ new_lines.extend(examples_lines)
483
+
484
+ return new_lines
485
+
486
+
487
+ def _create_examples_table(placeholders: set, base_indentation: str) -> list:
488
+ """Create the Examples table for the scenario outline."""
489
+ examples_indentation = base_indentation + " "
490
+ table_indentation = examples_indentation + " "
491
+
492
+ sorted_placeholders = sorted(placeholders)
493
+ header = "| " + " | ".join(sorted_placeholders) + " |"
494
+ sample_row = "| " + " | ".join(f"<{p}_value>" for p in sorted_placeholders) + " |"
495
+
496
+ return [
497
+ f"{examples_indentation}Examples:",
498
+ f"{table_indentation}{header}",
499
+ f"{table_indentation}{sample_row}"
500
+ ]
501
+
502
+
356
503
  def fix_title_mismatch(
357
504
  file_path: str, artefact_text: str, artefact_class, **kwargs
358
505
  ) -> str:
@@ -429,6 +576,42 @@ def fix_rule(
429
576
  return artefact.serialize()
430
577
 
431
578
 
579
+ def fix_misplaced_content(file_path: str, artefact_text: str, **kwargs) -> str:
580
+ """
581
+ Deterministically fixes content like 'Rule:' or 'Estimate:' misplaced in the description.
582
+ """
583
+ lines = artefact_text.splitlines()
584
+
585
+ desc_start_idx = -1
586
+ for i, line in enumerate(lines):
587
+ if line.strip().startswith("Description:"):
588
+ desc_start_idx = i
589
+ break
590
+
591
+ if desc_start_idx == -1:
592
+ return artefact_text # No description, nothing to fix.
593
+
594
+ pre_desc_lines = lines[:desc_start_idx]
595
+ desc_line = lines[desc_start_idx]
596
+ post_desc_lines = lines[desc_start_idx+1:]
597
+
598
+ misplaced_content = []
599
+ new_post_desc_lines = []
600
+
601
+ for line in post_desc_lines:
602
+ if line.strip().startswith("Rule:") or line.strip().startswith("Estimate:"):
603
+ misplaced_content.append(line)
604
+ else:
605
+ new_post_desc_lines.append(line)
606
+
607
+ if not misplaced_content:
608
+ return artefact_text
609
+
610
+ # Rebuild the file content
611
+ final_lines = pre_desc_lines + misplaced_content + [""] + [desc_line] + new_post_desc_lines
612
+ return "\n".join(final_lines)
613
+
614
+
432
615
  def should_skip_issue(deterministic_issue, deterministic, non_deterministic, file_path) -> bool:
433
616
  if not non_deterministic and not deterministic_issue:
434
617
  print(f"Skipping non-deterministic fix for {file_path} as per request.")
@@ -475,7 +658,7 @@ def apply_non_deterministic_fix(
475
658
  corrected_artefact = run_agent(prompt, artefact_class)
476
659
  corrected_text = corrected_artefact.serialize()
477
660
  except Exception as e:
478
- print(f" ❌ LLM agent failed to fix artefact at {file_path}: {e}")
661
+ print(f" ❌ LLM agent failed to fix artefact at {file_path}: {e}")
479
662
  return None
480
663
  return corrected_text
481
664
 
@@ -504,7 +687,7 @@ def attempt_autofix_loop(
504
687
  print(
505
688
  f"Attempting to fix {file_path} (Attempt {attempt + 1}/{max_attempts})..."
506
689
  )
507
- print(f" Reason: {current_reason}")
690
+ print(f" Reason: {current_reason}")
508
691
 
509
692
  artefact_text = read_artefact(file_path)
510
693
  if artefact_text is None:
@@ -536,13 +719,13 @@ def attempt_autofix_loop(
536
719
 
537
720
  if corrected_text is None or corrected_text.strip() == artefact_text.strip():
538
721
  print(
539
- " Fixing attempt did not alter the file. Stopping to prevent infinite loop."
722
+ " Fixing attempt did not alter the file. Stopping to prevent infinite loop."
540
723
  )
541
724
  return False
542
725
 
543
726
  write_corrected_artefact(file_path, corrected_text)
544
727
 
545
- print(" File modified. Re-classifying artefact information for next check...")
728
+ print(" File modified. Re-classifying artefact information for next check...")
546
729
  classified_artefact_info = populate_classified_artefact_info(classified_artefact_info, force=True)
547
730
 
548
731
  print(f"❌ Failed to fix {file_path} after {max_attempts} attempts.")
@@ -565,6 +748,9 @@ def apply_autofix(
565
748
  "Filename-Title Mismatch": fix_title_mismatch,
566
749
  "Invalid Contribution Reference": fix_contribution,
567
750
  "Rule Mismatch": fix_rule,
751
+ "Scenario Contains Placeholders": fix_scenario_placeholder_mismatch,
752
+ "Found 'Rule:' inside description": fix_misplaced_content,
753
+ "Found 'Estimate:' inside description": fix_misplaced_content,
568
754
  }
569
755
 
570
756
  artefact_type, artefact_class = determine_artefact_type_and_class(classifier)
@@ -583,4 +769,4 @@ def apply_autofix(
583
769
  deterministic=deterministic,
584
770
  non_deterministic=non_deterministic,
585
771
  classified_artefact_info=classified_artefact_info,
586
- )
772
+ )
@@ -38,11 +38,9 @@ class ArtefactCreator:
38
38
  destination = Path(dir_path) / dest_name
39
39
 
40
40
  if not source.exists():
41
- print("[ERROR] Source file does not exist!")
42
41
  raise FileNotFoundError(f"Source file {source} not found!")
43
42
 
44
43
  if not destination.parent.exists():
45
- print("[ERROR] Destination directory does not exist!")
46
44
  raise NotADirectoryError(f"Destination directory {destination.parent} does not exist!")
47
45
 
48
46
  copyfile(source, destination)
@@ -70,9 +68,7 @@ class ArtefactCreator:
70
68
  def validate_template(self, template_path, classifier):
71
69
  template_name = f"template.{classifier}"
72
70
  if not self.template_exists(template_path, template_name):
73
- print(f"Template file '{template_name}' not found in the specified template path.")
74
- return False
75
- return True
71
+ raise FileNotFoundError(f"Template file '{template_name}' not found in the specified template path.")
76
72
 
77
73
  def set_artefact_parent(self, artefact, parent_classifier, parent_file_name) -> Artefact:
78
74
  classified_artefacts = ArtefactReader.read_artefacts()
@@ -94,8 +90,7 @@ class ArtefactCreator:
94
90
  navigator.navigate_to_target()
95
91
 
96
92
  if not Classifier.is_valid_classifier(classifier):
97
- print("Invalid classifier provided. Please provide a valid classifier.")
98
- return
93
+ raise ValueError("Invalid classifier provided. Please provide a valid classifier.")
99
94
 
100
95
  sub_directory = Classifier.get_sub_directory(classifier)
101
96
  file_path = self.file_system.path.join(sub_directory, f"{filename}.{classifier}")
@@ -106,7 +101,7 @@ class ArtefactCreator:
106
101
  if not self.handle_existing_files(file_exists):
107
102
  return
108
103
 
109
- artefact = template_artefact_of_type(classifier, filename, False)
104
+ artefact = template_artefact_of_type(classifier, filename, True)
110
105
 
111
106
  if parent_classifier and parent_name:
112
107
  artefact.set_contribution(
@@ -114,6 +109,8 @@ class ArtefactCreator:
114
109
  classifier=parent_classifier,
115
110
  rule=rule
116
111
  )
112
+ else:
113
+ artefact.set_contribution(None, None, None)
117
114
 
118
115
  artefact_content = artefact.serialize()
119
116
  rmtree(dir_path, ignore_errors=True)
@@ -20,16 +20,14 @@ class ArtefactDeleter:
20
20
  self.navigate_to_target()
21
21
 
22
22
  if not Classifier.is_valid_classifier(classifier):
23
- print("Invalid classifier provided. Please provide a valid classifier.")
24
- return
23
+ raise ValueError("Invalid classifier provided. Please provide a valid classifier.")
25
24
 
26
25
  sub_directory = Classifier.get_sub_directory(classifier)
27
26
  file_path = self.file_system.path.join(sub_directory, f"{filename}.{classifier}")
28
27
  dir_path = self.file_system.path.join(sub_directory, f"{filename}.data")
29
28
 
30
29
  if not self.file_system.path.exists(file_path):
31
- print(f"Artefact {file_path} not found.")
32
- return
30
+ raise FileNotFoundError(f"Artefact {file_path} not found.")
33
31
  if not force:
34
32
  user_choice = input(f"Are you sure you want to delete the file {filename} and data directory if existing? (y/N): ")
35
33
 
@@ -1,11 +1,16 @@
1
1
  import difflib
2
2
  from textwrap import indent
3
3
  from typing import Optional
4
+ from . import error_handler
5
+ from ara_cli.error_handler import AraError
4
6
 
5
7
 
6
- def suggest_close_names(artefact_name: str, all_artefact_names: list[str], message: str, cutoff=0.5):
8
+ def suggest_close_names(artefact_name: str, all_artefact_names: list[str], message: str, cutoff=0.5, report_as_error: bool = False):
7
9
  closest_matches = difflib.get_close_matches(artefact_name, all_artefact_names, cutoff=cutoff)
8
- print(message)
10
+ if report_as_error:
11
+ error_handler.report_error(AraError(message))
12
+ else:
13
+ print(message)
9
14
  if not closest_matches:
10
15
  return
11
16
  print("Closest matches:")
@@ -13,23 +18,25 @@ def suggest_close_names(artefact_name: str, all_artefact_names: list[str], messa
13
18
  print(f" - {match}")
14
19
 
15
20
 
16
- def suggest_close_name_matches(artefact_name: str, all_artefact_names: list[str]):
21
+ def suggest_close_name_matches(artefact_name: str, all_artefact_names: list[str], report_as_error: bool = False):
17
22
  message = f"No match found for artefact with name '{artefact_name}'"
18
23
 
19
24
  suggest_close_names(
20
25
  artefact_name=artefact_name,
21
26
  all_artefact_names=all_artefact_names,
22
- message=message
27
+ message=message,
28
+ report_as_error=report_as_error
23
29
  )
24
30
 
25
31
 
26
- def suggest_close_name_matches_for_parent(artefact_name: str, all_artefact_names: list[str], parent_name: str):
32
+ def suggest_close_name_matches_for_parent(artefact_name: str, all_artefact_names: list[str], parent_name: str, report_as_error: bool = False):
27
33
  message = f"No match found for parent of '{artefact_name}' with name '{parent_name}'"
28
34
 
29
35
  suggest_close_names(
30
36
  artefact_name=parent_name,
31
37
  all_artefact_names=all_artefact_names,
32
- message=message
38
+ message=message,
39
+ report_as_error=report_as_error
33
40
  )
34
41
 
35
42
 
@@ -1,9 +1,13 @@
1
1
  from ara_cli.file_classifier import FileClassifier
2
2
  from ara_cli.artefact_reader import ArtefactReader
3
3
  from ara_cli.file_lister import list_files_in_directory
4
- from ara_cli.artefact_models.artefact_model import Artefact
5
4
  from ara_cli.list_filter import ListFilter, filter_list
6
5
  from ara_cli.artefact_fuzzy_search import suggest_close_name_matches
6
+ from ara_cli.artefact_models.artefact_data_retrieval import (
7
+ artefact_content_retrieval,
8
+ artefact_path_retrieval,
9
+ artefact_tags_retrieval,
10
+ )
7
11
  import os
8
12
 
9
13
 
@@ -11,43 +15,18 @@ class ArtefactLister:
11
15
  def __init__(self, file_system=None):
12
16
  self.file_system = file_system or os
13
17
 
14
- @staticmethod
15
- def artefact_content_retrieval(artefact: Artefact):
16
- content = artefact.serialize()
17
- return content
18
-
19
- @staticmethod
20
- def artefact_path_retrieval(artefact: Artefact):
21
- return artefact.file_path
22
-
23
- @staticmethod
24
- def artefact_tags_retrieval(artefact: Artefact):
25
- final_tags = []
26
-
27
- if not artefact:
28
- return []
29
-
30
- final_tags.extend([f"user_{user}" for user in artefact.users])
31
- final_tags.append(artefact.status)
32
- final_tags.extend(artefact.tags)
33
-
34
- return final_tags
35
-
36
18
  def filter_artefacts(self, classified_files: list, list_filter: ListFilter):
37
19
  filtered_list = filter_list(
38
20
  list_to_filter=classified_files,
39
21
  list_filter=list_filter,
40
- content_retrieval_strategy=ArtefactLister.artefact_content_retrieval,
41
- file_path_retrieval=ArtefactLister.artefact_path_retrieval,
42
- tag_retrieval=ArtefactLister.artefact_tags_retrieval
22
+ content_retrieval_strategy=artefact_content_retrieval,
23
+ file_path_retrieval=artefact_path_retrieval,
24
+ tag_retrieval=artefact_tags_retrieval,
43
25
  )
44
26
  return filtered_list
45
27
 
46
28
  def list_files(
47
- self,
48
- tags=None,
49
- navigate_to_target=False,
50
- list_filter: ListFilter | None = None
29
+ self, tags=None, navigate_to_target=False, list_filter: ListFilter | None = None
51
30
  ):
52
31
  artefact_list = ArtefactReader.read_artefacts(tags=tags)
53
32
  artefact_list = self.filter_artefacts(artefact_list, list_filter)
@@ -60,20 +39,18 @@ class ArtefactLister:
60
39
  file_classifier.print_classified_files(filtered_artefact_list)
61
40
 
62
41
  def list_branch(
63
- self,
64
- classifier,
65
- artefact_name,
66
- list_filter: ListFilter | None = None
42
+ self, classifier, artefact_name, list_filter: ListFilter | None = None
67
43
  ):
68
44
  file_classifier = FileClassifier(os)
69
45
  classified_artefacts = file_classifier.classify_files()
70
46
  artefact_info = classified_artefacts.get(classifier, [])
71
- matching_artefact_info = [p for p in artefact_info if p["title"] == artefact_name]
47
+ matching_artefact_info = [
48
+ p for p in artefact_info if p["title"] == artefact_name
49
+ ]
72
50
 
73
51
  if not matching_artefact_info:
74
52
  suggest_close_name_matches(
75
- artefact_name,
76
- [info["title"] for info in artefact_info]
53
+ artefact_name, [info["title"] for info in artefact_info]
77
54
  )
78
55
 
79
56
  artefacts_by_classifier = {classifier: []}
@@ -82,29 +59,28 @@ class ArtefactLister:
82
59
  classifier=classifier,
83
60
  artefacts_by_classifier=artefacts_by_classifier,
84
61
  )
85
- artefacts_by_classifier = self.filter_artefacts(artefacts_by_classifier, list_filter)
62
+ artefacts_by_classifier = self.filter_artefacts(
63
+ artefacts_by_classifier, list_filter
64
+ )
86
65
  file_classifier.print_classified_files(artefacts_by_classifier)
87
66
 
88
67
  def list_children(
89
- self,
90
- classifier,
91
- artefact_name,
92
- list_filter: ListFilter | None = None
68
+ self, classifier, artefact_name, list_filter: ListFilter | None = None
93
69
  ):
94
70
  file_classifier = FileClassifier(os)
95
71
  classified_artefacts = file_classifier.classify_files()
96
72
  artefact_info = classified_artefacts.get(classifier, [])
97
- matching_artefact_info = [p for p in artefact_info if p["title"] == artefact_name]
73
+ matching_artefact_info = [
74
+ p for p in artefact_info if p["title"] == artefact_name
75
+ ]
98
76
 
99
77
  if not matching_artefact_info:
100
78
  suggest_close_name_matches(
101
- artefact_name,
102
- [info["title"] for info in artefact_info]
79
+ artefact_name, [info["title"] for info in artefact_info]
103
80
  )
104
81
 
105
82
  child_artefacts = ArtefactReader.find_children(
106
- artefact_name=artefact_name,
107
- classifier=classifier
83
+ artefact_name=artefact_name, classifier=classifier
108
84
  )
109
85
 
110
86
  child_artefacts = self.filter_artefacts(child_artefacts, list_filter)
@@ -112,25 +88,23 @@ class ArtefactLister:
112
88
  file_classifier.print_classified_files(child_artefacts)
113
89
 
114
90
  def list_data(
115
- self,
116
- classifier,
117
- artefact_name,
118
- list_filter: ListFilter | None = None
91
+ self, classifier, artefact_name, list_filter: ListFilter | None = None
119
92
  ):
120
93
  file_classifier = FileClassifier(os)
121
94
  classified_artefact_info = file_classifier.classify_files()
122
95
  artefact_info_dict = classified_artefact_info.get(classifier, [])
123
96
 
124
- matching_info = [info for info in artefact_info_dict if info["title"] == artefact_name]
97
+ matching_info = [
98
+ info for info in artefact_info_dict if info["title"] == artefact_name
99
+ ]
125
100
 
126
101
  if not matching_info:
127
102
  suggest_close_name_matches(
128
- artefact_name,
129
- [info["title"] for info in artefact_info_dict]
103
+ artefact_name, [info["title"] for info in artefact_info_dict]
130
104
  )
131
105
  return
132
106
 
133
107
  artefact_info = matching_info[0]
134
- data_dir = os.path.splitext(artefact_info["file_path"])[0] + '.data'
108
+ data_dir = os.path.splitext(artefact_info["file_path"])[0] + ".data"
135
109
  if os.path.exists(data_dir):
136
110
  list_files_in_directory(data_dir, list_filter)
@@ -0,0 +1,23 @@
1
+ from ara_cli.artefact_models.artefact_model import Artefact
2
+
3
+
4
+ def artefact_content_retrieval(artefact: Artefact):
5
+ content = artefact.serialize()
6
+ return content
7
+
8
+
9
+ def artefact_path_retrieval(artefact: Artefact):
10
+ return artefact.file_path
11
+
12
+
13
+ def artefact_tags_retrieval(artefact: Artefact):
14
+ final_tags = []
15
+
16
+ if not artefact:
17
+ return []
18
+
19
+ final_tags.extend([f"user_{user}" for user in artefact.users])
20
+ final_tags.append(artefact.status)
21
+ final_tags.extend(artefact.tags)
22
+
23
+ return final_tags