ara-cli 0.1.9.94__py3-none-any.whl → 0.1.9.96__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.
Files changed (40) hide show
  1. ara_cli/__init__.py +18 -1
  2. ara_cli/__main__.py +57 -11
  3. ara_cli/ara_command_action.py +31 -19
  4. ara_cli/ara_config.py +17 -2
  5. ara_cli/artefact_autofix.py +171 -23
  6. ara_cli/artefact_creator.py +5 -8
  7. ara_cli/artefact_deleter.py +2 -4
  8. ara_cli/artefact_fuzzy_search.py +13 -6
  9. ara_cli/artefact_models/artefact_templates.py +3 -3
  10. ara_cli/artefact_models/feature_artefact_model.py +25 -0
  11. ara_cli/artefact_reader.py +4 -5
  12. ara_cli/chat.py +79 -37
  13. ara_cli/commands/extract_command.py +4 -11
  14. ara_cli/error_handler.py +134 -0
  15. ara_cli/file_classifier.py +3 -2
  16. ara_cli/file_loaders/document_readers.py +233 -0
  17. ara_cli/file_loaders/file_loaders.py +123 -0
  18. ara_cli/file_loaders/image_processor.py +89 -0
  19. ara_cli/file_loaders/markdown_reader.py +75 -0
  20. ara_cli/file_loaders/text_file_loader.py +9 -11
  21. ara_cli/global_file_lister.py +61 -0
  22. ara_cli/prompt_extractor.py +1 -1
  23. ara_cli/prompt_handler.py +24 -4
  24. ara_cli/template_manager.py +14 -4
  25. ara_cli/update_config_prompt.py +7 -1
  26. ara_cli/version.py +1 -1
  27. {ara_cli-0.1.9.94.dist-info → ara_cli-0.1.9.96.dist-info}/METADATA +2 -1
  28. {ara_cli-0.1.9.94.dist-info → ara_cli-0.1.9.96.dist-info}/RECORD +40 -33
  29. tests/test_ara_command_action.py +66 -52
  30. tests/test_ara_config.py +28 -0
  31. tests/test_artefact_autofix.py +361 -5
  32. tests/test_chat.py +105 -36
  33. tests/test_file_classifier.py +23 -0
  34. tests/test_file_creator.py +3 -5
  35. tests/test_global_file_lister.py +131 -0
  36. tests/test_prompt_handler.py +26 -1
  37. tests/test_template_manager.py +5 -4
  38. {ara_cli-0.1.9.94.dist-info → ara_cli-0.1.9.96.dist-info}/WHEEL +0 -0
  39. {ara_cli-0.1.9.94.dist-info → ara_cli-0.1.9.96.dist-info}/entry_points.txt +0 -0
  40. {ara_cli-0.1.9.94.dist-info → ara_cli-0.1.9.96.dist-info}/top_level.txt +0 -0
@@ -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
 
@@ -148,9 +148,9 @@ def _default_feature(title: str, use_default_contribution: bool) -> FeatureArtef
148
148
  Scenario(
149
149
  title="<descriptive_scenario_title>",
150
150
  steps=[
151
- "Given <precondition>",
152
- "When <action>",
153
- "Then <expected result>"
151
+ "Given [precondition]",
152
+ "When [action]",
153
+ "Then [expected result]"
154
154
  ],
155
155
  ),
156
156
  ScenarioOutline(
@@ -148,6 +148,30 @@ class Scenario(BaseModel):
148
148
  raise ValueError("steps list must not be empty")
149
149
  return steps
150
150
 
151
+ @model_validator(mode='after')
152
+ def check_no_placeholders(cls, values: 'Scenario') -> 'Scenario':
153
+ """Ensure regular scenarios don't contain placeholders that should be in scenario outlines."""
154
+ placeholders = set()
155
+ for step in values.steps:
156
+ # Skip validation if step contains docstring placeholders (during parsing)
157
+ if '__DOCSTRING_PLACEHOLDER_' in step:
158
+ continue
159
+
160
+ # Skip validation if step contains docstring markers (after reinjection)
161
+ if '"""' in step:
162
+ continue
163
+
164
+ found = re.findall(r'<([^>]+)>', step)
165
+ placeholders.update(found)
166
+
167
+ if placeholders:
168
+ placeholder_list = ', '.join(f"<{p}>" for p in sorted(placeholders))
169
+ raise ValueError(
170
+ f"Scenario Contains Placeholders ({placeholder_list}) but is not a Scenario Outline. "
171
+ f"Use 'Scenario Outline:' instead of 'Scenario:' and provide an Examples table."
172
+ )
173
+ return values
174
+
151
175
  @classmethod
152
176
  def from_lines(cls, lines: List[str], start_idx: int) -> Tuple['Scenario', int]:
153
177
  """Parse a Scenario from a list of lines starting at start_idx."""
@@ -277,6 +301,7 @@ class FeatureArtefact(Artefact):
277
301
  f"FeatureArtefact must have artefact_type of '{ArtefactType.feature}', not '{v}'")
