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/__init__.py
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
import warnings
|
|
2
|
+
from .error_handler import ErrorHandler
|
|
3
|
+
|
|
2
4
|
|
|
3
5
|
whitelisted_commands = ["RERUN", "SEND", "EXTRACT", "LOAD_IMAGE", "CHOOSE_MODEL", "CHOOSE_EXTRACTION_MODEL", "CURRENT_MODEL", "CURRENT_EXTRACTION_MODEL", "LIST_MODELS"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
error_handler = ErrorHandler()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# ANSI escape codes for coloring
|
|
12
|
+
YELLOW = '\033[93m'
|
|
13
|
+
RESET = '\033[0m'
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def format_warning(message, category, *args, **kwargs):
|
|
17
|
+
return f'{YELLOW}{category.__name__}: {message}{RESET}\n'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
warnings.formatwarning = format_warning
|
ara_cli/__main__.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# PYTHON_ARGCOMPLETE_OK
|
|
2
2
|
from ara_cli.ara_command_parser import action_parser
|
|
3
|
+
from ara_cli.error_handler import AraError
|
|
3
4
|
from ara_cli.version import __version__
|
|
4
5
|
from ara_cli.ara_command_action import (
|
|
5
6
|
create_action,
|
|
@@ -22,8 +23,10 @@ from ara_cli.ara_command_action import (
|
|
|
22
23
|
autofix_action,
|
|
23
24
|
extract_action
|
|
24
25
|
)
|
|
26
|
+
from . import error_handler
|
|
25
27
|
import argcomplete
|
|
26
28
|
import sys
|
|
29
|
+
from os import getenv
|
|
27
30
|
|
|
28
31
|
|
|
29
32
|
def define_action_mapping():
|
|
@@ -51,31 +54,74 @@ def define_action_mapping():
|
|
|
51
54
|
|
|
52
55
|
|
|
53
56
|
def handle_invalid_action(args):
|
|
54
|
-
|
|
57
|
+
raise AraError("Invalid action provided. Type ara -h for help", error_code=1)
|
|
55
58
|
|
|
56
59
|
|
|
57
|
-
def
|
|
58
|
-
|
|
60
|
+
def is_debug_mode_enabled():
|
|
61
|
+
"""Check if debug mode is enabled via environment variable."""
|
|
62
|
+
return getenv('ARA_DEBUG', '').lower() in ('1', 'true', 'yes')
|
|
59
63
|
|
|
64
|
+
|
|
65
|
+
def setup_parser():
|
|
66
|
+
"""Create and configure the argument parser."""
|
|
67
|
+
parser = action_parser()
|
|
68
|
+
|
|
60
69
|
# Show examples when help is called
|
|
61
70
|
if any(arg in sys.argv for arg in ["-h", "--help"]):
|
|
62
71
|
parser.add_examples = True
|
|
63
|
-
|
|
72
|
+
|
|
64
73
|
parser.add_argument(
|
|
65
74
|
"-v", "--version", action="version", version=f"%(prog)s {__version__}"
|
|
66
75
|
)
|
|
76
|
+
|
|
77
|
+
parser.add_argument(
|
|
78
|
+
"--debug", action="store_true", help="Enable debug mode for detailed error output"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return parser
|
|
67
82
|
|
|
68
|
-
action_mapping = define_action_mapping()
|
|
69
83
|
|
|
70
|
-
|
|
84
|
+
def configure_debug_mode(args, env_debug_mode):
|
|
85
|
+
"""Configure debug mode based on arguments and environment."""
|
|
86
|
+
if (hasattr(args, 'debug') and args.debug) or env_debug_mode:
|
|
87
|
+
error_handler.debug_mode = True
|
|
71
88
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
89
|
+
|
|
90
|
+
def should_show_help(args):
|
|
91
|
+
"""Check if help should be displayed."""
|
|
92
|
+
return not hasattr(args, "action") or not args.action
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def execute_action(args, action_mapping):
|
|
96
|
+
"""Execute the specified action."""
|
|
76
97
|
action = action_mapping.get(args.action, handle_invalid_action)
|
|
77
98
|
action(args)
|
|
78
99
|
|
|
79
100
|
|
|
101
|
+
def cli():
|
|
102
|
+
debug_mode = is_debug_mode_enabled()
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
parser = setup_parser()
|
|
106
|
+
action_mapping = define_action_mapping()
|
|
107
|
+
|
|
108
|
+
argcomplete.autocomplete(parser)
|
|
109
|
+
args = parser.parse_args()
|
|
110
|
+
|
|
111
|
+
configure_debug_mode(args, debug_mode)
|
|
112
|
+
|
|
113
|
+
if should_show_help(args):
|
|
114
|
+
parser.print_help()
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
execute_action(args, action_mapping)
|
|
118
|
+
|
|
119
|
+
except KeyboardInterrupt:
|
|
120
|
+
print("\n[INFO] Operation cancelled by user", file=sys.stderr)
|
|
121
|
+
sys.exit(130) # Standard exit code for Ctrl+C
|
|
122
|
+
except Exception as e:
|
|
123
|
+
error_handler.handle_error(e, context="cli")
|
|
124
|
+
|
|
125
|
+
|
|
80
126
|
if __name__ == "__main__":
|
|
81
|
-
cli()
|
|
127
|
+
cli()
|
ara_cli/ara_command_action.py
CHANGED
|
@@ -2,17 +2,19 @@ from os.path import join
|
|
|
2
2
|
import os
|
|
3
3
|
import sys
|
|
4
4
|
import json
|
|
5
|
+
from ara_cli.error_handler import AraError
|
|
6
|
+
from ara_cli.error_handler import handle_errors, AraValidationError
|
|
5
7
|
from ara_cli.output_suppressor import suppress_stdout
|
|
6
8
|
from ara_cli.artefact_fuzzy_search import suggest_close_name_matches
|
|
7
|
-
from . import whitelisted_commands
|
|
9
|
+
from . import whitelisted_commands, error_handler
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
def check_validity(condition, error_message):
|
|
11
13
|
if not condition:
|
|
12
|
-
|
|
13
|
-
sys.exit(1)
|
|
14
|
+
raise AraValidationError(error_message)
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
@handle_errors(context="create action", error_handler=error_handler)
|
|
16
18
|
def create_action(args):
|
|
17
19
|
from ara_cli.artefact_creator import ArtefactCreator
|
|
18
20
|
from ara_cli.classifier import Classifier
|
|
@@ -62,6 +64,7 @@ def create_action(args):
|
|
|
62
64
|
artefact_creator.run(args.parameter, args.classifier, parent_classifier, parent_name, rule)
|
|
63
65
|
|
|
64
66
|
|
|
67
|
+
@handle_errors(context="delete action", error_handler=error_handler)
|
|
65
68
|
def delete_action(args):
|
|
66
69
|
from ara_cli.artefact_deleter import ArtefactDeleter
|
|
67
70
|
|
|
@@ -69,6 +72,7 @@ def delete_action(args):
|
|
|
69
72
|
artefact_deleter.delete(args.parameter, args.classifier, args.force)
|
|
70
73
|
|
|
71
74
|
|
|
75
|
+
@handle_errors(context="rename action", error_handler=error_handler)
|
|
72
76
|
def rename_action(args):
|
|
73
77
|
from ara_cli.artefact_renamer import ArtefactRenamer
|
|
74
78
|
from ara_cli.classifier import Classifier
|
|
@@ -82,6 +86,7 @@ def rename_action(args):
|
|
|
82
86
|
artefact_renamer.rename(args.parameter, args.aspect, args.classifier)
|
|
83
87
|
|
|
84
88
|
|
|
89
|
+
@handle_errors(context="rename action", error_handler=error_handler)
|
|
85
90
|
def list_action(args):
|
|
86
91
|
from ara_cli.artefact_lister import ArtefactLister
|
|
87
92
|
from ara_cli.list_filter import ListFilter
|
|
@@ -131,6 +136,7 @@ def list_action(args):
|
|
|
131
136
|
artefact_lister.list_files(list_filter=list_filter)
|
|
132
137
|
|
|
133
138
|
|
|
139
|
+
@handle_errors(context="list-tags action", error_handler=error_handler)
|
|
134
140
|
def list_tags_action(args):
|
|
135
141
|
from ara_cli.tag_extractor import TagExtractor
|
|
136
142
|
from ara_cli.list_filter import ListFilter
|
|
@@ -155,6 +161,7 @@ def list_tags_action(args):
|
|
|
155
161
|
print(output)
|
|
156
162
|
|
|
157
163
|
|
|
164
|
+
@handle_errors(context="prompt action", error_handler=error_handler)
|
|
158
165
|
def prompt_action(args):
|
|
159
166
|
from ara_cli.classifier import Classifier
|
|
160
167
|
from ara_cli.filename_validator import is_valid_filename
|
|
@@ -227,6 +234,7 @@ def prompt_action(args):
|
|
|
227
234
|
raise ValueError(f"Unknown command '{init}' provided.")
|
|
228
235
|
|
|
229
236
|
|
|
237
|
+
@handle_errors(context="chat action", error_handler=error_handler)
|
|
230
238
|
def chat_action(args):
|
|
231
239
|
from ara_cli.chat import Chat
|
|
232
240
|
|
|
@@ -253,6 +261,7 @@ def chat_action(args):
|
|
|
253
261
|
chat.start()
|
|
254
262
|
|
|
255
263
|
|
|
264
|
+
@handle_errors(context="template action", error_handler=error_handler)
|
|
256
265
|
def template_action(args):
|
|
257
266
|
from ara_cli.classifier import Classifier
|
|
258
267
|
from ara_cli.template_manager import TemplatePathManager
|
|
@@ -265,6 +274,7 @@ def template_action(args):
|
|
|
265
274
|
print(content)
|
|
266
275
|
|
|
267
276
|
|
|
277
|
+
@handle_errors(context="fetch-templates action", error_handler=error_handler)
|
|
268
278
|
def fetch_templates_action(args):
|
|
269
279
|
import shutil
|
|
270
280
|
from ara_cli.ara_config import ConfigManager
|
|
@@ -294,6 +304,7 @@ def fetch_templates_action(args):
|
|
|
294
304
|
os.makedirs(join(local_prompt_modules_dir, subdir), exist_ok=True)
|
|
295
305
|
|
|
296
306
|
|
|
307
|
+
@handle_errors(context="read action", error_handler=error_handler)
|
|
297
308
|
def read_action(args):
|
|
298
309
|
from ara_cli.commands.read_command import ReadCommand
|
|
299
310
|
from ara_cli.list_filter import ListFilter
|
|
@@ -321,6 +332,7 @@ def read_action(args):
|
|
|
321
332
|
command.execute()
|
|
322
333
|
|
|
323
334
|
|
|
335
|
+
@handle_errors(context="reconnect action", error_handler=error_handler)
|
|
324
336
|
def reconnect_action(args):
|
|
325
337
|
from ara_cli.artefact_models.artefact_load import artefact_from_content
|
|
326
338
|
from ara_cli.artefact_models.artefact_model import Contribution
|
|
@@ -348,8 +360,7 @@ def reconnect_action(args):
|
|
|
348
360
|
)
|
|
349
361
|
|
|
350
362
|
if not artefact:
|
|
351
|
-
|
|
352
|
-
return
|
|
363
|
+
raise AraError(read_error_message)
|
|
353
364
|
|
|
354
365
|
parent = ArtefactReader.read_artefact(
|
|
355
366
|
artefact_name=parent_name,
|
|
@@ -358,8 +369,7 @@ def reconnect_action(args):
|
|
|
358
369
|
)
|
|
359
370
|
|
|
360
371
|
if not parent:
|
|
361
|
-
|
|
362
|
-
return
|
|
372
|
+
raise AraError(read_error_message)
|
|
363
373
|
|
|
364
374
|
contribution = Contribution(
|
|
365
375
|
artefact_name=parent.title,
|
|
@@ -367,13 +377,9 @@ def reconnect_action(args):
|
|
|
367
377
|
)
|
|
368
378
|
|
|
369
379
|
if rule:
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
feedback_message += f" using rule '{closest_rule}'"
|
|
374
|
-
except TypeError as e:
|
|
375
|
-
print(f"{type(e).__name__}:", e)
|
|
376
|
-
exit(1)
|
|
380
|
+
closest_rule = find_closest_rule(parent, rule)
|
|
381
|
+
contribution.rule = closest_rule
|
|
382
|
+
feedback_message += f" using rule '{closest_rule}'"
|
|
377
383
|
|
|
378
384
|
artefact.contribution = contribution
|
|
379
385
|
with open(artefact.file_path, 'w', encoding='utf-8') as file:
|
|
@@ -383,6 +389,7 @@ def reconnect_action(args):
|
|
|
383
389
|
print(feedback_message + ".")
|
|
384
390
|
|
|
385
391
|
|
|
392
|
+
@handle_errors(context="read-status action", error_handler=error_handler)
|
|
386
393
|
def read_status_action(args):
|
|
387
394
|
from ara_cli.file_classifier import FileClassifier
|
|
388
395
|
from ara_cli.artefact_models.artefact_load import artefact_from_content
|
|
@@ -396,7 +403,7 @@ def read_status_action(args):
|
|
|
396
403
|
|
|
397
404
|
all_artefact_names = [artefact_info["title"] for artefact_info in artefact_info_dicts]
|
|
398
405
|
if artefact_name not in all_artefact_names:
|
|
399
|
-
suggest_close_name_matches(artefact_name, all_artefact_names)
|
|
406
|
+
suggest_close_name_matches(artefact_name, all_artefact_names, report_as_error=True)
|
|
400
407
|
return
|
|
401
408
|
|
|
402
409
|
artefact_info = next(filter(
|
|
@@ -415,6 +422,7 @@ def read_status_action(args):
|
|
|
415
422
|
print(status)
|
|
416
423
|
|
|
417
424
|
|
|
425
|
+
@handle_errors(context="read-user action", error_handler=error_handler)
|
|
418
426
|
def read_user_action(args):
|
|
419
427
|
from ara_cli.artefact_models.artefact_load import artefact_from_content
|
|
420
428
|
from ara_cli.file_classifier import FileClassifier
|
|
@@ -428,7 +436,7 @@ def read_user_action(args):
|
|
|
428
436
|
|
|
429
437
|
all_artefact_names = [artefact_info["title"] for artefact_info in artefact_info_dicts]
|
|
430
438
|
if artefact_name not in all_artefact_names:
|
|
431
|
-
suggest_close_name_matches(artefact_name, all_artefact_names)
|
|
439
|
+
suggest_close_name_matches(artefact_name, all_artefact_names, report_as_error=True)
|
|
432
440
|
return
|
|
433
441
|
|
|
434
442
|
artefact_info = next(filter(
|
|
@@ -448,6 +456,7 @@ def read_user_action(args):
|
|
|
448
456
|
print(f" - {tag}")
|
|
449
457
|
|
|
450
458
|
|
|
459
|
+
@handle_errors(context="set-status action", error_handler=error_handler)
|
|
451
460
|
def set_status_action(args):
|
|
452
461
|
from ara_cli.artefact_models.artefact_model import ALLOWED_STATUS_VALUES
|
|
453
462
|
from ara_cli.artefact_models.artefact_load import artefact_from_content
|
|
@@ -490,6 +499,7 @@ def set_status_action(args):
|
|
|
490
499
|
print(f"Status of task '{artefact_name}' has been updated to '{new_status}'.")
|
|
491
500
|
|
|
492
501
|
|
|
502
|
+
@handle_errors(context="set-user action", error_handler=error_handler)
|
|
493
503
|
def set_user_action(args):
|
|
494
504
|
from ara_cli.file_classifier import FileClassifier
|
|
495
505
|
from ara_cli.artefact_models.artefact_load import artefact_from_content
|
|
@@ -528,6 +538,7 @@ def set_user_action(args):
|
|
|
528
538
|
print(f"User of task '{artefact_name}' has been updated to '{new_user}'.")
|
|
529
539
|
|
|
530
540
|
|
|
541
|
+
@handle_errors(context="classifier-directory action", error_handler=error_handler)
|
|
531
542
|
def classifier_directory_action(args):
|
|
532
543
|
from ara_cli.classifier import Classifier
|
|
533
544
|
|
|
@@ -536,6 +547,7 @@ def classifier_directory_action(args):
|
|
|
536
547
|
print(subdirectory)
|
|
537
548
|
|
|
538
549
|
|
|
550
|
+
@handle_errors(context="scan action", error_handler=error_handler)
|
|
539
551
|
def scan_action(args):
|
|
540
552
|
from ara_cli.file_classifier import FileClassifier
|
|
541
553
|
from ara_cli.artefact_scan import find_invalid_files, show_results
|
|
@@ -550,6 +562,7 @@ def scan_action(args):
|
|
|
550
562
|
show_results(invalid_artefacts)
|
|
551
563
|
|
|
552
564
|
|
|
565
|
+
@handle_errors(context="autofix_action", error_handler=error_handler)
|
|
553
566
|
def autofix_action(args):
|
|
554
567
|
from ara_cli.artefact_autofix import parse_report, apply_autofix, read_report_file
|
|
555
568
|
from ara_cli.file_classifier import FileClassifier
|
|
@@ -589,18 +602,17 @@ def autofix_action(args):
|
|
|
589
602
|
print("\nAutofix process completed. Please review the changes.")
|
|
590
603
|
|
|
591
604
|
|
|
605
|
+
@handle_errors(context="extract action", error_handler=error_handler)
|
|
592
606
|
def extract_action(args):
|
|
593
607
|
from ara_cli.commands.extract_command import ExtractCommand
|
|
594
608
|
|
|
595
609
|
filename = args.filename
|
|
596
610
|
force = args.force
|
|
597
611
|
write = getattr(args, 'write', False)
|
|
598
|
-
print(filename)
|
|
599
612
|
command = ExtractCommand(
|
|
600
613
|
file_name=filename,
|
|
601
614
|
force=force,
|
|
602
615
|
write=write,
|
|
603
|
-
output=lambda msg: print(msg, file=sys.stdout)
|
|
604
|
-
error_output=lambda msg: print(msg, file=sys.stderr)
|
|
616
|
+
output=lambda msg: print(msg, file=sys.stdout)
|
|
605
617
|
)
|
|
606
618
|
command.execute()
|
ara_cli/ara_config.py
CHANGED
|
@@ -6,6 +6,7 @@ from os.path import exists, dirname
|
|
|
6
6
|
from os import makedirs
|
|
7
7
|
from functools import lru_cache
|
|
8
8
|
import sys
|
|
9
|
+
import warnings
|
|
9
10
|
|
|
10
11
|
DEFAULT_CONFIG_LOCATION = "./ara/.araconfig/ara_config.json"
|
|
11
12
|
|
|
@@ -21,6 +22,7 @@ class ARAconfig(BaseModel):
|
|
|
21
22
|
{"source_dir": "./src"},
|
|
22
23
|
{"source_dir": "./tests"}
|
|
23
24
|
])
|
|
25
|
+
global_dirs: Optional[List[Dict[str, str]]] = Field(default=[])
|
|
24
26
|
glossary_dir: str = "./glossary"
|
|
25
27
|
doc_dir: str = "./docs"
|
|
26
28
|
local_prompt_templates_dir: str = "./ara/.araconfig"
|
|
@@ -162,6 +164,7 @@ def handle_unrecognized_keys(data: dict) -> dict:
|
|
|
162
164
|
cleaned_data[key] = value
|
|
163
165
|
return cleaned_data
|
|
164
166
|
|
|
167
|
+
|
|
165
168
|
# Function to read the JSON file and return an ARAconfig model
|
|
166
169
|
@lru_cache(maxsize=1)
|
|
167
170
|
def read_data(filepath: str) -> ARAconfig:
|
|
@@ -170,6 +173,16 @@ def read_data(filepath: str) -> ARAconfig:
|
|
|
170
173
|
If the file doesn't exist, it creates a default one.
|
|
171
174
|
If the file is invalid, it corrects only the broken parts.
|
|
172
175
|
"""
|
|
176
|
+
|
|
177
|
+
def warn_on_duplicate_llm_dict_key(ordered_pairs):
|
|
178
|
+
"""Reject duplicate keys."""
|
|
179
|
+
d = {}
|
|
180
|
+
for k, v in ordered_pairs:
|
|
181
|
+
if k in d:
|
|
182
|
+
warnings.warn(f"Duplicate LLM configuration identifier '{k}'. The previous entry will be removed.", UserWarning)
|
|
183
|
+
d[k] = v
|
|
184
|
+
return d
|
|
185
|
+
|
|
173
186
|
ensure_directory_exists(dirname(filepath))
|
|
174
187
|
|
|
175
188
|
if not exists(filepath):
|
|
@@ -181,7 +194,8 @@ def read_data(filepath: str) -> ARAconfig:
|
|
|
181
194
|
|
|
182
195
|
try:
|
|
183
196
|
with open(filepath, "r", encoding="utf-8") as file:
|
|
184
|
-
|
|
197
|
+
content = file.read()
|
|
198
|
+
data = json.loads(content, object_pairs_hook=warn_on_duplicate_llm_dict_key)
|
|
185
199
|
except json.JSONDecodeError as e:
|
|
186
200
|
print(f"Error: Invalid JSON in configuration file: {e}")
|
|
187
201
|
print("Creating a new configuration with defaults...")
|
|
@@ -206,7 +220,8 @@ def read_data(filepath: str) -> ARAconfig:
|
|
|
206
220
|
|
|
207
221
|
for field_name in error_fields:
|
|
208
222
|
print(f"-> Field '{field_name}' is invalid and will be reverted to its default value.")
|
|
209
|
-
|
|
223
|
+
if field_name in corrected_data:
|
|
224
|
+
corrected_data[field_name] = defaults.get(field_name)
|
|
210
225
|
|
|
211
226
|
print("--- End of Error Report ---")
|
|
212
227
|
|
ara_cli/artefact_autofix.py
CHANGED
|
@@ -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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
150
|
+
output_type=artefact_class,
|
|
148
151
|
instrument=True,
|
|
149
152
|
)
|
|
150
153
|
result = agent.run_sync(prompt)
|
|
151
|
-
return result.
|
|
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
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
|
236
|
+
print("Invalid choice. Aborting operation.")
|
|
218
237
|
return None
|
|
219
|
-
|
|
238
|
+
return choices[choice_index]
|
|
220
239
|
except ValueError:
|
|
221
|
-
print("Invalid input. Aborting
|
|
240
|
+
print("Invalid input. Aborting operation.")
|
|
222
241
|
return None
|
|
223
|
-
return choice
|
|
224
242
|
|
|
225
243
|
|
|
226
|
-
def
|
|
227
|
-
|
|
228
|
-
|
|
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:
|
|
@@ -565,6 +712,7 @@ def apply_autofix(
|
|
|
565
712
|
"Filename-Title Mismatch": fix_title_mismatch,
|
|
566
713
|
"Invalid Contribution Reference": fix_contribution,
|
|
567
714
|
"Rule Mismatch": fix_rule,
|
|
715
|
+
"Scenario Contains Placeholders": fix_scenario_placeholder_mismatch,
|
|
568
716
|
}
|
|
569
717
|
|
|
570
718
|
artefact_type, artefact_class = determine_artefact_type_and_class(classifier)
|