ara-cli 0.1.9.96__py3-none-any.whl → 0.1.10.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ara-cli might be problematic. Click here for more details.

Files changed (48) hide show
  1. ara_cli/__init__.py +1 -1
  2. ara_cli/__main__.py +141 -103
  3. ara_cli/ara_command_action.py +65 -7
  4. ara_cli/ara_config.py +118 -94
  5. ara_cli/ara_subcommands/__init__.py +0 -0
  6. ara_cli/ara_subcommands/autofix.py +26 -0
  7. ara_cli/ara_subcommands/chat.py +27 -0
  8. ara_cli/ara_subcommands/classifier_directory.py +16 -0
  9. ara_cli/ara_subcommands/common.py +100 -0
  10. ara_cli/ara_subcommands/create.py +75 -0
  11. ara_cli/ara_subcommands/delete.py +22 -0
  12. ara_cli/ara_subcommands/extract.py +22 -0
  13. ara_cli/ara_subcommands/fetch_templates.py +14 -0
  14. ara_cli/ara_subcommands/list.py +65 -0
  15. ara_cli/ara_subcommands/list_tags.py +25 -0
  16. ara_cli/ara_subcommands/load.py +48 -0
  17. ara_cli/ara_subcommands/prompt.py +136 -0
  18. ara_cli/ara_subcommands/read.py +47 -0
  19. ara_cli/ara_subcommands/read_status.py +20 -0
  20. ara_cli/ara_subcommands/read_user.py +20 -0
  21. ara_cli/ara_subcommands/reconnect.py +27 -0
  22. ara_cli/ara_subcommands/rename.py +22 -0
  23. ara_cli/ara_subcommands/scan.py +14 -0
  24. ara_cli/ara_subcommands/set_status.py +22 -0
  25. ara_cli/ara_subcommands/set_user.py +22 -0
  26. ara_cli/ara_subcommands/template.py +16 -0
  27. ara_cli/artefact_models/artefact_model.py +88 -19
  28. ara_cli/artefact_models/artefact_templates.py +18 -9
  29. ara_cli/artefact_models/userstory_artefact_model.py +2 -2
  30. ara_cli/artefact_scan.py +2 -2
  31. ara_cli/chat.py +204 -142
  32. ara_cli/commands/read_command.py +17 -4
  33. ara_cli/completers.py +144 -0
  34. ara_cli/prompt_handler.py +268 -127
  35. ara_cli/tag_extractor.py +33 -16
  36. ara_cli/template_loader.py +245 -0
  37. ara_cli/version.py +1 -1
  38. {ara_cli-0.1.9.96.dist-info → ara_cli-0.1.10.1.dist-info}/METADATA +3 -1
  39. {ara_cli-0.1.9.96.dist-info → ara_cli-0.1.10.1.dist-info}/RECORD +47 -23
  40. tests/test_artefact_scan.py +1 -1
  41. tests/test_chat.py +1840 -574
  42. tests/test_prompt_handler.py +40 -4
  43. tests/test_tag_extractor.py +19 -13
  44. tests/test_template_loader.py +192 -0
  45. ara_cli/ara_command_parser.py +0 -565
  46. {ara_cli-0.1.9.96.dist-info → ara_cli-0.1.10.1.dist-info}/WHEEL +0 -0
  47. {ara_cli-0.1.9.96.dist-info → ara_cli-0.1.10.1.dist-info}/entry_points.txt +0 -0
  48. {ara_cli-0.1.9.96.dist-info → ara_cli-0.1.10.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,65 @@
1
+ import typer
2
+ from ara_cli.error_handler import AraError
3
+ from typing import Optional, List, Tuple
4
+ from .common import MockArgs
5
+ from ara_cli.ara_command_action import list_action
6
+
7
+
8
+ def _validate_extension_options(include_extension: Optional[List[str]], exclude_extension: Optional[List[str]]) -> None:
9
+ """Validate that include and exclude extension options are mutually exclusive."""
10
+ if include_extension and exclude_extension:
11
+ raise AraError("--include-extension/-i and --exclude-extension/-e are mutually exclusive")
12
+
13
+
14
+ def _validate_exclusive_options(branch: Optional[Tuple[str, str]],
15
+ children: Optional[Tuple[str, str]],
16
+ data: Optional[Tuple[str, str]]) -> None:
17
+ """Validate that branch, children, and data options are mutually exclusive."""
18
+ exclusive_options = [branch, children, data]
19
+ non_none_options = [opt for opt in exclusive_options if opt is not None]
20
+ if len(non_none_options) > 1:
21
+ raise AraError("--branch, --children, and --data are mutually exclusive")
22
+
23
+
24
+ def list_main(
25
+ ctx: typer.Context,
26
+ include_content: Optional[List[str]] = typer.Option(None, "-I", "--include-content", help="filter for files which include given content"),
27
+ exclude_content: Optional[List[str]] = typer.Option(None, "-E", "--exclude-content", help="filter for files which do not include given content"),
28
+ include_tags: Optional[List[str]] = typer.Option(None, "--include-tags", help="filter for files which include given tags"),
29
+ exclude_tags: Optional[List[str]] = typer.Option(None, "--exclude-tags", help="filter for files which do not include given tags"),
30
+ include_extension: Optional[List[str]] = typer.Option(None, "-i", "--include-extension", "--include-classifier", help="list of extensions to include in listing"),
31
+ exclude_extension: Optional[List[str]] = typer.Option(None, "-e", "--exclude-extension", "--exclude-classifier", help="list of extensions to exclude from listing"),
32
+ branch: Optional[Tuple[str, str]] = typer.Option(None, "-b", "--branch", help="List artefacts in the parent chain (classifier artefact_name)"),
33
+ children: Optional[Tuple[str, str]] = typer.Option(None, "-c", "--children", help="List child artefacts (classifier artefact_name)"),
34
+ data: Optional[Tuple[str, str]] = typer.Option(None, "-d", "--data", help="List file in the data directory (classifier artefact_name)")
35
+ ):
36
+ """List files with optional tags.
37
+
38
+ Examples:
39
+ ara list --data feature my_feature --include-extension .md
40
+ ara list --include-extension .feature
41
+ ara list --children userstory my_story
42
+ ara list --branch userstory my_story --include-extension .businessgoal
43
+ ara list --include-content "example content" --include-extension .task
44
+ """
45
+ _validate_extension_options(include_extension, exclude_extension)
46
+ _validate_exclusive_options(branch, children, data)
47
+
48
+ args = MockArgs(
49
+ include_content=include_content,
50
+ exclude_content=exclude_content,
51
+ include_tags=include_tags,
52
+ exclude_tags=exclude_tags,
53
+ include_extension=include_extension,
54
+ exclude_extension=exclude_extension,
55
+ branch_args=tuple(branch) if branch else (None, None),
56
+ children_args=tuple(children) if children else (None, None),
57
+ data_args=tuple(data) if data else (None, None)
58
+ )
59
+
60
+ list_action(args)
61
+
62
+
63
+ def register(parent: typer.Typer):
64
+ help_text = "List files with optional tags"
65
+ parent.command(name="list", help=help_text)(list_main)
@@ -0,0 +1,25 @@
1
+ import typer
2
+ from typing import Optional
3
+ from .common import ClassifierEnum, MockArgs, ClassifierOption
4
+ from ara_cli.ara_command_action import list_tags_action
5
+
6
+
7
+ def list_tags_main(
8
+ json_output: bool = typer.Option(False, "-j", "--json/--no-json", help="Output tags as JSON"),
9
+ include_classifier: Optional[ClassifierEnum] = ClassifierOption("Show tags for an artefact type", "--include-classifier"),
10
+ exclude_classifier: Optional[ClassifierEnum] = ClassifierOption("Show tags for an artefact type", "--exclude-classifier"),
11
+ filtered_extra_column: bool = typer.Option(False, "--filtered-extra-column", help="Filter tags for extra column")
12
+ ):
13
+ """Show tags."""
14
+ args = MockArgs(
15
+ json=json_output,
16
+ include_classifier=include_classifier.value if include_classifier else None,
17
+ exclude_classifier=exclude_classifier.value if exclude_classifier else None,
18
+ filtered_extra_column=filtered_extra_column
19
+ )
20
+ list_tags_action(args)
21
+
22
+
23
+ def register(parent: typer.Typer):
24
+ help_text = "Show tags"
25
+ parent.command(name="list-tags", help=help_text)(list_tags_main)
@@ -0,0 +1,48 @@
1
+ import typer
2
+ from .common import TemplateTypeEnum, MockArgs, TemplateTypeArgument, ChatNameArgument
3
+ from ara_cli.ara_command_action import load_action
4
+
5
+
6
+ def create_template_name_completer():
7
+ """Create a template name completer that can access the template_type context."""
8
+ def completer(incomplete: str) -> list[str]:
9
+ # This is a simplified version - in practice, you'd need to access
10
+ # the template_type from the current command context
11
+ from ara_cli.template_loader import TemplateLoader
12
+ import os
13
+
14
+ # For all template types since we can't easily get context in typer
15
+ all_templates = []
16
+ for template_type in ['rules', 'intention', 'commands', 'blueprint']:
17
+ try:
18
+ loader = TemplateLoader()
19
+ templates = loader.get_available_templates(template_type, os.getcwd())
20
+ all_templates.extend(templates)
21
+ except Exception:
22
+ continue
23
+
24
+ return [t for t in all_templates if t.startswith(incomplete)]
25
+ return completer
26
+
27
+
28
+ def load_main(
29
+ chat_name: str = ChatNameArgument("Name of the chat file to load template into (without extension)"),
30
+ template_type: TemplateTypeEnum = TemplateTypeArgument("Type of template to load"),
31
+ template_name: str = typer.Argument(
32
+ "",
33
+ help="Name of the template to load. Supports wildcards and 'global/' prefix",
34
+ autocompletion=create_template_name_completer()
35
+ )
36
+ ):
37
+ """Load a template into a chat file."""
38
+ args = MockArgs(
39
+ chat_name=chat_name,
40
+ template_type=template_type.value,
41
+ template_name=template_name
42
+ )
43
+ load_action(args)
44
+
45
+
46
+ def register(parent: typer.Typer):
47
+ help_text = "Load a template into a chat file"
48
+ parent.command(name="load", help=help_text)(load_main)
@@ -0,0 +1,136 @@
1
+ import typer
2
+ from typing import Optional, List
3
+ from .common import ClassifierEnum, MockArgs, ClassifierArgument, ArtefactNameArgument, ChatNameArgument
4
+ from ara_cli.ara_command_action import prompt_action
5
+
6
+
7
+ def prompt_init(
8
+ classifier: ClassifierEnum = ClassifierArgument("Classifier of the artefact"),
9
+ parameter: str = ArtefactNameArgument("Name of artefact data directory")
10
+ ):
11
+ """Initialize a macro prompt."""
12
+ args = MockArgs(
13
+ classifier=classifier.value,
14
+ parameter=parameter,
15
+ steps="init"
16
+ )
17
+ prompt_action(args)
18
+
19
+
20
+ def prompt_load(
21
+ classifier: ClassifierEnum = ClassifierArgument("Classifier of the artefact"),
22
+ parameter: str = ArtefactNameArgument("Name of artefact data directory")
23
+ ):
24
+ """Load selected templates."""
25
+ args = MockArgs(
26
+ classifier=classifier.value,
27
+ parameter=parameter,
28
+ steps="load"
29
+ )
30
+ prompt_action(args)
31
+
32
+
33
+ def prompt_send(
34
+ classifier: ClassifierEnum = ClassifierArgument("Classifier of the artefact"),
35
+ parameter: str = ArtefactNameArgument("Name of artefact data directory")
36
+ ):
37
+ """Send configured prompt to LLM."""
38
+ args = MockArgs(
39
+ classifier=classifier.value,
40
+ parameter=parameter,
41
+ steps="send"
42
+ )
43
+ prompt_action(args)
44
+
45
+
46
+ def prompt_load_and_send(
47
+ classifier: ClassifierEnum = ClassifierArgument("Classifier of the artefact"),
48
+ parameter: str = ArtefactNameArgument("Name of artefact data directory")
49
+ ):
50
+ """Load templates and send prompt to LLM."""
51
+ args = MockArgs(
52
+ classifier=classifier.value,
53
+ parameter=parameter,
54
+ steps="load-and-send"
55
+ )
56
+ prompt_action(args)
57
+
58
+
59
+ def prompt_extract(
60
+ classifier: ClassifierEnum = ClassifierArgument("Classifier of the artefact"),
61
+ parameter: str = ArtefactNameArgument("Name of artefact data directory"),
62
+ write: bool = typer.Option(False, "-w", "--write", help="Overwrite existing files without using LLM for merging")
63
+ ):
64
+ """Extract LLM response and save to disk."""
65
+ args = MockArgs(
66
+ classifier=classifier.value,
67
+ parameter=parameter,
68
+ steps="extract",
69
+ write=write
70
+ )
71
+ prompt_action(args)
72
+
73
+
74
+ def prompt_update(
75
+ classifier: ClassifierEnum = ClassifierArgument("Classifier of the artefact"),
76
+ parameter: str = ArtefactNameArgument("Name of artefact data directory")
77
+ ):
78
+ """Update artefact config prompt files."""
79
+ args = MockArgs(
80
+ classifier=classifier.value,
81
+ parameter=parameter,
82
+ steps="update"
83
+ )
84
+ prompt_action(args)
85
+
86
+
87
+ def prompt_chat(
88
+ classifier: ClassifierEnum = ClassifierArgument("Classifier of the artefact"),
89
+ parameter: str = ArtefactNameArgument("Name of artefact data directory"),
90
+ chat_name: Optional[str] = ChatNameArgument("Optional name for a specific chat", None),
91
+ reset: Optional[bool] = typer.Option(None, "-r", "--reset/--no-reset", help="Reset the chat file if it exists"),
92
+ output_mode: bool = typer.Option(False, "--out", help="Output the contents of the chat file instead of entering interactive chat mode"),
93
+ append: Optional[List[str]] = typer.Option(None, "--append", help="Append strings to the chat file"),
94
+ restricted: Optional[bool] = typer.Option(None, "--restricted/--no-restricted", help="Start with a limited set of commands")
95
+ ):
96
+ """Start chat mode for the artefact."""
97
+ args = MockArgs(
98
+ classifier=classifier.value,
99
+ parameter=parameter,
100
+ steps="chat",
101
+ chat_name=chat_name,
102
+ reset=reset,
103
+ output_mode=output_mode,
104
+ append=append,
105
+ restricted=restricted
106
+ )
107
+ prompt_action(args)
108
+
109
+
110
+ def prompt_init_rag(
111
+ classifier: ClassifierEnum = ClassifierArgument("Classifier of the artefact"),
112
+ parameter: str = ArtefactNameArgument("Name of artefact data directory")
113
+ ):
114
+ """Initialize RAG prompt."""
115
+ args = MockArgs(
116
+ classifier=classifier.value,
117
+ parameter=parameter,
118
+ steps="init-rag"
119
+ )
120
+ prompt_action(args)
121
+
122
+
123
+ def register(parent: typer.Typer):
124
+ prompt_app = typer.Typer(
125
+ help="Base command for prompt interaction mode",
126
+ add_completion=False # Disable completion on subcommand
127
+ )
128
+ prompt_app.command("init")(prompt_init)
129
+ prompt_app.command("load")(prompt_load)
130
+ prompt_app.command("send")(prompt_send)
131
+ prompt_app.command("load-and-send")(prompt_load_and_send)
132
+ prompt_app.command("extract")(prompt_extract)
133
+ prompt_app.command("update")(prompt_update)
134
+ prompt_app.command("chat")(prompt_chat)
135
+ prompt_app.command("init-rag")(prompt_init_rag)
136
+ parent.add_typer(prompt_app, name="prompt")
@@ -0,0 +1,47 @@
1
+ import typer
2
+ from typing import Optional, List
3
+ from .common import ClassifierEnum, MockArgs, ClassifierArgument, ArtefactNameArgument
4
+ from ara_cli.ara_command_action import read_action
5
+
6
+
7
+ def read_main(
8
+ classifier: ClassifierEnum = ClassifierArgument("Classifier of the artefact type", default=None),
9
+ parameter: str = ArtefactNameArgument("Filename of artefact", default=None),
10
+ include_content: Optional[List[str]] = typer.Option(None, "-I", "--include-content", help="filter for files which include given content"),
11
+ exclude_content: Optional[List[str]] = typer.Option(None, "-E", "--exclude-content", help="filter for files which do not include given content"),
12
+ include_tags: Optional[List[str]] = typer.Option(None, "--include-tags", help="filter for files which include given tags"),
13
+ exclude_tags: Optional[List[str]] = typer.Option(None, "--exclude-tags", help="filter for files which do not include given tags"),
14
+ include_extension: Optional[List[str]] = typer.Option(None, "-i", "--include-extension", "--include-classifier", help="list of extensions to include in listing"),
15
+ exclude_extension: Optional[List[str]] = typer.Option(None, "-e", "--exclude-extension", "--exclude-classifier", help="list of extensions to exclude from listing"),
16
+ branch: bool = typer.Option(False, "-b", "--branch", help="Output the contents of artefacts in the parent chain"),
17
+ children: bool = typer.Option(False, "-c", "--children", help="Output the contents of child artefacts")
18
+ ):
19
+ """Reads contents of artefacts."""
20
+ # Handle mutually exclusive options
21
+ if branch and children:
22
+ typer.echo("Error: --branch and --children are mutually exclusive", err=True)
23
+ raise typer.Exit(1)
24
+
25
+ read_mode = None
26
+ if branch:
27
+ read_mode = "branch"
28
+ elif children:
29
+ read_mode = "children"
30
+
31
+ args = MockArgs(
32
+ classifier=classifier.value if classifier else None,
33
+ parameter=parameter,
34
+ include_content=include_content,
35
+ exclude_content=exclude_content,
36
+ include_tags=include_tags,
37
+ exclude_tags=exclude_tags,
38
+ include_extension=include_extension,
39
+ exclude_extension=exclude_extension,
40
+ read_mode=read_mode
41
+ )
42
+ read_action(args)
43
+
44
+
45
+ def register(parent: typer.Typer):
46
+ help_text = "Reads contents of artefacts"
47
+ parent.command(name="read", help=help_text)(read_main)
@@ -0,0 +1,20 @@
1
+ import typer
2
+ from .common import ClassifierEnum, MockArgs, ClassifierArgument, ArtefactNameArgument
3
+ from ara_cli.ara_command_action import read_status_action
4
+
5
+
6
+ def read_status_main(
7
+ classifier: ClassifierEnum = ClassifierArgument("Classifier of the artefact type"),
8
+ parameter: str = ArtefactNameArgument("Filename of artefact")
9
+ ):
10
+ """Read status of an artefact by checking its tags."""
11
+ args = MockArgs(
12
+ classifier=classifier.value,
13
+ parameter=parameter
14
+ )
15
+ read_status_action(args)
16
+
17
+
18
+ def register(parent: typer.Typer):
19
+ help_text = "Read status of an artefact by checking its tags"
20
+ parent.command(name="read-status", help=help_text)(read_status_main)
@@ -0,0 +1,20 @@
1
+ import typer
2
+ from .common import ClassifierEnum, MockArgs, ClassifierArgument, ArtefactNameArgument
3
+ from ara_cli.ara_command_action import read_user_action
4
+
5
+
6
+ def read_user_main(
7
+ classifier: ClassifierEnum = ClassifierArgument("Classifier of the artefact type"),
8
+ parameter: str = ArtefactNameArgument("Filename of artefact")
9
+ ):
10
+ """Read user of an artefact by checking its tags."""
11
+ args = MockArgs(
12
+ classifier=classifier.value,
13
+ parameter=parameter
14
+ )
15
+ read_user_action(args)
16
+
17
+
18
+ def register(parent: typer.Typer):
19
+ help_text = "Read user of an artefact by checking its tags"
20
+ parent.command(name="read-user", help=help_text)(read_user_main)
@@ -0,0 +1,27 @@
1
+ import typer
2
+ from typing import Optional
3
+ from .common import ClassifierEnum, MockArgs, ClassifierArgument, ArtefactNameArgument, ParentNameArgument
4
+ from ara_cli.ara_command_action import reconnect_action
5
+
6
+
7
+ def reconnect_main(
8
+ classifier: ClassifierEnum = ClassifierArgument("Classifier of the artefact type"),
9
+ parameter: str = ArtefactNameArgument("Filename of artefact"),
10
+ parent_classifier: ClassifierEnum = ClassifierArgument("Classifier of the parent artefact type"),
11
+ parent_name: str = ParentNameArgument("Filename of parent artefact"),
12
+ rule: Optional[str] = typer.Option(None, "-r", "--rule", help="Rule for connection")
13
+ ):
14
+ """Connect an artefact to a parent artefact."""
15
+ args = MockArgs(
16
+ classifier=classifier.value,
17
+ parameter=parameter,
18
+ parent_classifier=parent_classifier.value,
19
+ parent_name=parent_name,
20
+ rule=rule
21
+ )
22
+ reconnect_action(args)
23
+
24
+
25
+ def register(parent: typer.Typer):
26
+ help_text = "Connect an artefact to a parent artefact"
27
+ parent.command(name="reconnect", help=help_text)(reconnect_main)
@@ -0,0 +1,22 @@
1
+ import typer
2
+ from .common import ClassifierEnum, MockArgs, ClassifierArgument, ArtefactNameArgument
3
+ from ara_cli.ara_command_action import rename_action
4
+
5
+
6
+ def rename_main(
7
+ classifier: ClassifierEnum = ClassifierArgument("Classifier of the artefact"),
8
+ parameter: str = ArtefactNameArgument("Filename of artefact"),
9
+ aspect: str = typer.Argument(help="New artefact name and new data directory name")
10
+ ):
11
+ """Rename a classified artefact and its data directory."""
12
+ args = MockArgs(
13
+ classifier=classifier.value,
14
+ parameter=parameter,
15
+ aspect=aspect
16
+ )
17
+ rename_action(args)
18
+
19
+
20
+ def register(parent: typer.Typer):
21
+ help_text = "Rename a classified artefact and its data directory"
22
+ parent.command(name="rename", help=help_text)(rename_main)
@@ -0,0 +1,14 @@
1
+ import typer
2
+ from .common import MockArgs
3
+ from ara_cli.ara_command_action import scan_action
4
+
5
+
6
+ def scan_main():
7
+ """Scan ARA tree for incompatible artefacts."""
8
+ args = MockArgs()
9
+ scan_action(args)
10
+
11
+
12
+ def register(parent: typer.Typer):
13
+ help_text = "Scan ARA tree for incompatible artefacts"
14
+ parent.command(name="scan", help=help_text)(scan_main)
@@ -0,0 +1,22 @@
1
+ import typer
2
+ from .common import ClassifierEnum, MockArgs, ClassifierArgument, ArtefactNameArgument, StatusArgument
3
+ from ara_cli.ara_command_action import set_status_action
4
+
5
+
6
+ def set_status_main(
7
+ classifier: ClassifierEnum = ClassifierArgument("Classifier of the artefact type, typically 'task'"),
8
+ parameter: str = ArtefactNameArgument("Name of the task artefact"),
9
+ new_status: str = StatusArgument("New status to set for the task")
10
+ ):
11
+ """Set the status of a task."""
12
+ args = MockArgs(
13
+ classifier=classifier.value,
14
+ parameter=parameter,
15
+ new_status=new_status
16
+ )
17
+ set_status_action(args)
18
+
19
+
20
+ def register(parent: typer.Typer):
21
+ help_text = "Set the status of a task"
22
+ parent.command(name="set-status", help=help_text)(set_status_main)
@@ -0,0 +1,22 @@
1
+ import typer
2
+ from .common import ClassifierEnum, MockArgs, ClassifierArgument, ArtefactNameArgument
3
+ from ara_cli.ara_command_action import set_user_action
4
+
5
+
6
+ def set_user_main(
7
+ classifier: ClassifierEnum = ClassifierArgument("Classifier of the artefact type, typically 'task'"),
8
+ parameter: str = ArtefactNameArgument("Name of the task artefact"),
9
+ new_user: str = typer.Argument(help="New user to assign to the task")
10
+ ):
11
+ """Set the user of a task."""
12
+ args = MockArgs(
13
+ classifier=classifier.value,
14
+ parameter=parameter,
15
+ new_user=new_user
16
+ )
17
+ set_user_action(args)
18
+
19
+
20
+ def register(parent: typer.Typer):
21
+ help_text = "Set the user of a task"
22
+ parent.command(name="set-user", help=help_text)(set_user_main)
@@ -0,0 +1,16 @@
1
+ import typer
2
+ from .common import ClassifierEnum, MockArgs, ClassifierArgument
3
+ from ara_cli.ara_command_action import template_action
4
+
5
+
6
+ def template_main(
7
+ classifier: ClassifierEnum = ClassifierArgument("Classifier of the artefact type")
8
+ ):
9
+ """Outputs a classified ara template in the terminal."""
10
+ args = MockArgs(classifier=classifier.value)
11
+ template_action(args)
12
+
13
+
14
+ def register(parent: typer.Typer):
15
+ help_text = "Outputs a classified ara template in the terminal"
16
+ parent.command(name="template", help=help_text)(template_main)
@@ -184,6 +184,10 @@ class Artefact(BaseModel, ABC):
184
184
  default=[],
185
185
  description="Optional list of tags (0-many)",
186
186
  )
