ara-cli 0.1.9.76__py3-none-any.whl → 0.1.9.78__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.
- ara_cli/__main__.py +11 -4
- ara_cli/ara_command_parser.py +300 -92
- ara_cli/artefact_autofix.py +167 -89
- ara_cli/artefact_models/artefact_load.py +11 -3
- ara_cli/artefact_models/task_artefact_model.py +31 -23
- ara_cli/artefact_reader.py +74 -50
- ara_cli/chat.py +68 -1
- ara_cli/version.py +1 -1
- ara_cli-0.1.9.78.dist-info/METADATA +208 -0
- {ara_cli-0.1.9.76.dist-info → ara_cli-0.1.9.78.dist-info}/RECORD +15 -15
- tests/test_artefact_autofix.py +88 -1
- tests/test_chat.py +169 -30
- ara_cli-0.1.9.76.dist-info/METADATA +0 -16
- {ara_cli-0.1.9.76.dist-info → ara_cli-0.1.9.78.dist-info}/WHEEL +0 -0
- {ara_cli-0.1.9.76.dist-info → ara_cli-0.1.9.78.dist-info}/entry_points.txt +0 -0
- {ara_cli-0.1.9.76.dist-info → ara_cli-0.1.9.78.dist-info}/top_level.txt +0 -0
ara_cli/artefact_autofix.py
CHANGED
|
@@ -12,6 +12,15 @@ import difflib
|
|
|
12
12
|
import os
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
def populate_classified_artefact_info(
|
|
16
|
+
classified_artefact_info: Optional[dict], force: bool = False
|
|
17
|
+
):
|
|
18
|
+
if not classified_artefact_info or force:
|
|
19
|
+
file_classifier = FileClassifier(os)
|
|
20
|
+
classified_artefact_info = file_classifier.classify_files()
|
|
21
|
+
return classified_artefact_info
|
|
22
|
+
|
|
23
|
+
|
|
15
24
|
def read_report_file():
|
|
16
25
|
file_path = "incompatible_artefacts_report.md"
|
|
17
26
|
try:
|
|
@@ -30,6 +39,7 @@ def parse_report(content: str) -> Dict[str, List[Tuple[str, str]]]:
|
|
|
30
39
|
Parses the incompatible artefacts report and returns structured data.
|
|
31
40
|
Returns a dictionary where keys are artefact classifiers, and values are lists of (file_path, reason) tuples.
|
|
32
41
|
"""
|
|
42
|
+
|
|
33
43
|
def is_valid_report(lines: List[str]) -> bool:
|
|
34
44
|
return bool(lines) and lines[0] == "# Artefact Check Report"
|
|
35
45
|
|
|
@@ -219,7 +229,7 @@ def _has_valid_contribution(artefact: Artefact) -> bool:
|
|
|
219
229
|
|
|
220
230
|
|
|
221
231
|
def _update_rule(
|
|
222
|
-
artefact: Artefact, name: str, classifier: str, classified_file_info: dict
|
|
232
|
+
artefact: Artefact, name: str, classifier: str, classified_file_info: dict, delete_if_not_found: bool = False
|
|
223
233
|
) -> None:
|
|
224
234
|
"""Updates the rule in the contribution if a close match is found."""
|
|
225
235
|
rule = artefact.contribution.rule
|
|
@@ -234,8 +244,12 @@ def _update_rule(
|
|
|
234
244
|
rules = parent.rules
|
|
235
245
|
|
|
236
246
|
closest_rule_match = difflib.get_close_matches(rule, rules, cutoff=0.5)
|
|
237
|
-
if closest_rule_match:
|
|
238
|
-
artefact.contribution.rule =
|
|
247
|
+
if not closest_rule_match and delete_if_not_found:
|
|
248
|
+
artefact.contribution.rule = None
|
|
249
|
+
return
|
|
250
|
+
if not closest_rule_match:
|
|
251
|
+
return
|
|
252
|
+
artefact.contribution.rule = closest_rule_match[0]
|
|
239
253
|
|
|
240
254
|
|
|
241
255
|
def _set_contribution_multiple_matches(
|
|
@@ -281,9 +295,7 @@ def set_closest_contribution(
|
|
|
281
295
|
classifier = contribution.classifier
|
|
282
296
|
rule = contribution.rule
|
|
283
297
|
|
|
284
|
-
|
|
285
|
-
file_classifier = FileClassifier(os)
|
|
286
|
-
classified_file_info = file_classifier.classify_files()
|
|
298
|
+
classified_file_info = populate_classified_artefact_info(classified_artefact_info=classified_file_info)
|
|
287
299
|
|
|
288
300
|
all_artefact_names = extract_artefact_names_of_classifier(
|
|
289
301
|
classified_files=classified_file_info, classifier=classifier
|
|
@@ -299,7 +311,9 @@ def set_closest_contribution(
|
|
|
299
311
|
if not name or not classifier:
|
|
300
312
|
artefact.contribution = None
|
|
301
313
|
return artefact, True
|
|
302
|
-
print(
|
|
314
|
+
print(
|
|
315
|
+
f"Updating contribution of {artefact._artefact_type().value} '{artefact.title}' to {classifier} '{name}'"
|
|
316
|
+
)
|
|
303
317
|
contribution.artefact_name = name
|
|
304
318
|
contribution.classifier = classifier
|
|
305
319
|
artefact.contribution = contribution
|
|
@@ -378,100 +392,118 @@ def fix_contribution(
|
|
|
378
392
|
classified_artefact_info: dict,
|
|
379
393
|
**kwargs,
|
|
380
394
|
):
|
|
381
|
-
|
|
382
|
-
file_classifier = FileClassifier(os)
|
|
383
|
-
classified_artefact_info = file_classifier.classify_files()
|
|
395
|
+
classified_artefact_info = populate_classified_artefact_info(classified_artefact_info=classified_artefact_info)
|
|
384
396
|
artefact = artefact_class.deserialize(artefact_text)
|
|
385
397
|
artefact, _ = set_closest_contribution(artefact)
|
|
386
398
|
artefact_text = artefact.serialize()
|
|
387
399
|
return artefact_text
|
|
388
400
|
|
|
389
401
|
|
|
390
|
-
def
|
|
402
|
+
def fix_rule(
|
|
391
403
|
file_path: str,
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
classified_artefact_info
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
404
|
+
artefact_text: str,
|
|
405
|
+
artefact_class: str,
|
|
406
|
+
classified_artefact_info: dict,
|
|
407
|
+
**kwargs,
|
|
408
|
+
):
|
|
409
|
+
classified_artefact_info = populate_classified_artefact_info(classified_artefact_info=classified_artefact_info)
|
|
410
|
+
artefact = artefact_class.deserialize(artefact_text)
|
|
411
|
+
contribution = artefact.contribution
|
|
412
|
+
assert contribution is not None
|
|
413
|
+
_update_rule(
|
|
414
|
+
artefact=artefact,
|
|
415
|
+
name=contribution.artefact_name,
|
|
416
|
+
classifier=contribution.classifier,
|
|
417
|
+
classified_file_info=classified_artefact_info,
|
|
418
|
+
delete_if_not_found=True
|
|
419
|
+
)
|
|
420
|
+
feedback_message = (f"Updating contribution of {artefact._artefact_type().value} "
|
|
421
|
+
f"'{artefact.title}' to {contribution.classifier} "
|
|
422
|
+
f"'{contribution.artefact_name}' ")
|
|
423
|
+
rule = contribution.rule
|
|
424
|
+
if rule:
|
|
425
|
+
feedback_message += f"with rule '{rule}'"
|
|
426
|
+
else:
|
|
427
|
+
feedback_message += "without a rule"
|
|
428
|
+
print(feedback_message)
|
|
429
|
+
return artefact.serialize()
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def should_skip_issue(deterministic_issue, deterministic, non_deterministic, file_path) -> bool:
|
|
433
|
+
if not non_deterministic and not deterministic_issue:
|
|
434
|
+
print(f"Skipping non-deterministic fix for {file_path} as per request.")
|
|
435
|
+
return True
|
|
436
|
+
if not deterministic and deterministic_issue:
|
|
437
|
+
print(f"Skipping fix for {file_path} as per request flags.")
|
|
438
|
+
return True
|
|
439
|
+
return False
|
|
407
440
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
def determine_attempt_count() -> int:
|
|
415
|
-
nonlocal single_pass, file_path
|
|
416
|
-
if single_pass:
|
|
417
|
-
print(f"Single-pass mode enabled for {file_path}. Running for 1 attempt.")
|
|
418
|
-
return 1
|
|
419
|
-
return 3
|
|
420
|
-
|
|
421
|
-
def apply_deterministic_fix() -> str:
|
|
422
|
-
nonlocal deterministic, deterministic_issue, corrected_text, file_path, artefact_text, artefact_class, classified_artefact_info
|
|
423
|
-
if deterministic and deterministic_issue:
|
|
424
|
-
print(f"Applying deterministic fix for '{deterministic_issue}'...")
|
|
425
|
-
fix_function = deterministic_markers_to_functions[deterministic_issue]
|
|
426
|
-
return fix_function(
|
|
427
|
-
file_path=file_path,
|
|
428
|
-
artefact_text=artefact_text,
|
|
429
|
-
artefact_class=artefact_class,
|
|
430
|
-
classified_artefact_info=classified_artefact_info,
|
|
431
|
-
)
|
|
432
|
-
return corrected_text
|
|
433
|
-
|
|
434
|
-
def apply_non_deterministic_fix() -> Optional[str]:
|
|
435
|
-
"""
|
|
436
|
-
Applies LLM fix. Return None in case of an exception
|
|
437
|
-
"""
|
|
438
|
-
nonlocal non_deterministic, deterministic_issue, corrected_text, artefact_type, current_reason, file_path, artefact_text
|
|
439
|
-
if non_deterministic and not deterministic_issue:
|
|
440
|
-
print("Applying non-deterministic (LLM) fix...")
|
|
441
|
-
prompt = construct_prompt(artefact_type, current_reason, file_path, artefact_text)
|
|
442
|
-
try:
|
|
443
|
-
corrected_artefact = run_agent(prompt, artefact_class)
|
|
444
|
-
corrected_text = corrected_artefact.serialize()
|
|
445
|
-
except Exception as e:
|
|
446
|
-
print(f" ❌ LLM agent failed to fix artefact at {file_path}: {e}")
|
|
447
|
-
return None
|
|
448
|
-
return corrected_text
|
|
449
|
-
|
|
450
|
-
def should_skip() -> bool:
|
|
451
|
-
nonlocal deterministic_issue, deterministic, non_deterministic
|
|
452
|
-
if not non_deterministic and not deterministic_issue:
|
|
453
|
-
print(f"Skipping non-deterministic fix for {file_path} as per request.")
|
|
454
|
-
return True
|
|
455
|
-
if not deterministic and deterministic_issue:
|
|
456
|
-
print(f"Skipping fix for {file_path} as per request flags.")
|
|
457
|
-
return True
|
|
458
|
-
return False
|
|
441
|
+
def determine_attempt_count(single_pass, file_path) -> int:
|
|
442
|
+
if single_pass:
|
|
443
|
+
print(f"Single-pass mode enabled for {file_path}. Running for 1 attempt.")
|
|
444
|
+
return 1
|
|
445
|
+
return 3
|
|
459
446
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
447
|
+
def apply_deterministic_fix(
|
|
448
|
+
deterministic, deterministic_issue, file_path, artefact_text, artefact_class, classified_artefact_info,
|
|
449
|
+
deterministic_markers_to_functions, corrected_text
|
|
450
|
+
) -> str:
|
|
451
|
+
if deterministic and deterministic_issue:
|
|
452
|
+
print(f"Applying deterministic fix for '{deterministic_issue}'...")
|
|
453
|
+
fix_function = deterministic_markers_to_functions[deterministic_issue]
|
|
454
|
+
return fix_function(
|
|
455
|
+
file_path=file_path,
|
|
456
|
+
artefact_text=artefact_text,
|
|
457
|
+
artefact_class=artefact_class,
|
|
458
|
+
classified_artefact_info=classified_artefact_info,
|
|
459
|
+
)
|
|
460
|
+
return corrected_text
|
|
463
461
|
|
|
464
|
-
|
|
465
|
-
|
|
462
|
+
def apply_non_deterministic_fix(
|
|
463
|
+
non_deterministic, deterministic_issue, corrected_text,
|
|
464
|
+
artefact_type, current_reason, file_path, artefact_text, artefact_class
|
|
465
|
+
) -> Optional[str]:
|
|
466
|
+
"""
|
|
467
|
+
Applies LLM fix. Return None in case of an exception
|
|
468
|
+
"""
|
|
469
|
+
if non_deterministic and not deterministic_issue:
|
|
470
|
+
print("Applying non-deterministic (LLM) fix...")
|
|
471
|
+
prompt = construct_prompt(
|
|
472
|
+
artefact_type, current_reason, file_path, artefact_text
|
|
473
|
+
)
|
|
474
|
+
try:
|
|
475
|
+
corrected_artefact = run_agent(prompt, artefact_class)
|
|
476
|
+
corrected_text = corrected_artefact.serialize()
|
|
477
|
+
except Exception as e:
|
|
478
|
+
print(f" ❌ LLM agent failed to fix artefact at {file_path}: {e}")
|
|
479
|
+
return None
|
|
480
|
+
return corrected_text
|
|
466
481
|
|
|
482
|
+
def attempt_autofix_loop(
|
|
483
|
+
file_path: str,
|
|
484
|
+
artefact_type,
|
|
485
|
+
artefact_class,
|
|
486
|
+
deterministic_markers_to_functions,
|
|
487
|
+
max_attempts,
|
|
488
|
+
deterministic: bool,
|
|
489
|
+
non_deterministic: bool,
|
|
490
|
+
classified_artefact_info: Optional[Dict[str, List[Dict[str, str]]]],
|
|
491
|
+
) -> bool:
|
|
492
|
+
"""
|
|
493
|
+
Attempts to fix the artefact in a loop, up to max_attempts.
|
|
494
|
+
"""
|
|
467
495
|
for attempt in range(max_attempts):
|
|
468
|
-
is_valid, current_reason = check_file(
|
|
496
|
+
is_valid, current_reason = check_file(
|
|
497
|
+
file_path, artefact_class, classified_artefact_info
|
|
498
|
+
)
|
|
469
499
|
|
|
470
500
|
if is_valid:
|
|
471
501
|
print(f"✅ Artefact at {file_path} is now valid.")
|
|
472
502
|
return True
|
|
473
503
|
|
|
474
|
-
print(
|
|
504
|
+
print(
|
|
505
|
+
f"Attempting to fix {file_path} (Attempt {attempt + 1}/{max_attempts})..."
|
|
506
|
+
)
|
|
475
507
|
print(f" Reason: {current_reason}")
|
|
476
508
|
|
|
477
509
|
artefact_text = read_artefact(file_path)
|
|
@@ -487,22 +519,68 @@ def apply_autofix(
|
|
|
487
519
|
None,
|
|
488
520
|
)
|
|
489
521
|
|
|
490
|
-
if
|
|
522
|
+
if should_skip_issue(deterministic_issue, deterministic, non_deterministic, file_path):
|
|
491
523
|
return False
|
|
492
524
|
|
|
493
525
|
corrected_text = None
|
|
494
526
|
|
|
495
|
-
corrected_text = apply_deterministic_fix(
|
|
496
|
-
|
|
527
|
+
corrected_text = apply_deterministic_fix(
|
|
528
|
+
deterministic, deterministic_issue, file_path, artefact_text,
|
|
529
|
+
artefact_class, classified_artefact_info,
|
|
530
|
+
deterministic_markers_to_functions, corrected_text
|
|
531
|
+
)
|
|
532
|
+
corrected_text = apply_non_deterministic_fix(
|
|
533
|
+
non_deterministic, deterministic_issue, corrected_text,
|
|
534
|
+
artefact_type, current_reason, file_path, artefact_text, artefact_class
|
|
535
|
+
)
|
|
497
536
|
|
|
498
537
|
if corrected_text is None or corrected_text.strip() == artefact_text.strip():
|
|
499
|
-
print(
|
|
538
|
+
print(
|
|
539
|
+
" Fixing attempt did not alter the file. Stopping to prevent infinite loop."
|
|
540
|
+
)
|
|
500
541
|
return False
|
|
501
542
|
|
|
502
543
|
write_corrected_artefact(file_path, corrected_text)
|
|
503
544
|
|
|
504
545
|
print(" File modified. Re-classifying artefact information for next check...")
|
|
505
|
-
populate_classified_artefact_info(force=True)
|
|
546
|
+
classified_artefact_info = populate_classified_artefact_info(classified_artefact_info, force=True)
|
|
506
547
|
|
|
507
548
|
print(f"❌ Failed to fix {file_path} after {max_attempts} attempts.")
|
|
508
549
|
return False
|
|
550
|
+
|
|
551
|
+
def apply_autofix(
|
|
552
|
+
file_path: str,
|
|
553
|
+
classifier: str,
|
|
554
|
+
reason: str,
|
|
555
|
+
single_pass: bool = False,
|
|
556
|
+
deterministic: bool = True,
|
|
557
|
+
non_deterministic: bool = True,
|
|
558
|
+
classified_artefact_info: Optional[Dict[str, List[Dict[str, str]]]] = None,
|
|
559
|
+
) -> bool:
|
|
560
|
+
"""
|
|
561
|
+
Applies fixes to a single artefact file iteratively until it is valid
|
|
562
|
+
or a fix cannot be applied. If single_pass is True, it runs for only one attempt.
|
|
563
|
+
"""
|
|
564
|
+
deterministic_markers_to_functions = {
|
|
565
|
+
"Filename-Title Mismatch": fix_title_mismatch,
|
|
566
|
+
"Invalid Contribution Reference": fix_contribution,
|
|
567
|
+
"Rule Mismatch": fix_rule,
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
artefact_type, artefact_class = determine_artefact_type_and_class(classifier)
|
|
571
|
+
if artefact_type is None or artefact_class is None:
|
|
572
|
+
return False
|
|
573
|
+
|
|
574
|
+
classified_artefact_info = populate_classified_artefact_info(classified_artefact_info)
|
|
575
|
+
max_attempts = determine_attempt_count(single_pass, file_path)
|
|
576
|
+
|
|
577
|
+
return attempt_autofix_loop(
|
|
578
|
+
file_path=file_path,
|
|
579
|
+
artefact_type=artefact_type,
|
|
580
|
+
artefact_class=artefact_class,
|
|
581
|
+
deterministic_markers_to_functions=deterministic_markers_to_functions,
|
|
582
|
+
max_attempts=max_attempts,
|
|
583
|
+
deterministic=deterministic,
|
|
584
|
+
non_deterministic=non_deterministic,
|
|
585
|
+
classified_artefact_info=classified_artefact_info,
|
|
586
|
+
)
|
|
@@ -2,9 +2,17 @@ from ara_cli.artefact_models.artefact_mapping import title_prefix_to_artefact_cl
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
def artefact_from_content(content):
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
lines = content.splitlines()
|
|
6
|
+
|
|
7
|
+
# Look through more lines to find the title, skipping empty lines
|
|
8
|
+
for line in lines:
|
|
9
|
+
line = line.strip()
|
|
10
|
+
if not line: # Skip empty lines
|
|
11
|
+
continue
|
|
12
|
+
if line.startswith('@'): # Skip tag lines
|
|
13
|
+
continue
|
|
14
|
+
|
|
7
15
|
for prefix, artefact_class in title_prefix_to_artefact_class.items():
|
|
8
|
-
if line.
|
|
16
|
+
if line.startswith(prefix):
|
|
9
17
|
return artefact_class.deserialize(content)
|
|
10
18
|
return None
|
|
@@ -74,47 +74,55 @@ class TaskArtefact(Artefact):
|
|
|
74
74
|
artefact_type: ArtefactType = ArtefactType.task
|
|
75
75
|
action_items: List[ActionItem] = Field(default_factory=list)
|
|
76
76
|
|
|
77
|
+
@classmethod
|
|
78
|
+
def _is_action_item_start(cls, line: str) -> bool:
|
|
79
|
+
return line.startswith('[@')
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def _is_section_start(cls, line: str, description_marker: str, contribution_marker: str) -> bool:
|
|
83
|
+
return (
|
|
84
|
+
line.startswith(description_marker) or
|
|
85
|
+
line.startswith(contribution_marker)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def _collect_action_item_lines(cls, lines, start_idx, description_marker, contribution_marker):
|
|
90
|
+
action_item_lines = [lines[start_idx]]
|
|
91
|
+
j = start_idx + 1
|
|
92
|
+
while j < len(lines):
|
|
93
|
+
next_line = lines[j]
|
|
94
|
+
if (
|
|
95
|
+
cls._is_action_item_start(next_line) or
|
|
96
|
+
cls._is_section_start(next_line, description_marker, contribution_marker)
|
|
97
|
+
):
|
|
98
|
+
break
|
|
99
|
+
action_item_lines.append(next_line)
|
|
100
|
+
j += 1
|
|
101
|
+
return action_item_lines, j
|
|
102
|
+
|
|
77
103
|
@classmethod
|
|
78
104
|
def _deserialize_action_items(cls, text) -> Tuple[List[ActionItem], List[str]]:
|
|
79
105
|
lines = [line.strip() for line in text.strip().splitlines() if line.strip()]
|
|
80
|
-
|
|
81
106
|
action_items = []
|
|
82
107
|
remaining_lines = []
|
|
83
108
|
i = 0
|
|
84
|
-
|
|
85
109
|
contribution_marker = cls._contribution_starts_with()
|
|
86
110
|
description_marker = cls._description_starts_with()
|
|
87
111
|
|
|
88
112
|
while i < len(lines):
|
|
89
113
|
line = lines[i]
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
j = i + 1
|
|
95
|
-
# Collect lines until we hit another action item or a known section
|
|
96
|
-
while j < len(lines):
|
|
97
|
-
next_line = lines[j]
|
|
98
|
-
# Check if next line is a new action item or a known section
|
|
99
|
-
if (next_line.startswith('[@') or
|
|
100
|
-
next_line.startswith(description_marker) or
|
|
101
|
-
next_line.startswith(contribution_marker)):
|
|
102
|
-
break
|
|
103
|
-
action_item_lines.append(next_line)
|
|
104
|
-
j += 1
|
|
105
|
-
|
|
106
|
-
# Join all lines and pass as a single string to deserialize
|
|
114
|
+
if cls._is_action_item_start(line):
|
|
115
|
+
action_item_lines, next_idx = cls._collect_action_item_lines(
|
|
116
|
+
lines, i, description_marker, contribution_marker
|
|
117
|
+
)
|
|
107
118
|
action_item_text = '\n'.join(action_item_lines)
|
|
108
119
|
try:
|
|
109
120
|
action_item = ActionItem.deserialize(action_item_text)
|
|
110
121
|
if action_item:
|
|
111
122
|
action_items.append(action_item)
|
|
112
123
|
except ValueError as e:
|
|
113
|
-
# Re-raise with more context about where the error occurred
|
|
114
124
|
raise ValueError(f"Error parsing action item: {e}")
|
|
115
|
-
|
|
116
|
-
# Move index to the next unprocessed line
|
|
117
|
-
i = j
|
|
125
|
+
i = next_idx
|
|
118
126
|
else:
|
|
119
127
|
remaining_lines.append(line)
|
|
120
128
|
i += 1
|
ara_cli/artefact_reader.py
CHANGED
|
@@ -94,7 +94,7 @@ class ArtefactReader:
|
|
|
94
94
|
return artefacts
|
|
95
95
|
|
|
96
96
|
@staticmethod
|
|
97
|
-
def find_children(artefact_name, classifier, artefacts_by_classifier=
|
|
97
|
+
def find_children(artefact_name, classifier, artefacts_by_classifier=None, classified_artefacts=None):
|
|
98
98
|
artefacts_by_classifier = artefacts_by_classifier or {}
|
|
99
99
|
filtered_artefacts = {k: [] for k in artefacts_by_classifier.keys()}
|
|
100
100
|
|
|
@@ -103,73 +103,97 @@ class ArtefactReader:
|
|
|
103
103
|
|
|
104
104
|
for artefact_classifier, artefacts in classified_artefacts.items():
|
|
105
105
|
for artefact in artefacts:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
try:
|
|
110
|
-
contribution = artefact.contribution
|
|
111
|
-
if (contribution and
|
|
112
|
-
contribution.artefact_name == artefact_name and
|
|
113
|
-
contribution.classifier == classifier):
|
|
106
|
+
ArtefactReader._process_artefact(
|
|
107
|
+
artefact, artefact_name, classifier, filtered_artefacts
|
|
108
|
+
)
|
|
114
109
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if file_classifier not in filtered_artefacts:
|
|
118
|
-
filtered_artefacts[file_classifier] = []
|
|
119
|
-
filtered_artefacts[file_classifier].append(artefact)
|
|
110
|
+
return ArtefactReader.merge_dicts(artefacts_by_classifier, filtered_artefacts)
|
|
120
111
|
|
|
121
|
-
|
|
122
|
-
|
|
112
|
+
@staticmethod
|
|
113
|
+
def _process_artefact(artefact, artefact_name, classifier, filtered_artefacts):
|
|
114
|
+
if not isinstance(artefact, Artefact):
|
|
115
|
+
return
|
|
116
|
+
contribution = getattr(artefact, 'contribution', None)
|
|
117
|
+
if not contribution:
|
|
118
|
+
return
|
|
119
|
+
if getattr(contribution, 'artefact_name', None) != artefact_name:
|
|
120
|
+
return
|
|
121
|
+
if getattr(contribution, 'classifier', None) != classifier:
|
|
122
|
+
return
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
file_classifier = getattr(artefact, '_file_path', '').split('.')[-1]
|
|
125
|
+
if file_classifier not in filtered_artefacts:
|
|
126
|
+
filtered_artefacts[file_classifier] = []
|
|
127
|
+
filtered_artefacts[file_classifier].append(artefact)
|
|
125
128
|
|
|
126
129
|
@staticmethod
|
|
127
130
|
def step_through_value_chain(
|
|
128
131
|
artefact_name,
|
|
129
132
|
classifier,
|
|
130
|
-
artefacts_by_classifier=
|
|
131
|
-
classified_artefacts: dict[str, list[Artefact]] | None = None
|
|
133
|
+
artefacts_by_classifier=None,
|
|
134
|
+
classified_artefacts: dict[str, list['Artefact']] | None = None
|
|
132
135
|
):
|
|
133
136
|
from ara_cli.artefact_models.artefact_load import artefact_from_content
|
|
134
137
|
|
|
138
|
+
artefacts_by_classifier = artefacts_by_classifier or {}
|
|
139
|
+
|
|
135
140
|
if classified_artefacts is None:
|
|
136
141
|
classified_artefacts = ArtefactReader.read_artefacts()
|
|
137
142
|
|
|
138
|
-
|
|
139
|
-
artefacts_by_classifier[classifier] = []
|
|
143
|
+
ArtefactReader._ensure_classifier_key(classifier, artefacts_by_classifier)
|
|
140
144
|
|
|
141
|
-
artefact =
|
|
142
|
-
|
|
143
|
-
|
|
145
|
+
artefact = ArtefactReader._find_artefact_by_name(
|
|
146
|
+
artefact_name,
|
|
147
|
+
classified_artefacts.get(classifier, [])
|
|
148
|
+
)
|
|
144
149
|
|
|
145
|
-
if not artefact:
|
|
146
|
-
return
|
|
147
|
-
if artefact in artefacts_by_classifier[classifier]:
|
|
150
|
+
if not artefact or artefact in artefacts_by_classifier[classifier]:
|
|
148
151
|
return
|
|
149
152
|
|
|
150
153
|
artefacts_by_classifier[classifier].append(artefact)
|
|
151
154
|
|
|
152
|
-
parent = artefact
|
|
153
|
-
if
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
155
|
+
parent = getattr(artefact, 'contribution', None)
|
|
156
|
+
if not ArtefactReader._has_valid_parent(parent):
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
parent_name = parent.artefact_name
|
|
160
|
+
parent_classifier = parent.classifier
|
|
161
|
+
|
|
162
|
+
parent_classifier_artefacts = classified_artefacts.get(parent_classifier, [])
|
|
163
|
+
all_artefact_names = [x.title for x in parent_classifier_artefacts]
|
|
164
|
+
|
|
165
|
+
if parent_name not in all_artefact_names:
|
|
166
|
+
ArtefactReader._suggest_parent_name_match(
|
|
167
|
+
artefact_name, all_artefact_names, parent_name
|
|
168
|
+
)
|
|
169
|
+
print()
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
ArtefactReader.step_through_value_chain(
|
|
173
|
+
artefact_name=parent_name,
|
|
174
|
+
classifier=parent_classifier,
|
|
175
|
+
artefacts_by_classifier=artefacts_by_classifier,
|
|
176
|
+
classified_artefacts=classified_artefacts
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
@staticmethod
|
|
180
|
+
def _ensure_classifier_key(classifier, artefacts_by_classifier):
|
|
181
|
+
if classifier not in artefacts_by_classifier:
|
|
182
|
+
artefacts_by_classifier[classifier] = []
|
|
183
|
+
|
|
184
|
+
@staticmethod
|
|
185
|
+
def _find_artefact_by_name(artefact_name, artefacts):
|
|
186
|
+
return next((x for x in artefacts if x.title == artefact_name), None)
|
|
187
|
+
|
|
188
|
+
@staticmethod
|
|
189
|
+
def _has_valid_parent(parent):
|
|
190
|
+
return parent and getattr(parent, 'artefact_name', None) and getattr(parent, 'classifier', None)
|
|
191
|
+
|
|
192
|
+
@staticmethod
|
|
193
|
+
def _suggest_parent_name_match(artefact_name, all_artefact_names, parent_name):
|
|
194
|
+
if parent_name is not None:
|
|
195
|
+
suggest_close_name_matches_for_parent(
|
|
196
|
+
artefact_name,
|
|
197
|
+
all_artefact_names,
|
|
198
|
+
parent_name
|
|
175
199
|
)
|