278
302
  return v
279
303
 
304
+
280
305
  @classmethod
281
306
  def _title_prefix(cls) -> str:
282
307
  return "Feature:"
@@ -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
@@ -12,8 +13,7 @@ class ArtefactReader:
12
13
  @staticmethod
13
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
18
  if not classified_file_info:
19
19
  file_classifier = FileClassifier(os)
@@ -74,7 +74,6 @@ class ArtefactReader:
74
74
 
75
75
  @staticmethod
76
76
  def read_artefacts(classified_artefacts=None, file_system=os, tags=None) -> Dict[str, List[Artefact]]:
77
- from ara_cli.artefact_models.artefact_load import artefact_from_content
78
77
 
79
78
  if classified_artefacts is None:
80
79
  file_classifier = FileClassifier(file_system)
@@ -89,7 +88,7 @@ class ArtefactReader:
89
88
  artefact = ArtefactReader.read_artefact(title, artefact_type, classified_artefacts)
90
89
  artefacts[artefact_type].append(artefact)
91
90
  except Exception as e:
92
- print(f"Failed to read {artefact_type} '{title}' with an error: ", e)
91
+ error_handler.report_error(e, f"reading {artefact_type} '{title}'")
93
92
  continue
94
93
  return artefacts
95
94
 
@@ -143,7 +142,7 @@ class ArtefactReader:
143
142
  ArtefactReader._ensure_classifier_key(classifier, artefacts_by_classifier)
144
143
 
145
144
  artefact = ArtefactReader._find_artefact_by_name(
146
- artefact_name,
145
+ artefact_name,
147
146
  classified_artefacts.get(classifier, [])
148
147
  )
149
148
 
ara_cli/chat.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import os
2
2
  import argparse
3
3
  import cmd2
4
+ from . import error_handler
4
5
  from ara_cli.prompt_handler import send_prompt
5
6
 
6
7
  from ara_cli.file_loaders.document_file_loader import DocumentFileLoader
@@ -16,6 +17,13 @@ load_parser = argparse.ArgumentParser()
16
17
  load_parser.add_argument('file_name', nargs='?', default='', help='File to load')
17
18
  load_parser.add_argument('--load-images', action='store_true', help='Extract and describe images from documents')
18
19
 
20
+ extract_parser = argparse.ArgumentParser()
21
+ extract_parser.add_argument('-f', '--force', action='store_true', help='Force extraction')
22
+ extract_parser.add_argument('-w','--write', action='store_true', help='Overwrite existing files without using LLM for merging.')
23
+
24
+ load_parser = argparse.ArgumentParser()
25
+ load_parser.add_argument('file_name', nargs='?', default='', help='File to load')
26
+ load_parser.add_argument('--load-images', action='store_true', help='Extract and describe images from documents')
19
27
 
20
28
 
21
29
  class Chat(cmd2.Cmd):
@@ -49,7 +57,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
49
57
  ".jpg": "image/jpeg",
50
58
  ".jpeg": "image/jpeg",
51
59
  }
52
-
60
+
53
61
  DOCUMENT_TYPE_EXTENSIONS = [".docx", ".doc", ".odt", ".pdf"]
54
62
 