187
+ author: Optional[str] = Field(
188
+ default="creator_unknown",
189
+ description="Author of the artefact, must be a single entry of the form 'creator_<someone>'."
190
+ )
187
191
  title: str = Field(
188
192
  ...,
189
193
  description="Descriptive Artefact title (mandatory)",
@@ -252,6 +256,17 @@ class Artefact(BaseModel, ABC):
252
256
  raise ValueError(f"Tag '{tag}' has the form of a status tag. Set `status` field instead of passing it with other tags")
253
257
  if tag.startswith("user_"):
254
258
  raise ValueError(f"Tag '{tag} has the form of a user tag. Set `users` field instead of passing it with other tags")
259
+ if tag.startswith("creator_"):
260
+ raise ValueError(f"Tag '{tag}' has the form of an author tag. Set `author` field instead of passing it with other tags")
261
+ return v
262
+
263
+ @field_validator('author')
264
+ def validate_author(cls, v):
265
+ if v:
266
+ if not v.startswith("creator_"):
267
+ raise ValueError(f"Author '{v}' must start with 'creator_'.")
268
+ if len(v) <= len("creator_"):
269
+ raise ValueError("Creator name cannot be empty in author tag.")
255
270
  return v
256
271
 
257
272
  @field_validator('title')
@@ -291,32 +306,81 @@ class Artefact(BaseModel, ABC):
291
306
  tag_line = lines[0]
292
307
  if not tag_line.startswith('@'):
293
308
  return {}, lines
309
+
294
310
  tags = tag_line.split()
311
+ tag_dict = cls._process_tags(tags)
312
+ return tag_dict, lines[1:]
313
+
314
+ @classmethod
315
+ def _process_tags(cls, tags) -> Dict[str, str]:
316
+ """Process a list of tags and return a dictionary with categorized tags."""
295
317
  status = None
296
318
  regular_tags = []
297
319
  users = []
298
- status_list = ["@to-do", "@in-progress", "@review", "@done", "@closed"]
299
- user_prefix = "@user_"
300
- user_prefix_length = len(user_prefix)
301
-
320
+ author = None
321
+
302
322
  for tag in tags:
303
- if not tag.startswith('@'):
304
- raise ValueError(f"Tag '{tag}' should start with '@' but started with '{tag[0]}'")
305
- if tag in status_list and status is not None:
306
- raise ValueError(f"Multiple status tags found: '@{status}' and '{tag}'")
307
- if tag in status_list:
308
- status = tag[1:]
309
- continue
310
- if tag.startswith("@user_") and len(tag) > user_prefix_length + 1:
311
- users.append(tag[user_prefix_length:])
312
- continue
313
- regular_tags.append(tag[1:])
314
- tag_dict = {
323
+ cls._validate_tag_format(tag)
324
+
325
+ if cls._is_status_tag(tag):
326
+ status = cls._process_status_tag(tag, status)
327
+ elif cls._is_user_tag(tag):
328
+ users.append(cls._extract_user_from_tag(tag))
329
+ elif cls._is_author_tag(tag):
330
+ author = cls._process_author_tag(tag, author)
331
+ else:
332
+ regular_tags.append(tag[1:])
333
+
334
+ return {
315
335
  "status": status,
316
336
  "users": users,
317
- "tags": regular_tags
337
+ "tags": regular_tags,
338
+ "author": author
318
339
  }
319
- return tag_dict, lines[1:]
340
+
341
+ @classmethod
342
+ def _validate_tag_format(cls, tag):
343
+ """Validate that tag starts with @."""
344
+ if not tag.startswith('@'):
345
+ raise ValueError(f"Tag '{tag}' should start with '@' but started with '{tag[0]}'")
346
+
347
+ @classmethod
348
+ def _is_status_tag(cls, tag) -> bool:
349
+ """Check if tag is a status tag."""
350
+ status_list = ["@to-do", "@in-progress", "@review", "@done", "@closed"]
351
+ return tag in status_list
352
+
353
+ @classmethod
354
+ def _process_status_tag(cls, tag, current_status):
355
+ """Process status tag and check for duplicates."""
356
+ if current_status is not None:
357
+ raise ValueError(f"Multiple status tags found: '@{current_status}' and '{tag}'")
358
+ return tag[1:] # Remove @ prefix
359
+
360
+ @classmethod
361
+ def _is_user_tag(cls, tag) -> bool:
362
+ """Check if tag is a user tag."""
363
+ user_prefix = "@user_"
364
+ return tag.startswith(user_prefix) and len(tag) > len(user_prefix)
365
+
366
+ @classmethod
367
+ def _extract_user_from_tag(cls, tag) -> str:
368
+ """Extract username from user tag."""
369
+ user_prefix = "@user_"
370
+ return tag[len(user_prefix):]
371
+
372
+ @classmethod
373
+ def _is_author_tag(cls, tag) -> bool:
374
+ """Check if tag is an author tag."""
375
+ creator_prefix = "@creator_"
376
+ return tag.startswith(creator_prefix) and len(tag) > len(creator_prefix)
377
+
378
+ @classmethod
379
+ def _process_author_tag(cls, tag, current_author):
380
+ """Process author tag and check for duplicates."""
381
+ if current_author is not None:
382
+ raise ValueError(f"Multiple author tags found: '@{current_author}' and '@{tag[1:]}'")
383
+ return tag[1:]
320
384
 
321
385
  @classmethod
322
386
  def _deserialize_title(cls, lines) -> (str, List[str]):
@@ -367,7 +431,7 @@ class Artefact(BaseModel, ABC):
367
431
  contribution, remaining_lines = cls._deserialize_contribution(remaining_lines)
368
432
  description, remaining_lines = cls._deserialize_description(remaining_lines)
369
433
 
370
- return {
434
+ fields = {
371
435
  'artefact_type': cls._artefact_type(),
372
436
  'tags': tags.get('tags', []),
373
437
  'users': tags.get('users', []),
@@ -376,6 +440,9 @@ class Artefact(BaseModel, ABC):
376
440
  'contribution': contribution,
377
441
  'description': description,
378
442
  }
443
+ if tags.get("author"):
444
+ fields["author"] = tags.get("author")
445
+ return fields
379
446
 
380
447
  @classmethod
381
448
  def deserialize(cls, text: str) -> 'Artefact':
@@ -407,6 +474,8 @@ class Artefact(BaseModel, ABC):
407
474
  tags.append(f"@{self.status}")
408
475
  for user in self.users:
409
476
  tags.append(f"@user_{user}")
477
+ if self.author:
478
+ tags.append(f"@{self.author}")
410
479
  for tag in self.tags:
411
480
  tags.append(f"@{tag}")
412
481
  return ' '.join(tags)