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.
- ara_cli/__init__.py +18 -1
- ara_cli/__main__.py +57 -11
- ara_cli/ara_command_action.py +31 -19
- ara_cli/ara_config.py +17 -2
- ara_cli/artefact_autofix.py +171 -23
- ara_cli/artefact_creator.py +5 -8
- ara_cli/artefact_deleter.py +2 -4
- ara_cli/artefact_fuzzy_search.py +13 -6
- ara_cli/artefact_models/artefact_templates.py +3 -3
- ara_cli/artefact_models/feature_artefact_model.py +25 -0
- ara_cli/artefact_reader.py +4 -5
- ara_cli/chat.py +79 -37
- ara_cli/commands/extract_command.py +4 -11
- ara_cli/error_handler.py +134 -0
- ara_cli/file_classifier.py +3 -2
- ara_cli/file_loaders/document_readers.py +233 -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 +9 -11
- ara_cli/global_file_lister.py +61 -0
- ara_cli/prompt_extractor.py +1 -1
- ara_cli/prompt_handler.py +24 -4
- ara_cli/template_manager.py +14 -4
- ara_cli/update_config_prompt.py +7 -1
- ara_cli/version.py +1 -1
- {ara_cli-0.1.9.94.dist-info → ara_cli-0.1.9.96.dist-info}/METADATA +2 -1
- {ara_cli-0.1.9.94.dist-info → ara_cli-0.1.9.96.dist-info}/RECORD +40 -33
- tests/test_ara_command_action.py +66 -52
- tests/test_ara_config.py +28 -0
- tests/test_artefact_autofix.py +361 -5
- tests/test_chat.py +105 -36
- tests/test_file_classifier.py +23 -0
- tests/test_file_creator.py +3 -5
- tests/test_global_file_lister.py +131 -0
- tests/test_prompt_handler.py +26 -1
- tests/test_template_manager.py +5 -4
- {ara_cli-0.1.9.94.dist-info → ara_cli-0.1.9.96.dist-info}/WHEEL +0 -0
- {ara_cli-0.1.9.94.dist-info → ara_cli-0.1.9.96.dist-info}/entry_points.txt +0 -0
- {ara_cli-0.1.9.94.dist-info → ara_cli-0.1.9.96.dist-info}/top_level.txt +0 -0
ara_cli/artefact_creator.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|
|
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)
|
ara_cli/artefact_deleter.py
CHANGED
|
@@ -20,16 +20,14 @@ class ArtefactDeleter:
|
|
|
20
20
|
self.navigate_to_target()
|
|
21
21
|
|
|
22
22
|
if not Classifier.is_valid_classifier(classifier):
|
|
23
|
-
|
|
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
|
-
|
|
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
|
|
ara_cli/artefact_fuzzy_search.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
152
|
-
"When
|
|
153
|
-
"Then
|
|
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:"
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
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
|
-
|
|
861
|
-
|
|
902
|
+
with open(self.chat_name, 'a', encoding='utf-8') as chat_file:
|
|
903
|
+
chat_file.write(content)
|
|
862
904
|
|
|
863
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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")
|
ara_cli/error_handler.py
ADDED
|
@@ -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
|
ara_cli/file_classifier.py
CHANGED
|
@@ -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
|
-
|
|
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):
|