55
63
  def __init__(
@@ -492,6 +500,13 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
492
500
  self.last_result = True
493
501
  return True
494
502
 
503
+ def onecmd(self, *args, **kwargs):
504
+ try:
505
+ return super().onecmd(*args, **kwargs)
506
+ except Exception as e:
507
+ error_handler.report_error(e)
508
+ return False
509
+
495
510
  def onecmd_plus_hooks(self, line, orig_rl_history_length):
496
511
  # store the full line for use with default()
497
512
  self.full_input = line
@@ -817,50 +832,77 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
817
832
  file_name=self.chat_name,
818
833
  force=args.force,
819
834
  write=args.write,
820
- output=self.poutput,
821
- error_output=self.perror
835
+ output=self.poutput
822
836
  )
823
837
  command.execute()
824
838
 
839
+ def _find_givens_files(self, file_name: str) -> list[str]:
840
+ """
841
+ Finds the givens files to be processed.
842
+ - If file_name is provided, it resolves that path.
843
+ - Otherwise, it looks for default givens files.
844
+ - If no defaults are found, it prompts the user.
845
+ Returns a list of absolute file paths or an empty list if none are found.
846
+ """
847
+ base_directory = os.path.dirname(self.chat_name)
848
+
849
+ def resolve_path(name):
850
+ """Inner helper to resolve a path relative to chat, then absolute."""
851
+ relative_path = os.path.join(base_directory, name)
852
+ if os.path.exists(relative_path):
853
+ return relative_path
854
+ if os.path.exists(name):
855
+ return name
856
+ return None
857
+
858
+ if file_name:
859
+ path = resolve_path(file_name)
860
+ if path:
861
+ return [path]
862
+ relative_path_for_error = os.path.join(base_directory, file_name)
863
+ self.perror(f"No givens file found at {relative_path_for_error} or {file_name}")
864
+ return []
865
+
866
+ # If no file_name, check for defaults
867
+ default_files_to_check = [
868
+ os.path.join(base_directory, "prompt.data", "config.prompt_givens.md"),
869
+ os.path.join(base_directory, "prompt.data", "config.prompt_global_givens.md")
870
+ ]
871
+ existing_defaults = [f for f in default_files_to_check if os.path.exists(f)]
872
+ if existing_defaults:
873
+ return existing_defaults
874
+
875
+ # No defaults found, prompt user
876
+ user_input = input("Please specify a givens file: ")
877
+ if not user_input:
878
+ self.poutput("Aborting.")
879
+ return []
880
+
881
+ path = resolve_path(user_input)
882
+ if path:
883
+ return [path]
884
+ self.perror(f"No givens file found at {user_input}. Aborting.")
885
+ return []
886
+
825
887
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
826
888
  def do_LOAD_GIVENS(self, file_name):
827
- """Load all files listed in a ./prompt.data/config.prompt_givens.md"""
828
- from ara_cli.directory_navigator import DirectoryNavigator
889
+ """Load all files listed in a ./prompt.data/config.prompt_givens.md and ./prompt.data/config.prompt_global_givens.md"""
829
890
  from ara_cli.prompt_handler import load_givens
830
891
 
831
- base_directory = os.path.dirname(self.chat_name)
832
-
833
- if file_name == "":
834
- file_name = f"{base_directory}/prompt.data/config.prompt_givens.md"
835
-
836
- # Check the relative path first
837
- relative_givens_path = os.path.join(base_directory, file_name)
838
- if os.path.exists(relative_givens_path):
839
- givens_path = relative_givens_path
840
- elif os.path.exists(file_name): # Check the absolute path
841
- givens_path = file_name
842
- else:
843
- print(f"No givens file found at {relative_givens_path} or {file_name}")
844
- user_input = input("Please specify a givens file: ")
845
- if os.path.exists(os.path.join(base_directory, user_input)):
846
- givens_path = os.path.join(base_directory, user_input)
847
- elif os.path.exists(user_input):
848
- givens_path = user_input
849
- else:
850
- print(f"No givens file found at {user_input}. Aborting.")
851
- return
892
+ givens_files_to_process = self._find_givens_files(file_name)
893
+ if not givens_files_to_process:
894
+ self.poutput("No givens files to load.")
895
+ return
852
896
 
