ara-cli 0.1.9.69__py3-none-any.whl → 0.1.10.8__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/__init__.py +18 -2
- ara_cli/__main__.py +248 -62
- ara_cli/ara_command_action.py +155 -86
- ara_cli/ara_config.py +226 -80
- ara_cli/ara_subcommands/__init__.py +0 -0
- ara_cli/ara_subcommands/autofix.py +26 -0
- ara_cli/ara_subcommands/chat.py +27 -0
- ara_cli/ara_subcommands/classifier_directory.py +16 -0
- ara_cli/ara_subcommands/common.py +100 -0
- ara_cli/ara_subcommands/create.py +75 -0
- ara_cli/ara_subcommands/delete.py +22 -0
- ara_cli/ara_subcommands/extract.py +22 -0
- ara_cli/ara_subcommands/fetch_templates.py +14 -0
- ara_cli/ara_subcommands/list.py +65 -0
- ara_cli/ara_subcommands/list_tags.py +25 -0
- ara_cli/ara_subcommands/load.py +48 -0
- ara_cli/ara_subcommands/prompt.py +136 -0
- ara_cli/ara_subcommands/read.py +47 -0
- ara_cli/ara_subcommands/read_status.py +20 -0
- ara_cli/ara_subcommands/read_user.py +20 -0
- ara_cli/ara_subcommands/reconnect.py +27 -0
- ara_cli/ara_subcommands/rename.py +22 -0
- ara_cli/ara_subcommands/scan.py +14 -0
- ara_cli/ara_subcommands/set_status.py +22 -0
- ara_cli/ara_subcommands/set_user.py +22 -0
- ara_cli/ara_subcommands/template.py +16 -0
- ara_cli/artefact_autofix.py +649 -68
- ara_cli/artefact_creator.py +8 -11
- ara_cli/artefact_deleter.py +2 -4
- ara_cli/artefact_fuzzy_search.py +22 -10
- ara_cli/artefact_link_updater.py +4 -4
- ara_cli/artefact_lister.py +29 -55
- ara_cli/artefact_models/artefact_data_retrieval.py +23 -0
- ara_cli/artefact_models/artefact_load.py +11 -3
- ara_cli/artefact_models/artefact_model.py +146 -39
- ara_cli/artefact_models/artefact_templates.py +70 -44
- ara_cli/artefact_models/businessgoal_artefact_model.py +23 -25
- ara_cli/artefact_models/epic_artefact_model.py +34 -26
- ara_cli/artefact_models/feature_artefact_model.py +203 -64
- ara_cli/artefact_models/keyfeature_artefact_model.py +21 -24
- ara_cli/artefact_models/serialize_helper.py +1 -1
- ara_cli/artefact_models/task_artefact_model.py +83 -15
- ara_cli/artefact_models/userstory_artefact_model.py +37 -27
- ara_cli/artefact_models/vision_artefact_model.py +23 -42
- ara_cli/artefact_reader.py +92 -91
- ara_cli/artefact_renamer.py +8 -4
- ara_cli/artefact_scan.py +66 -3
- ara_cli/chat.py +622 -162
- ara_cli/chat_agent/__init__.py +0 -0
- ara_cli/chat_agent/agent_communicator.py +62 -0
- ara_cli/chat_agent/agent_process_manager.py +211 -0
- ara_cli/chat_agent/agent_status_manager.py +73 -0
- ara_cli/chat_agent/agent_workspace_manager.py +76 -0
- ara_cli/commands/__init__.py +0 -0
- ara_cli/commands/command.py +7 -0
- ara_cli/commands/extract_command.py +15 -0
- ara_cli/commands/load_command.py +65 -0
- ara_cli/commands/load_image_command.py +34 -0
- ara_cli/commands/read_command.py +117 -0
- ara_cli/completers.py +144 -0
- ara_cli/directory_navigator.py +37 -4
- ara_cli/error_handler.py +134 -0
- ara_cli/file_classifier.py +6 -5
- ara_cli/file_lister.py +1 -1
- ara_cli/file_loaders/__init__.py +0 -0
- ara_cli/file_loaders/binary_file_loader.py +33 -0
- ara_cli/file_loaders/document_file_loader.py +34 -0
- ara_cli/file_loaders/document_reader.py +245 -0
- ara_cli/file_loaders/document_readers.py +233 -0
- ara_cli/file_loaders/file_loader.py +50 -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 +187 -0
- ara_cli/global_file_lister.py +51 -0
- ara_cli/list_filter.py +1 -1
- ara_cli/output_suppressor.py +1 -1
- ara_cli/prompt_extractor.py +215 -88
- ara_cli/prompt_handler.py +521 -134
- ara_cli/prompt_rag.py +2 -2
- ara_cli/tag_extractor.py +83 -38
- ara_cli/template_loader.py +245 -0
- ara_cli/template_manager.py +18 -13
- ara_cli/templates/prompt-modules/commands/empty.commands.md +2 -12
- ara_cli/templates/prompt-modules/commands/extract_general.commands.md +12 -0
- ara_cli/templates/prompt-modules/commands/extract_markdown.commands.md +11 -0
- ara_cli/templates/prompt-modules/commands/extract_python.commands.md +13 -0
- ara_cli/templates/prompt-modules/commands/feature_add_or_modifiy_specified_behavior.commands.md +36 -0
- ara_cli/templates/prompt-modules/commands/feature_generate_initial_specified_bevahior.commands.md +53 -0
- ara_cli/templates/prompt-modules/commands/prompt_template_tech_stack_transformer.commands.md +95 -0
- ara_cli/templates/prompt-modules/commands/python_bug_fixing_code.commands.md +34 -0
- ara_cli/templates/prompt-modules/commands/python_generate_code.commands.md +27 -0
- ara_cli/templates/prompt-modules/commands/python_refactoring_code.commands.md +39 -0
- ara_cli/templates/prompt-modules/commands/python_step_definitions_generation_and_fixing.commands.md +40 -0
- ara_cli/templates/prompt-modules/commands/python_unittest_generation_and_fixing.commands.md +48 -0
- ara_cli/update_config_prompt.py +9 -3
- ara_cli/version.py +1 -1
- ara_cli-0.1.10.8.dist-info/METADATA +241 -0
- ara_cli-0.1.10.8.dist-info/RECORD +193 -0
- tests/test_ara_command_action.py +73 -59
- tests/test_ara_config.py +341 -36
- tests/test_artefact_autofix.py +1060 -0
- tests/test_artefact_link_updater.py +3 -3
- tests/test_artefact_lister.py +52 -132
- tests/test_artefact_renamer.py +2 -2
- tests/test_artefact_scan.py +327 -33
- tests/test_chat.py +2063 -498
- tests/test_file_classifier.py +24 -1
- tests/test_file_creator.py +3 -5
- tests/test_file_lister.py +1 -1
- tests/test_global_file_lister.py +131 -0
- tests/test_list_filter.py +2 -2
- tests/test_prompt_handler.py +746 -0
- tests/test_tag_extractor.py +19 -13
- tests/test_template_loader.py +192 -0
- tests/test_template_manager.py +5 -4
- tests/test_update_config_prompt.py +2 -2
- ara_cli/ara_command_parser.py +0 -327
- ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +0 -27
- ara_cli/templates/prompt-modules/blueprints/task_todo_list_implement_feature_BDD_way.blueprint.md +0 -30
- ara_cli/templates/prompt-modules/commands/artefact_classification.commands.md +0 -9
- ara_cli/templates/prompt-modules/commands/artefact_extension.commands.md +0 -17
- ara_cli/templates/prompt-modules/commands/artefact_formulation.commands.md +0 -14
- ara_cli/templates/prompt-modules/commands/behave_step_generation.commands.md +0 -102
- ara_cli/templates/prompt-modules/commands/code_generation_complex.commands.md +0 -20
- ara_cli/templates/prompt-modules/commands/code_generation_simple.commands.md +0 -13
- ara_cli/templates/prompt-modules/commands/error_fixing.commands.md +0 -20
- ara_cli/templates/prompt-modules/commands/feature_file_update.commands.md +0 -18
- ara_cli/templates/prompt-modules/commands/feature_formulation.commands.md +0 -43
- ara_cli/templates/prompt-modules/commands/js_code_generation_simple.commands.md +0 -13
- ara_cli/templates/prompt-modules/commands/refactoring.commands.md +0 -15
- ara_cli/templates/prompt-modules/commands/refactoring_analysis.commands.md +0 -9
- ara_cli/templates/prompt-modules/commands/reverse_engineer_feature_file.commands.md +0 -15
- ara_cli/templates/prompt-modules/commands/reverse_engineer_program_flow.commands.md +0 -19
- ara_cli/templates/template.businessgoal +0 -10
- ara_cli/templates/template.capability +0 -10
- ara_cli/templates/template.epic +0 -15
- ara_cli/templates/template.example +0 -6
- ara_cli/templates/template.feature +0 -26
- ara_cli/templates/template.issue +0 -14
- ara_cli/templates/template.keyfeature +0 -15
- ara_cli/templates/template.task +0 -6
- ara_cli/templates/template.userstory +0 -17
- ara_cli/templates/template.vision +0 -14
- ara_cli-0.1.9.69.dist-info/METADATA +0 -16
- ara_cli-0.1.9.69.dist-info/RECORD +0 -158
- tests/test_ara_autofix.py +0 -219
- {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/WHEEL +0 -0
- {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/entry_points.txt +0 -0
- {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/top_level.txt +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
2
|
-
from typing import Optional, List, Literal, Union, Dict
|
|
2
|
+
from typing import Optional, List, Literal, Union, Dict, ClassVar
|
|
3
3
|
from typing_extensions import Self
|
|
4
4
|
from enum import Enum
|
|
5
5
|
from abc import ABC, abstractmethod
|
|
@@ -47,13 +47,23 @@ class Contribution(BaseModel):
|
|
|
47
47
|
description="Rule the contribution is using. The classifier of the parent must be userstory or epic if this is used"
|
|
48
48
|
)
|
|
49
49
|
|
|
50
|
+
PLACEHOLDER_NAME: ClassVar[str] = "<filename or title of the artefact>"
|
|
51
|
+
PLACEHOLDER_CLASSIFIER: ClassVar[str] = "<agile requirement artefact category> <(optional in case the contribution is to an artefact that is detailed with rules)"
|
|
52
|
+
PLACEHOLDER_RULE: ClassVar[str] = "<rule as it is formulated>"
|
|
53
|
+
|
|
50
54
|
@model_validator(mode="after")
|
|
51
55
|
def validate_parent(self) -> Self:
|
|
52
|
-
|
|
53
56
|
artefact_name = self.artefact_name
|
|
54
57
|
classifier = self.classifier
|
|
55
58
|
rule = self.rule
|
|
56
59
|
|
|
60
|
+
if (
|
|
61
|
+
artefact_name == Contribution.PLACEHOLDER_NAME
|
|
62
|
+
or classifier == Contribution.PLACEHOLDER_CLASSIFIER
|
|
63
|
+
or rule == Contribution.PLACEHOLDER_RULE
|
|
64
|
+
):
|
|
65
|
+
return self
|
|
66
|
+
|
|
57
67
|
if artefact_name:
|
|
58
68
|
artefact_name = replace_space_with_underscore(artefact_name)
|
|
59
69
|
if not artefact_name or not classifier:
|
|
@@ -68,7 +78,7 @@ class Contribution(BaseModel):
|
|
|
68
78
|
|
|
69
79
|
@field_validator('artefact_name')
|
|
70
80
|
def validate_artefact_name(cls, value):
|
|
71
|
-
if not value:
|
|
81
|
+
if not value or value == Contribution.PLACEHOLDER_NAME:
|
|
72
82
|
return value
|
|
73
83
|
if ' ' in value:
|
|
74
84
|
warnings.warn(message="artefact_name can not contain spaces. Replacing spaces with '_'")
|
|
@@ -77,7 +87,7 @@ class Contribution(BaseModel):
|
|
|
77
87
|
|
|
78
88
|
@field_validator('classifier', mode='after')
|
|
79
89
|
def validate_classifier(cls, v):
|
|
80
|
-
if not v:
|
|
90
|
+
if not v or v == Contribution.PLACEHOLDER_CLASSIFIER:
|
|
81
91
|
return v
|
|
82
92
|
try:
|
|
83
93
|
return ArtefactType(v)
|
|
@@ -91,12 +101,21 @@ class Contribution(BaseModel):
|
|
|
91
101
|
if not line.startswith(contribution_line_start):
|
|
92
102
|
raise ValueError(f"Contribution line '{line}' does not start with '{contribution_line_start}'")
|
|
93
103
|
|
|
104
|
+
parent_text = line[len(contribution_line_start):].strip()
|
|
105
|
+
rule_specifier = " using rule "
|
|
106
|
+
|
|
107
|
+
placeholder_line = f"{cls.PLACEHOLDER_NAME} {cls.PLACEHOLDER_CLASSIFIER}{rule_specifier}{cls.PLACEHOLDER_RULE}"
|
|
108
|
+
if parent_text == placeholder_line:
|
|
109
|
+
return cls(
|
|
110
|
+
artefact_name=cls.PLACEHOLDER_NAME,
|
|
111
|
+
classifier=cls.PLACEHOLDER_CLASSIFIER,
|
|
112
|
+
rule=cls.PLACEHOLDER_RULE
|
|
113
|
+
)
|
|
114
|
+
|
|
94
115
|
artefact_name = None
|
|
95
116
|
classifier = None
|
|
96
117
|
rule = None
|
|
97
118
|
|
|
98
|
-
parent_text = line[len(contribution_line_start):].strip()
|
|
99
|
-
rule_specifier = " using rule "
|
|
100
119
|
if rule_specifier in parent_text:
|
|
101
120
|
parent_text, rule_text = parent_text.split(rule_specifier, 1)
|
|
102
121
|
rule = rule_text
|
|
@@ -113,7 +132,7 @@ class Contribution(BaseModel):
|
|
|
113
132
|
def serialize(self) -> str:
|
|
114
133
|
if not self.classifier or not self.artefact_name:
|
|
115
134
|
return ""
|
|
116
|
-
artefact_type = Classifier.get_artefact_title(self.classifier)
|
|
135
|
+
artefact_type = Classifier.get_artefact_title(self.classifier) or self.classifier
|
|
117
136
|
artefact_name = replace_underscore_with_space(self.artefact_name)
|
|
118
137
|
contribution = f"{artefact_name} {artefact_type}"
|
|
119
138
|
if self.rule:
|
|
@@ -165,6 +184,10 @@ class Artefact(BaseModel, ABC):
|
|
|
165
184
|
default=[],
|
|
166
185
|
description="Optional list of tags (0-many)",
|
|
167
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
|
+
)
|
|
168
191
|
title: str = Field(
|
|
169
192
|
...,
|
|
170
193
|
description="Descriptive Artefact title (mandatory)",
|
|
@@ -197,6 +220,15 @@ class Artefact(BaseModel, ABC):
|
|
|
197
220
|
self._file_path = file_path
|
|
198
221
|
return self
|
|
199
222
|
|
|
223
|
+
@model_validator(mode="after")
|
|
224
|
+
def validate_contribution(self):
|
|
225
|
+
contribution = self.contribution
|
|
226
|
+
classifier = self.artefact_type.value
|
|
227
|
+
name = self.title
|
|
228
|
+
if not contribution:
|
|
229
|
+
warnings.warn(f"Contribution of {classifier} '{name}' is not set and will be empty")
|
|
230
|
+
return self
|
|
231
|
+
|
|
200
232
|
@field_validator('artefact_type')
|
|
201
233
|
def validate_artefact_type(cls, v):
|
|
202
234
|
if not isinstance(v, ArtefactType):
|
|
@@ -224,13 +256,28 @@ class Artefact(BaseModel, ABC):
|
|
|
224
256
|
raise ValueError(f"Tag '{tag}' has the form of a status tag. Set `status` field instead of passing it with other tags")
|
|
225
257
|
if tag.startswith("user_"):
|
|
226
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.")
|
|
227
270
|
return v
|
|
228
271
|
|
|
229
272
|
@field_validator('title')
|
|
230
273
|
def validate_title(cls, v):
|
|
231
274
|
if not v.strip():
|
|
232
275
|
raise ValueError("artefact_title must not be empty")
|
|
233
|
-
v = replace_space_with_underscore(v)
|
|
276
|
+
v = replace_space_with_underscore(v).strip()
|
|
277
|
+
|
|
278
|
+
whitelisted_placeholder = "<descriptive_title>"
|
|
279
|
+
if v == whitelisted_placeholder:
|
|
280
|
+
return v
|
|
234
281
|
|
|
235
282
|
letters = list(string.ascii_letters)
|
|
236
283
|
digits = list(string.digits)
|
|
@@ -243,12 +290,6 @@ class Artefact(BaseModel, ABC):
|
|
|
243
290
|
|
|
244
291
|
return v
|
|
245
292
|
|
|
246
|
-
@field_validator('contribution')
|
|
247
|
-
def validate_contribution(cls, v):
|
|
248
|
-
if not v:
|
|
249
|
-
warnings.warn("Contribution is not set and will be empty")
|
|
250
|
-
return v
|
|
251
|
-
|
|
252
293
|
@classmethod
|
|
253
294
|
@abstractmethod
|
|
254
295
|
def _title_prefix(cls) -> str: # pragma: no cover
|
|
@@ -265,32 +306,81 @@ class Artefact(BaseModel, ABC):
|
|
|
265
306
|
tag_line = lines[0]
|
|
266
307
|
if not tag_line.startswith('@'):
|
|
267
308
|
return {}, lines
|
|
309
|
+
|
|
268
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."""
|
|
269
317
|
status = None
|
|
270
318
|
regular_tags = []
|
|
271
319
|
users = []
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
user_prefix_length = len(user_prefix)
|
|
275
|
-
|
|
320
|
+
author = None
|
|
321
|
+
|
|
276
322
|
for tag in tags:
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if tag
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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 {
|
|
289
335
|
"status": status,
|
|
290
336
|
"users": users,
|
|
291
|
-
"tags": regular_tags
|
|
337
|
+
"tags": regular_tags,
|
|
338
|
+
"author": author
|
|
292
339
|
}
|
|
293
|
-
|
|
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:]
|
|
294
384
|
|
|
295
385
|
@classmethod
|
|
296
386
|
def _deserialize_title(cls, lines) -> (str, List[str]):
|
|
@@ -320,14 +410,26 @@ class Artefact(BaseModel, ABC):
|
|
|
320
410
|
return contribution, lines
|
|
321
411
|
|
|
322
412
|
@classmethod
|
|
323
|
-
def _deserialize_description(cls, lines) -> (Optional[str], List[str]):
|
|
413
|
+
def _deserialize_description(cls, lines: List[str]) -> (Optional[str], List[str]):
|
|
324
414
|
description_start = cls._description_starts_with()
|
|
415
|
+
start_index = -1
|
|
325
416
|
for i, line in enumerate(lines):
|
|
326
417
|
if line.startswith(description_start):
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
418
|
+
start_index = i
|
|
419
|
+
break
|
|
420
|
+
|
|
421
|
+
if start_index == -1:
|
|
422
|
+
return None, lines
|
|
423
|
+
|
|
424
|
+
first_line_content = lines[start_index][len(description_start):].strip()
|
|
425
|
+
|
|
426
|
+
description_lines = ([first_line_content] if first_line_content else []) + lines[start_index + 1:]
|
|
427
|
+
|
|
428
|
+
description = "\n".join(description_lines)
|
|
429
|
+
|
|
430
|
+
remaining_lines = lines[:start_index]
|
|
431
|
+
|
|
432
|
+
return (description if description else None), remaining_lines
|
|
331
433
|
|
|
332
434
|
@classmethod
|
|
333
435
|
def _parse_common_fields(cls, text: str) -> dict:
|
|
@@ -341,7 +443,7 @@ class Artefact(BaseModel, ABC):
|
|
|
341
443
|
contribution, remaining_lines = cls._deserialize_contribution(remaining_lines)
|
|
342
444
|
description, remaining_lines = cls._deserialize_description(remaining_lines)
|
|
343
445
|
|
|
344
|
-
|
|
446
|
+
fields = {
|
|
345
447
|
'artefact_type': cls._artefact_type(),
|
|
346
448
|
'tags': tags.get('tags', []),
|
|
347
449
|
'users': tags.get('users', []),
|
|
@@ -350,6 +452,9 @@ class Artefact(BaseModel, ABC):
|
|
|
350
452
|
'contribution': contribution,
|
|
351
453
|
'description': description,
|
|
352
454
|
}
|
|
455
|
+
if tags.get("author"):
|
|
456
|
+
fields["author"] = tags.get("author")
|
|
457
|
+
return fields
|
|
353
458
|
|
|
354
459
|
@classmethod
|
|
355
460
|
def deserialize(cls, text: str) -> 'Artefact':
|
|
@@ -381,6 +486,8 @@ class Artefact(BaseModel, ABC):
|
|
|
381
486
|
tags.append(f"@{self.status}")
|
|
382
487
|
for user in self.users:
|
|
383
488
|
tags.append(f"@user_{user}")
|
|
489
|
+
if self.author:
|
|
490
|
+
tags.append(f"@{self.author}")
|
|
384
491
|
for tag in self.tags:
|
|
385
492
|
tags.append(f"@{tag}")
|
|
386
493
|
return ' '.join(tags)
|
|
@@ -404,4 +511,4 @@ class Artefact(BaseModel, ABC):
|
|
|
404
511
|
classifier=classifier,
|
|
405
512
|
rule=rule
|
|
406
513
|
)
|
|
407
|
-
self.contribution = contribution
|
|
514
|
+
self.contribution = contribution
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from ara_cli.artefact_models.artefact_model import ArtefactType, Artefact
|
|
1
|
+
from ara_cli.artefact_models.artefact_model import ArtefactType, Artefact, Contribution
|
|
2
2
|
from ara_cli.artefact_models.vision_artefact_model import VisionArtefact, VisionIntent
|
|
3
3
|
from ara_cli.artefact_models.businessgoal_artefact_model import BusinessgoalArtefact, BusinessgoalIntent
|
|
4
4
|
from ara_cli.artefact_models.capability_artefact_model import CapabilityArtefact, CapabilityIntent
|
|
@@ -11,7 +11,15 @@ from ara_cli.artefact_models.task_artefact_model import TaskArtefact
|
|
|
11
11
|
from ara_cli.artefact_models.issue_artefact_model import IssueArtefact
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
def
|
|
14
|
+
def default_contribution() -> Contribution:
|
|
15
|
+
return Contribution(
|
|
16
|
+
artefact_name=Contribution.PLACEHOLDER_NAME,
|
|
17
|
+
classifier=Contribution.PLACEHOLDER_CLASSIFIER,
|
|
18
|
+
rule=Contribution.PLACEHOLDER_RULE
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _default_vision(title: str, use_default_contribution: bool) -> VisionArtefact:
|
|
15
23
|
intent = VisionIntent(
|
|
16
24
|
for_="<target customer>",
|
|
17
25
|
who="<needs something>",
|
|
@@ -21,42 +29,46 @@ def _default_vision(title: str) -> VisionArtefact:
|
|
|
21
29
|
our_product="<statement of primary differentiation>"
|
|
22
30
|
)
|
|
23
31
|
return VisionArtefact(
|
|
24
|
-
tags=[
|
|
32
|
+
tags=[],
|
|
33
|
+
author="creator_unknown",
|
|
25
34
|
title=title,
|
|
26
35
|
description="<further optional description to understand the vision, markdown capable text formatting>",
|
|
27
|
-
intent=intent
|
|
36
|
+
intent=intent,
|
|
37
|
+
contribution=default_contribution() if use_default_contribution else None
|
|
28
38
|
)
|
|
29
39
|
|
|
30
40
|
|
|
31
|
-
def _default_businessgoal(title: str) -> BusinessgoalArtefact:
|
|
41
|
+
def _default_businessgoal(title: str, use_default_contribution: bool) -> BusinessgoalArtefact:
|
|
32
42
|
intent = BusinessgoalIntent(
|
|
33
43
|
in_order_to="<reach primarily a monetary business goal>",
|
|
34
44
|
as_a="<business related role>",
|
|
35
45
|
i_want="<something that helps me to reach my monetary goal>"
|
|
36
46
|
)
|
|
37
47
|
return BusinessgoalArtefact(
|
|
38
|
-
tags=[
|
|
48
|
+
tags=[],
|
|
49
|
+
author="creator_unknown",
|
|
39
50
|
title=title,
|
|
40
|
-
description="<further optional description to understand the
|
|
41
|
-
intent=intent
|
|
51
|
+
description="<further optional description to understand the businessgoal, markdown capable text formatting>",
|
|
52
|
+
intent=intent,
|
|
53
|
+
contribution=default_contribution() if use_default_contribution else None
|
|
42
54
|
)
|
|
43
55
|
|
|
44
56
|
|
|
45
|
-
def _default_capability(title: str) -> CapabilityArtefact:
|
|
57
|
+
def _default_capability(title: str, use_default_contribution: bool) -> CapabilityArtefact:
|
|
46
58
|
intent = CapabilityIntent(
|
|
47
|
-
|
|
48
|
-
as_a="<business related role>",
|
|
49
|
-
to_be_able_to="<something that helps me to reach my monetary goal>"
|
|
59
|
+
to_be_able_to="<needed capability for stakeholders that are the enablers/relevant for reaching the business goal>"
|
|
50
60
|
)
|
|
51
61
|
return CapabilityArtefact(
|
|
52
|
-
tags=[
|
|
62
|
+
tags=[],
|
|
63
|
+
author="creator_unknown",
|
|
53
64
|
title=title,
|
|
54
65
|
description="<further optional description to understand the capability, markdown capable text formatting>",
|
|
55
|
-
intent=intent
|
|
66
|
+
intent=intent,
|
|
67
|
+
contribution=default_contribution() if use_default_contribution else None
|
|
56
68
|
)
|
|
57
69
|
|
|
58
70
|
|
|
59
|
-
def _default_epic(title: str) -> EpicArtefact:
|
|
71
|
+
def _default_epic(title: str, use_default_contribution: bool) -> EpicArtefact:
|
|
60
72
|
intent = EpicIntent(
|
|
61
73
|
in_order_to="<achieve a benefit>",
|
|
62
74
|
as_a="<(user) role>",
|
|
@@ -68,15 +80,17 @@ def _default_epic(title: str) -> EpicArtefact:
|
|
|
68
80
|
"<rule needed to fulfill the wanted product behavior>"
|
|
69
81
|
]
|
|
70
82
|
return EpicArtefact(
|
|
71
|
-
tags=[
|
|
83
|
+
tags=[],
|
|
84
|
+
author="creator_unknown",
|
|
72
85
|
title=title,
|
|
73
|
-
description="<further optional description to understand the
|
|
86
|
+
description="<further optional description to understand the epic, markdown capable text formatting>",
|
|
74
87
|
intent=intent,
|
|
75
|
-
rules=rules
|
|
88
|
+
rules=rules,
|
|
89
|
+
contribution=default_contribution() if use_default_contribution else None
|
|
76
90
|
)
|
|
77
91
|
|
|
78
92
|
|
|
79
|
-
def _default_userstory(title: str) -> UserstoryArtefact:
|
|
93
|
+
def _default_userstory(title: str, use_default_contribution: bool) -> UserstoryArtefact:
|
|
80
94
|
intent = UserstoryIntent(
|
|
81
95
|
in_order_to="<achieve a benefit>",
|
|
82
96
|
as_a="<(user) role>",
|
|
@@ -88,44 +102,50 @@ def _default_userstory(title: str) -> UserstoryArtefact:
|
|
|
88
102
|
"<rule needed to fulfill the wanted product behavior>"
|
|
89
103
|
]
|
|
90
104
|
return UserstoryArtefact(
|
|
91
|
-
tags=[
|
|
105
|
+
tags=[],
|
|
106
|
+
author="creator_unknown",
|
|
92
107
|
title=title,
|
|
93
108
|
description="<further optional description to understand the userstory, markdown capable text formatting>",
|
|
94
109
|
intent=intent,
|
|
95
110
|
rules=rules,
|
|
96
|
-
estimate="<story points, scale?>"
|
|
111
|
+
estimate="<story points, scale?>",
|
|
112
|
+
contribution=default_contribution() if use_default_contribution else None
|
|
97
113
|
)
|
|
98
114
|
|
|
99
115
|
|
|
100
|
-
def _default_example(title: str) -> ExampleArtefact:
|
|
116
|
+
def _default_example(title: str, use_default_contribution: bool) -> ExampleArtefact:
|
|
101
117
|
return ExampleArtefact(
|
|
102
|
-
tags=[
|
|
118
|
+
tags=[],
|
|
119
|
+
author="creator_unknown",
|
|
103
120
|
title=title,
|
|
104
121
|
description="<further optional description to understand the example, markdown capable text formatting>",
|
|
122
|
+
contribution=default_contribution() if use_default_contribution else None
|
|
105
123
|
)
|
|
106
124
|
|
|
107
125
|
|
|
108
|
-
def _default_keyfeature(title: str) -> KeyfeatureArtefact:
|
|
126
|
+
def _default_keyfeature(title: str, use_default_contribution: bool) -> KeyfeatureArtefact:
|
|
109
127
|
intent = KeyfeatureIntent(
|
|
110
128
|
in_order_to="<support a capability or business goal>",
|
|
111
129
|
as_a="<main stakeholder who will benefit>",
|
|
112
130
|
i_want="<a product feature that helps me doing something so that I can achieve my named goal>"
|
|
113
131
|
)
|
|
114
|
-
description = """<further optional description to understand the
|
|
132
|
+
description = """<further optional description to understand the keyfeature, markdown capable text formatting, best practice is using
|
|
115
133
|
GIVEN any precondition
|
|
116
134
|
AND another precondition
|
|
117
135
|
WHEN some action takes place
|
|
118
136
|
THEN some result is to be expected
|
|
119
137
|
AND some other result is to be expected>"""
|
|
120
138
|
return KeyfeatureArtefact(
|
|
121
|
-
tags=[
|
|
139
|
+
tags=[],
|
|
140
|
+
author="creator_unknown",
|
|
122
141
|
title=title,
|
|
123
142
|
description=description,
|
|
124
|
-
intent=intent
|
|
143
|
+
intent=intent,
|
|
144
|
+
contribution=default_contribution() if use_default_contribution else None
|
|
125
145
|
)
|
|
126
146
|
|
|
127
147
|
|
|
128
|
-
def _default_feature(title: str) -> FeatureArtefact:
|
|
148
|
+
def _default_feature(title: str, use_default_contribution: bool) -> FeatureArtefact:
|
|
129
149
|
intent = FeatureIntent(
|
|
130
150
|
as_a="<user>",
|
|
131
151
|
i_want_to="<do something | need something>",
|
|
@@ -135,9 +155,9 @@ def _default_feature(title: str) -> FeatureArtefact:
|
|
|
135
155
|
Scenario(
|
|
136
156
|
title="<descriptive_scenario_title>",
|
|
137
157
|
steps=[
|
|
138
|
-
"Given
|
|
139
|
-
"When
|
|
140
|
-
"Then
|
|
158
|
+
"Given [precondition]",
|
|
159
|
+
"When [action]",
|
|
160
|
+
"Then [expected result]"
|
|
141
161
|
],
|
|
142
162
|
),
|
|
143
163
|
ScenarioOutline(
|
|
@@ -169,27 +189,30 @@ def _default_feature(title: str) -> FeatureArtefact:
|
|
|
169
189
|
]
|
|
170
190
|
)
|
|
171
191
|
]
|
|
172
|
-
description = """<further optional description to understand
|
|
173
|
-
the rule, no format defined, the example artefact is only a placeholder>"""
|
|
192
|
+
description = """<further optional description to understand the feature, no format defined, the example artefact is only a placeholder>"""
|
|
174
193
|
|
|
175
194
|
return FeatureArtefact(
|
|
176
|
-
tags=[
|
|
195
|
+
tags=[],
|
|
196
|
+
author="creator_unknown",
|
|
177
197
|
title=title,
|
|
178
198
|
description=description,
|
|
179
199
|
intent=intent,
|
|
180
|
-
scenarios=scenarios
|
|
200
|
+
scenarios=scenarios,
|
|
201
|
+
contribution=default_contribution() if use_default_contribution else None
|
|
181
202
|
)
|
|
182
203
|
|
|
183
204
|
|
|
184
|
-
def _default_task(title: str) -> TaskArtefact:
|
|
205
|
+
def _default_task(title: str, use_default_contribution: bool) -> TaskArtefact:
|
|
185
206
|
return TaskArtefact(
|
|
186
|
-
|
|
207
|
+
tags=[],
|
|
208
|
+
status=None,
|
|
187
209
|
title=title,
|
|
188
|
-
description="<further optional description to understand the task, no format defined>"
|
|
210
|
+
description="<further optional description to understand the task, no format defined>",
|
|
211
|
+
contribution=default_contribution() if use_default_contribution else None
|
|
189
212
|
)
|
|
190
213
|
|
|
191
214
|
|
|
192
|
-
def _default_issue(title: str) -> IssueArtefact:
|
|
215
|
+
def _default_issue(title: str, use_default_contribution: bool) -> IssueArtefact:
|
|
193
216
|
description = "<further free text description to understand the issue, no format defined>"
|
|
194
217
|
additional_description = """*Optional descriptions of the issue in Gherkin style*
|
|
195
218
|
|
|
@@ -200,14 +223,16 @@ def _default_issue(title: str) -> IssueArtefact:
|
|
|
200
223
|
*or optional free text description*"""
|
|
201
224
|
|
|
202
225
|
return IssueArtefact(
|
|
203
|
-
tags=[
|
|
226
|
+
tags=[],
|
|
227
|
+
author="creator_unknown",
|
|
204
228
|
title=title,
|
|
205
229
|
description=description,
|
|
206
|
-
additional_description=additional_description
|
|
230
|
+
additional_description=additional_description,
|
|
231
|
+
contribution=default_contribution() if use_default_contribution else None
|
|
207
232
|
)
|
|
208
233
|
|
|
209
234
|
|
|
210
|
-
def template_artefact_of_type(artefact_type: ArtefactType, title: str) -> Artefact:
|
|
235
|
+
def template_artefact_of_type(artefact_type: ArtefactType, title: str = "<descriptive_title>", use_default_contribution: bool = True) -> Artefact:
|
|
211
236
|
default_creation_functions = {
|
|
212
237
|
ArtefactType.vision: _default_vision,
|
|
213
238
|
ArtefactType.businessgoal: _default_businessgoal,
|
|
@@ -220,5 +245,6 @@ def template_artefact_of_type(artefact_type: ArtefactType, title: str) -> Artefa
|
|
|
220
245
|
ArtefactType.task: _default_task,
|
|
221
246
|
ArtefactType.issue: _default_issue
|
|
222
247
|
}
|
|
223
|
-
|
|
224
|
-
|
|
248
|
+
if artefact_type not in default_creation_functions.keys():
|
|
249
|
+
return None
|
|
250
|
+
return default_creation_functions[artefact_type](title, use_default_contribution)
|
|
@@ -38,7 +38,7 @@ class BusinessgoalIntent(Intent):
|
|
|
38
38
|
lines = []
|
|
39
39
|
|
|
40
40
|
as_a_line = as_a_serializer(self.as_a)
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
lines.append(f"In order to {self.in_order_to}")
|
|
43
43
|
lines.append(as_a_line)
|
|
44
44
|
lines.append(f"I want {self.i_want}")
|
|
@@ -47,39 +47,37 @@ class BusinessgoalIntent(Intent):
|
|
|
47
47
|
|
|
48
48
|
@classmethod
|
|
49
49
|
def deserialize_from_lines(cls, lines: List[str], start_index: int = 0) -> 'BusinessgoalIntent':
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
50
|
+
prefixes = [
|
|
51
|
+
("In order to ", "in_order_to"),
|
|
52
|
+
("As a ", "as_a"),
|
|
53
|
+
("As an ", "as_a"),
|
|
54
|
+
("I want ", "i_want"),
|
|
55
|
+
]
|
|
56
|
+
found = {"in_order_to": None, "as_a": None, "i_want": None}
|
|
57
|
+
|
|
58
|
+
def match_and_store(line):
|
|
59
|
+
for prefix, field in prefixes:
|
|
60
|
+
if line.startswith(prefix) and found[field] is None:
|
|
61
|
+
found[field] = line[len(prefix):].strip()
|
|
62
|
+
return True
|
|
63
|
+
return False
|
|
58
64
|
|
|
59
65
|
index = start_index
|
|
60
|
-
while index < len(lines) and (
|
|
61
|
-
|
|
62
|
-
if line.startswith(in_order_to_prefix) and not in_order_to:
|
|
63
|
-
in_order_to = line[len(in_order_to_prefix):].strip()
|
|
64
|
-
elif line.startswith(as_a_prefix) and not as_a:
|
|
65
|
-
as_a = line[len(as_a_prefix):].strip()
|
|
66
|
-
elif line.startswith(as_a_prefix_alt) and not as_a:
|
|
67
|
-
as_a = line[len(as_a_prefix_alt):].strip()
|
|
68
|
-
elif line.startswith(i_want_prefix) and not i_want:
|
|
69
|
-
i_want = line[len(i_want_prefix):].strip()
|
|
66
|
+
while index < len(lines) and any(v is None for v in found.values()):
|
|
67
|
+
match_and_store(lines[index])
|
|
70
68
|
index += 1
|
|
71
69
|
|
|
72
|
-
if not in_order_to:
|
|
70
|
+
if not found["in_order_to"]:
|
|
73
71
|
raise ValueError("Could not find 'In order to' line")
|
|
74
|
-
if not as_a:
|
|
72
|
+
if not found["as_a"]:
|
|
75
73
|
raise ValueError("Could not find 'As a' line")
|
|
76
|
-
if not i_want:
|
|
74
|
+
if not found["i_want"]:
|
|
77
75
|
raise ValueError("Could not find 'I want' line")
|
|
78
76
|
|
|
79
77
|
return cls(
|
|
80
|
-
in_order_to=in_order_to,
|
|
81
|
-
as_a=as_a,
|
|
82
|
-
i_want=i_want
|
|
78
|
+
in_order_to=found["in_order_to"],
|
|
79
|
+
as_a=found["as_a"],
|
|
80
|
+
i_want=found["i_want"]
|
|
83
81
|
)
|
|
84
82
|
|
|
85
83
|
|