853
- cwd = os.getcwd()
854
- navigator = DirectoryNavigator()
855
- navigator.navigate_to_target()
856
- os.chdir('..')
857
- content, image_data = load_givens(givens_path)
858
- os.chdir(cwd)
897
+ for givens_path in givens_files_to_process:
898
+ # The givens_path is absolute, and load_givens reconstructs absolute paths
899
+ # from the markdown file. No directory change is needed.
900
+ content, _ = load_givens(givens_path)
859
901
 
860
- with open(self.chat_name, 'a', encoding='utf-8') as chat_file:
861
- chat_file.write(content)
902
+ with open(self.chat_name, 'a', encoding='utf-8') as chat_file:
903
+ chat_file.write(content)
862
904
 
863
- print(f"Loaded files listed and marked in {givens_path}")
905
+ self.poutput(f"Loaded files listed and marked in {givens_path}")
864
906
 
865
907
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
866
908
  def do_SEND(self, _):
@@ -876,7 +918,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
876
918
 
877
919
  artefact = template_artefact_of_type(''.join(template_name))
878
920
  if not artefact:
879
- return
921
+ raise ValueError(f"No template for '{template_name}' found.")
880
922
  write_content = artefact.serialize()
881
923
  self.add_prompt_tag_if_needed(self.chat_name)
882
924
  with open(self.chat_name, 'a', encoding='utf-8') as chat_file:
@@ -884,7 +926,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
884
926
  print(f"Loaded {template_name} artefact template")
885
927
 
886
928
  def complete_LOAD_TEMPLATE(self, text, line, begidx, endidx):
887
- return self._complete_classifiers(self, text, line, begidx, endidx)
929
+ return self._complete_classifiers(text, line, begidx, endidx)
888
930
 
889
931
  def _complete_classifiers(self, text, line, begidx, endidx):
890
932
  from ara_cli.classifier import Classifier
@@ -3,20 +3,13 @@ from ara_cli.prompt_extractor import extract_responses
3
3
  import os
4
4
 
5
5
  class ExtractCommand(Command):
6
- def __init__(self, file_name, force=False, write=False, output=None, error_output=None):
6
+ def __init__(self, file_name, force=False, write=False, output=None):
7
7
  self.file_name = file_name
8
8
  self.force = force
9
9
  self.write = write
10
10
  self.output = output # Callable for standard output (optional)
11
- self.error_output = error_output # Callable for errors (optional)
12
11
 
13
12
  def execute(self, *args, **kwargs):
14
- try:
15
- extract_responses(self.file_name, True, force=self.force, write=self.write)
16
- if self.output:
17
- self.output("End of extraction")
18
- except Exception as e:
19
- if self.error_output:
20
- self.error_output(f"Extraction failed: {e}")
21
- else:
22
- raise
13
+ extract_responses(self.file_name, True, force=self.force, write=self.write)
14
+ if self.output:
15
+ self.output("End of extraction")
@@ -0,0 +1,134 @@
1
+ import sys
2
+ import traceback
3
+ from typing import Optional
4
+ from enum import Enum
5
+ from functools import wraps
6
+
7
+
8
+ RED = '\033[91m'
9
+ RESET = '\033[0m'
10
+
11
+
12
+ class ErrorLevel(Enum):
13
+ INFO = "INFO"
14
+ WARNING = "WARNING"
15
+ ERROR = "ERROR"
16
+ CRITICAL = "CRITICAL"
17
+
18
+
19
+ class AraError(Exception):
20
+ """Base exception class for ARA CLI errors"""
21
+
22
+ def __init__(
23
+ self, message: str, error_code: int = 1, level: ErrorLevel = ErrorLevel.ERROR
24
+ ):
25
+ self.message = message
26
+ self.error_code = error_code
27
+ self.level = level
28
+ super().__init__(self.message)
29
+
30
+
31
+ class AraValidationError(AraError):
32
+ """Raised when validation fails"""
33
+
34
+ def __init__(self, message: str):
35
+ super().__init__(message, error_code=2, level=ErrorLevel.ERROR)
36
+
37
+
38
+ class AraConfigurationError(AraError):
39
+ """Raised when configuration is invalid"""
40
+
41
+ def __init__(self, message: str):
42
+ super().__init__(message, error_code=4, level=ErrorLevel.ERROR)
43
+
44
+
45
+ class ErrorHandler:
46
+ """Centralized error handler for ARA CLI"""
47
+
48
+ def __init__(self, debug_mode: bool = False):
49
+ self.debug_mode = debug_mode
50
+
51
+ def handle_error(self, error: Exception, context: Optional[str] = None) -> None:
52
+ """Handle any error with standardized output"""
53
+ if isinstance(error, AraError):
54
+ self._handle_ara_error(error, context)
55
+ else:
56
+ self._handle_generic_error(error, context)
57
+
58
+ def _handle_ara_error(self, error: AraError, context: Optional[str] = None) -> None:
59
+ """Handle ARA-specific errors"""
60
+ self._report_ara_error(error, context)
61
+
62
+ sys.exit(error.error_code)
63
+
64
+ def _handle_generic_error(
65
+ self, error: Exception, context: Optional[str] = None
66
+ ) -> None:
67
+ """Handle generic Python errors"""
68
+ self._report_generic_error(error, context)
69
+
70
+ sys.exit(1)
71
+
72
+
73
+ def report_error(self, error: Exception, context: Optional[str] = None) -> None:
74
+ """Report error with standardized formatting but don't exit"""
75
+ if isinstance(error, AraError):
76
+ self._report_ara_error(error, context)
77
+ else:
78
+ self._report_generic_error(error, context)
79
+
80
+
81
+ def _report_ara_error(self, error: AraError, context: Optional[str] = None) -> None:
82
+ """Report ARA-specific errors without exiting"""
83
+ error_prefix = f"[{error.level.value}]"
84
+
85
+ if context:
86
+ print(f"{RED}{error_prefix} {context}: {error.message}{RESET}", file=sys.stderr)
87
+ else:
88
+ print(f"{RED}{error_prefix} {error.message}{RESET}", file=sys.stderr)
89
+
90
+ if self.debug_mode:
91
+ traceback.print_exc()
92
+
93
+
94
+ def _report_generic_error(self, error: Exception, context: Optional[str] = None) -> None:
95
+ """Report generic Python errors without exiting"""
96
+ error_type = type(error).__name__
97
+
98
+ if context:
99
+ print(f"{RED}[ERROR] {context}: {error_type}: {str(error)}{RESET}", file=sys.stderr)
100
+ else:
101
+ print(f"{RED}[ERROR] {error_type}: {str(error)}{RESET}", file=sys.stderr)
102
+
103
+ if self.debug_mode:
104
+ traceback.print_exc()
105
+
106
+
107
+ def validate_and_exit(
108
+ self, condition: bool, message: str, error_code: int = 1
109
+ ) -> None:
110
+ """Validate condition and exit with error if false"""
111
+ if not condition:
112
+ raise AraValidationError(message)
113
+
114
+
115
+ def handle_errors(_func=None, context: Optional[str] = None, error_handler: Optional[ErrorHandler] = None):
116
+ """Decorator to handle errors in action functions"""
117
+
118
+ def decorator(func):
119
+ @wraps(func)
120
+ def wrapper(*args, **kwargs):
121
+ nonlocal error_handler
122
+ if error_handler is None:
123
+ error_handler = ErrorHandler()
124
+
125
+ try:
126
+ return func(*args, **kwargs)
127
+ except Exception as e:
128
+ error_handler.handle_error(e, context or func.__name__)
129
+
130
+ return wrapper
131
+
132
+ if callable(_func):
133
+ return decorator(_func)
134
+ return decorator
@@ -1,3 +1,4 @@
1
+ from . import error_handler
1
2
  from ara_cli.classifier import Classifier
2
3
  from ara_cli.artefact_models.artefact_model import Artefact
3
4
  from ara_cli.artefact_fuzzy_search import find_closest_name_matches
@@ -33,8 +34,8 @@ class FileClassifier:
33
34
  if byte > 127:
34
35
  return True
35
36
  except Exception as e:
36
- # Handle unexpected errors while reading the file in binary mode
37
- print(f"Error while checking if file is binary: {e}")
37
+ error_handler.report_error(e, "checking if file is binary")
38
+ # print(f"Error while checking if file is binary: {e}")
38
39
  return False
39
40
 
40
41
  def read_file_with_fallback(self, file_path):