codeplain 0.1.0__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.
Files changed (51) hide show
  1. codeplain-0.1.0.dist-info/METADATA +142 -0
  2. codeplain-0.1.0.dist-info/RECORD +51 -0
  3. codeplain-0.1.0.dist-info/WHEEL +5 -0
  4. codeplain-0.1.0.dist-info/entry_points.txt +2 -0
  5. codeplain-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. codeplain-0.1.0.dist-info/top_level.txt +36 -0
  7. codeplain_REST_api.py +370 -0
  8. config/__init__.py +2 -0
  9. config/system_config.yaml +27 -0
  10. file_utils.py +316 -0
  11. git_utils.py +304 -0
  12. hash_key.py +29 -0
  13. plain2code.py +218 -0
  14. plain2code_arguments.py +286 -0
  15. plain2code_console.py +107 -0
  16. plain2code_exceptions.py +45 -0
  17. plain2code_nodes.py +108 -0
  18. plain2code_read_config.py +74 -0
  19. plain2code_state.py +75 -0
  20. plain2code_utils.py +56 -0
  21. plain_spec.py +360 -0
  22. render_machine/actions/analyze_specification_ambiguity.py +50 -0
  23. render_machine/actions/base_action.py +19 -0
  24. render_machine/actions/commit_conformance_tests_changes.py +46 -0
  25. render_machine/actions/commit_implementation_code_changes.py +22 -0
  26. render_machine/actions/create_dist.py +26 -0
  27. render_machine/actions/exit_with_error.py +22 -0
  28. render_machine/actions/fix_conformance_test.py +121 -0
  29. render_machine/actions/fix_unit_tests.py +57 -0
  30. render_machine/actions/prepare_repositories.py +50 -0
  31. render_machine/actions/prepare_testing_environment.py +30 -0
  32. render_machine/actions/refactor_code.py +48 -0
  33. render_machine/actions/render_conformance_tests.py +169 -0
  34. render_machine/actions/render_functional_requirement.py +69 -0
  35. render_machine/actions/run_conformance_tests.py +44 -0
  36. render_machine/actions/run_unit_tests.py +38 -0
  37. render_machine/actions/summarize_conformance_tests.py +34 -0
  38. render_machine/code_renderer.py +50 -0
  39. render_machine/conformance_test_helpers.py +68 -0
  40. render_machine/implementation_code_helpers.py +20 -0
  41. render_machine/render_context.py +280 -0
  42. render_machine/render_types.py +36 -0
  43. render_machine/render_utils.py +92 -0
  44. render_machine/state_machine_config.py +408 -0
  45. render_machine/states.py +52 -0
  46. render_machine/triggers.py +27 -0
  47. standard_template_library/__init__.py +1 -0
  48. standard_template_library/golang-console-app-template.plain +36 -0
  49. standard_template_library/python-console-app-template.plain +32 -0
  50. standard_template_library/typescript-react-app-template.plain +22 -0
  51. system_config.py +49 -0
plain2code_nodes.py ADDED
@@ -0,0 +1,108 @@
1
+ from pathlib import Path
2
+ from typing import Mapping, Sequence, TextIO
3
+
4
+ from liquid2 import Environment, RenderContext, Template, TemplateNotFoundError
5
+ from liquid2.builtin import IncludeTag
6
+ from liquid2.builtin.tags.include_tag import IncludeNode
7
+
8
+
9
+ class Plain2CodeIncludeNode(IncludeNode):
10
+ def render_to_output(self, context: RenderContext, buffer: TextIO) -> int:
11
+ """Render the node to the output buffer."""
12
+ name = self.name.evaluate(context)
13
+ whitespaces = 0
14
+ is_comment = False
15
+ i = self.token.start
16
+ while self.token.source[i] != "\n" and i >= 0:
17
+ if self.token.source[i] == " ":
18
+ whitespaces += 1
19
+ elif self.token.source[i] == ">":
20
+ is_comment = True
21
+ break
22
+ else:
23
+ whitespaces = 0
24
+ i -= 1
25
+
26
+ if is_comment:
27
+ return buffer.write(str(self))
28
+
29
+ try:
30
+ template = context.env.get_template(str(name), context=context, tag=self.tag, whitespaces=whitespaces)
31
+ except TemplateNotFoundError as err:
32
+ err.token = self.name.token
33
+ err.template_name = context.template.full_name()
34
+ raise
35
+
36
+ namespace: dict[str, object] = dict(arg.evaluate(context) for arg in self.args)
37
+
38
+ character_count = 0
39
+
40
+ with context.extend(namespace, template=template):
41
+ if self.var:
42
+ val = self.var.evaluate(context)
43
+ key = self.alias or template.name.split(".")[0]
44
+
45
+ if isinstance(val, Sequence) and not isinstance(val, str):
46
+ context.raise_for_loop_limit(len(val))
47
+ for itm in val:
48
+ namespace[key] = itm
49
+ character_count += template.render_with_context(context, buffer, partial=True)
50
+ else:
51
+ namespace[key] = val
52
+ character_count = template.render_with_context(context, buffer, partial=True)
53
+ else:
54
+
55
+ character_count = template.render_with_context(context, buffer, partial=True)
56
+
57
+ return character_count
58
+
59
+
60
+ class Plain2CodeIncludeTag(IncludeTag):
61
+ node_class = Plain2CodeIncludeNode
62
+
63
+
64
+ class Plain2CodeLoaderMixin:
65
+ def __init__(self, *args, **kwargs):
66
+ if not hasattr(self, "get_source"):
67
+ raise NotImplementedError("Class must implement get_source")
68
+ super().__init__(*args, **kwargs)
69
+
70
+ def load(
71
+ self,
72
+ env: Environment,
73
+ name: str,
74
+ *,
75
+ globals: Mapping[str, object] | None = None,
76
+ context: RenderContext | None = None,
77
+ **kwargs: object,
78
+ ) -> Template:
79
+ """
80
+ Find and parse template source code.
81
+
82
+ Args:
83
+ env: The `Environment` attempting to load the template source text.
84
+ name: A name or identifier for a template's source text.
85
+ globals: A mapping of render context variables attached to the
86
+ resulting template.
87
+ context: An optional render context that can be used to narrow the template
88
+ source search space.
89
+ kwargs: Arbitrary arguments that can be used to narrow the template source
90
+ search space.
91
+ """
92
+ source, full_name, uptodate, matter = self.get_source(env, name, context=context, **kwargs)
93
+ whitespaces = kwargs.get("whitespaces", 0)
94
+ assert isinstance(whitespaces, int)
95
+ source = source.rstrip().replace("\n", "\n" + " " * whitespaces)
96
+
97
+ path = Path(full_name)
98
+
99
+ template = env.from_string(
100
+ source,
101
+ name=path.name,
102
+ path=path,
103
+ globals=globals,
104
+ overlay_data=matter,
105
+ )
106
+
107
+ template.uptodate = uptodate
108
+ return template
@@ -0,0 +1,74 @@
1
+ import os
2
+ from argparse import ArgumentParser, Namespace
3
+ from typing import Any, Dict
4
+
5
+ import yaml
6
+
7
+ from plain2code_console import console
8
+
9
+
10
+ def load_config(config_file: str) -> Dict[str, Any]:
11
+ """Load configuration from YAML file."""
12
+ try:
13
+ with open(config_file, "r") as f:
14
+ return yaml.safe_load(f)
15
+ except Exception as e:
16
+ console.error(f"Error loading config file: {e}. Please check the config file path and the config file content.")
17
+ raise e
18
+
19
+
20
+ def validate_config(config: Dict[str, Any], parser: ArgumentParser) -> None:
21
+ """Validate the configuration against the parser."""
22
+ actions = [action.dest for action in parser._actions]
23
+ for action in parser._actions:
24
+ if hasattr(action, "option_strings"):
25
+ actions.extend(opt.lstrip("-") for opt in action.option_strings)
26
+
27
+ for key in config.keys():
28
+ if key not in actions:
29
+ raise KeyError(f"Invalid configuration key: {key}")
30
+ return config
31
+
32
+
33
+ def get_args_from_config(config_file: str, parser: ArgumentParser) -> Namespace:
34
+ """
35
+ Read configuration from YAML file and return args compatible with plain2code_arguments.py.
36
+
37
+ Args:
38
+ config_file: Path to the YAML config file
39
+ Returns:
40
+ Namespace object with arguments as defined in plain2code_arguments.py
41
+
42
+ Raises:
43
+ FileNotFoundError: If config file doesn't exist
44
+ KeyError: If argument not found in parser arguments
45
+ """
46
+
47
+ args = Namespace()
48
+
49
+ if config_file == "config.yaml":
50
+ if not os.path.exists(config_file):
51
+ console.info(f"Default config file {config_file} not found. No config file is read.")
52
+ return args
53
+
54
+ # Load config
55
+ config = load_config(config_file)
56
+ config = validate_config(config, parser)
57
+
58
+ for action in parser._actions:
59
+ # Create a list of possible config keys for this argument
60
+ possible_keys = [action.dest]
61
+ if hasattr(action, "option_strings"):
62
+ # Add all option strings without leading dashes
63
+ possible_keys.extend(opt.lstrip("-") for opt in action.option_strings)
64
+
65
+ # Handling multi-named arguments like --verbose and -v
66
+ config_value = None
67
+ for key in possible_keys:
68
+ if key in config:
69
+ config_value = config[key]
70
+ break
71
+
72
+ if config_value is not None:
73
+ setattr(args, action.dest, config_value)
74
+ return args
plain2code_state.py ADDED
@@ -0,0 +1,75 @@
1
+ """Contains all state and context information we need for the rendering process."""
2
+
3
+ import json
4
+ import os
5
+ import uuid
6
+ from typing import Optional
7
+
8
+ from plain2code_console import console
9
+
10
+ CONFORMANCE_TESTS_DEFINITION_FILE_NAME = "conformance_tests.json"
11
+
12
+
13
+ class ConformanceTestsUtils:
14
+ """Manages the state of conformance tests."""
15
+
16
+ def __init__(
17
+ self,
18
+ conformance_tests_folder: str,
19
+ conformance_tests_definition_file_name: str,
20
+ verbose: bool,
21
+ ):
22
+ self.conformance_tests_folder = conformance_tests_folder
23
+ self.full_conformance_tests_definition_file_name = os.path.join(
24
+ self.conformance_tests_folder, conformance_tests_definition_file_name
25
+ )
26
+ self.verbose = verbose
27
+
28
+ def get_conformance_tests_json(self):
29
+ try:
30
+ with open(self.full_conformance_tests_definition_file_name, "r") as f:
31
+ return json.load(f)
32
+ except FileNotFoundError:
33
+ return {}
34
+
35
+ def dump_conformance_tests_json(self, conformance_tests_json: dict):
36
+ """Dump the conformance tests definition to the file."""
37
+ if os.path.exists(self.conformance_tests_folder):
38
+ if self.verbose:
39
+ console.info(
40
+ f"Storing conformance tests definition to {self.full_conformance_tests_definition_file_name}"
41
+ )
42
+ with open(self.full_conformance_tests_definition_file_name, "w") as f:
43
+ json.dump(conformance_tests_json, f, indent=4)
44
+
45
+
46
+ class RunState:
47
+ """Contains information about the identifiable state of the rendering process."""
48
+
49
+ def __init__(self, spec_filename: str, replay_with: Optional[str] = None):
50
+ self.replay: bool = replay_with is not None
51
+ if replay_with:
52
+ self.render_id: str = replay_with
53
+ else:
54
+ self.render_id: str = str(uuid.uuid4())
55
+ self.spec_filename: str = spec_filename
56
+ self.call_count: int = 0
57
+ self.unittest_batch_id: int = 0
58
+ self.frid_render_anaysis: dict[str, str] = {}
59
+
60
+ def increment_call_count(self):
61
+ self.call_count += 1
62
+
63
+ def increment_unittest_batch_id(self):
64
+ self.unittest_batch_id += 1
65
+
66
+ def add_rendering_analysis_for_frid(self, frid, rendering_analysis) -> None:
67
+ self.frid_render_anaysis[frid] = rendering_analysis
68
+
69
+ def to_dict(self):
70
+ return {
71
+ "render_id": self.render_id,
72
+ "call_count": self.call_count,
73
+ "replay": self.replay,
74
+ "spec_filename": self.spec_filename,
75
+ }
plain2code_utils.py ADDED
@@ -0,0 +1,56 @@
1
+ import logging
2
+ from typing import Optional
3
+
4
+ import plain_spec
5
+ from plain2code_console import console
6
+
7
+ AMBIGUITY_CAUSES = {
8
+ "reference_resource_ambiguity": "Ambiguity is in the reference resources",
9
+ "definition_ambiguity": "Ambiguity is in the definitions",
10
+ "non_functional_requirement_ambiguity": "Ambiguity is in the non-functional requirements",
11
+ "functional_requirement_ambiguity": "Ambiguity is in the functional requirements",
12
+ "other": "Ambiguity in the other parts of the specification",
13
+ }
14
+
15
+
16
+ class RetryOnlyFilter(logging.Filter):
17
+ def filter(self, record):
18
+ # Allow all logs with level > DEBUG (i.e., INFO and above)
19
+ if record.levelno > logging.DEBUG:
20
+ return True
21
+ # For DEBUG logs, only allow if message matches retry-related patterns
22
+ msg = record.getMessage().lower()
23
+ return (
24
+ "retrying due to" in msg
25
+ or "raising timeout error" in msg
26
+ or "raising connection error" in msg
27
+ or "encountered exception" in msg
28
+ or "retrying request" in msg
29
+ or "retry left" in msg
30
+ or "1 retry left" in msg
31
+ or "retries left" in msg
32
+ )
33
+
34
+
35
+ def print_dry_run_output(plain_source_tree: dict, render_range: Optional[list[str]]):
36
+ frid = plain_spec.get_first_frid(plain_source_tree)
37
+
38
+ while frid is not None:
39
+ is_inside_range = render_range is None or frid in render_range
40
+
41
+ if is_inside_range:
42
+ specifications, _ = plain_spec.get_specifications_for_frid(plain_source_tree, frid)
43
+ functional_requirement_text = specifications[plain_spec.FUNCTIONAL_REQUIREMENTS][-1]
44
+ console.info("\n-------------------------------------")
45
+ console.info(f"Rendering functional requirement {frid}")
46
+ console.info(f"[b]{functional_requirement_text}[/b]")
47
+ console.info("-------------------------------------\n")
48
+ if plain_spec.ACCEPTANCE_TESTS in specifications:
49
+ for i, acceptance_test in enumerate(specifications[plain_spec.ACCEPTANCE_TESTS], 1):
50
+ console.info(f"\nGenerating acceptance test #{i}:\n\n{acceptance_test}")
51
+ else:
52
+ console.info("\n-------------------------------------")
53
+ console.info(f"Skipping rendering iteration: {frid}")
54
+ console.info("-------------------------------------\n")
55
+
56
+ frid = plain_spec.get_next_frid(plain_source_tree, frid)
plain_spec.py ADDED
@@ -0,0 +1,360 @@
1
+ import hashlib
2
+ import json
3
+ import uuid
4
+ from typing import Optional
5
+
6
+ from liquid2.filter import with_context
7
+
8
+ DEFINITIONS = "Definitions:"
9
+ NON_FUNCTIONAL_REQUIREMENTS = "Non-Functional Requirements:"
10
+ TEST_REQUIREMENTS = "Test Requirements:"
11
+ FUNCTIONAL_REQUIREMENTS = "Functional Requirements:"
12
+ ACCEPTANCE_TESTS = "acceptance_tests"
13
+ ACCEPTANCE_TEST_HEADING = "Acceptance Tests:"
14
+
15
+ ALLOWED_SPECIFICATION_HEADINGS = [
16
+ DEFINITIONS,
17
+ NON_FUNCTIONAL_REQUIREMENTS,
18
+ TEST_REQUIREMENTS,
19
+ FUNCTIONAL_REQUIREMENTS,
20
+ ACCEPTANCE_TEST_HEADING,
21
+ ]
22
+
23
+
24
+ class InvalidLiquidVariableName(Exception):
25
+ pass
26
+
27
+
28
+ def collect_specification_linked_resources(specification, specification_heading, linked_resources_list):
29
+ linked_resources = []
30
+ if "linked_resources" in specification:
31
+ linked_resources.extend(specification["linked_resources"])
32
+
33
+ for resource in linked_resources:
34
+ resource_found = False
35
+ for resource_map in linked_resources_list:
36
+ if resource["text"] == resource_map["text"]:
37
+ if resource["target"] != resource_map["target"]:
38
+ raise Exception(
39
+ f"The file {resource['target']} is linked to multiple linked resources with the same text: {resource['text']}"
40
+ )
41
+ elif resource["target"] == resource_map["target"]:
42
+ if resource["text"] != resource_map["text"]:
43
+ raise Exception(
44
+ f"The linked resource {resource['text']} is linked to multiple files: {resource_map['target']}"
45
+ )
46
+ else:
47
+ continue
48
+
49
+ if resource_found:
50
+ raise Exception(
51
+ "Duplicate linked resource found: " + resource["text"] + " (" + resource["target"] + ")"
52
+ )
53
+
54
+ resource_found = True
55
+ resource_map["sections"].append(specification_heading)
56
+
57
+ if not resource_found:
58
+ linked_resources_list.append(
59
+ {"text": resource["text"], "target": resource["target"], "sections": [specification_heading]}
60
+ )
61
+
62
+
63
+ def collect_linked_resources_in_section(
64
+ section, linked_resources_list, specifications_list, include_acceptance_tests, frid=Optional[str]
65
+ ):
66
+ # When should we collect specification headings in the current section. Either when one of the following holds true:
67
+ # - frid wasn't specified
68
+ # - section has no ID (and that's exactly when it's the root section)
69
+ # - section has ID, frid was specified and the specified frid inside the current section tree
70
+ should_collect_specification_headings_in_current_section = (
71
+ frid is None or "ID" not in section or frid.startswith(section["ID"])
72
+ )
73
+ if should_collect_specification_headings_in_current_section:
74
+ specifications_to_collect = list(set([DEFINITIONS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS]))
75
+ if specifications_list:
76
+ specifications_to_collect = list(set(specifications_to_collect) & set(specifications_list))
77
+ for specification_heading in specifications_to_collect:
78
+ if specification_heading in section:
79
+ for requirement in section[specification_heading]:
80
+ collect_specification_linked_resources(requirement, specification_heading, linked_resources_list)
81
+
82
+ if FUNCTIONAL_REQUIREMENTS in section and (
83
+ not specifications_list or FUNCTIONAL_REQUIREMENTS in specifications_list
84
+ ):
85
+ functional_requirement_count = 0
86
+ for requirement in section[FUNCTIONAL_REQUIREMENTS]:
87
+ collect_specification_linked_resources(requirement, FUNCTIONAL_REQUIREMENTS, linked_resources_list)
88
+
89
+ functional_requirement_count += 1
90
+ section_id = section.get("ID", None)
91
+ current_frid = get_current_frid(section_id, functional_requirement_count)
92
+
93
+ if ACCEPTANCE_TESTS in requirement and include_acceptance_tests and (frid is None or frid == current_frid):
94
+ for acceptance_test in requirement[ACCEPTANCE_TESTS]:
95
+ collect_specification_linked_resources(
96
+ acceptance_test, FUNCTIONAL_REQUIREMENTS, linked_resources_list
97
+ )
98
+
99
+ if frid is not None and current_frid == frid:
100
+ # here, we rely on the fact that FRIDs are incrementing. And if we have reached the current FRID, we should
101
+ # not collect any FRIDs after the current one.
102
+ return True
103
+
104
+ if "sections" in section:
105
+ for subsection in section["sections"]:
106
+ if collect_linked_resources_in_section(
107
+ subsection, linked_resources_list, specifications_list, include_acceptance_tests, frid
108
+ ):
109
+ return True
110
+
111
+ return False
112
+
113
+
114
+ # TODO: check if this function can be refactored to return the list of linked resources instead of modifying it in place
115
+ def collect_linked_resources(
116
+ plain_source_tree, linked_resources_list, specifications_list, include_acceptance_tests, frid=None
117
+ ):
118
+
119
+ if not isinstance(plain_source_tree, dict):
120
+ raise ValueError("[plain_source_tree must be a dictionary.")
121
+
122
+ if frid is not None:
123
+ functional_requirements = get_frids(plain_source_tree)
124
+ if frid not in functional_requirements:
125
+ raise ValueError(f"frid {frid} does not exist.")
126
+
127
+ result = collect_linked_resources_in_section(
128
+ plain_source_tree, linked_resources_list, specifications_list, include_acceptance_tests, frid
129
+ )
130
+
131
+ # Sort linked_resources_list by the "text" field
132
+ linked_resources_list.sort(key=lambda x: x["text"])
133
+
134
+ return result
135
+
136
+
137
+ def get_frids(plain_source_tree):
138
+ if FUNCTIONAL_REQUIREMENTS in plain_source_tree:
139
+ for functional_requirement_id in range(1, len(plain_source_tree[FUNCTIONAL_REQUIREMENTS]) + 1):
140
+ if "ID" in plain_source_tree:
141
+ yield plain_source_tree["ID"] + "." + str(functional_requirement_id)
142
+ else:
143
+ yield str(functional_requirement_id)
144
+
145
+ if "sections" in plain_source_tree:
146
+ for section in plain_source_tree["sections"]:
147
+ yield from get_frids(section)
148
+
149
+
150
+ def get_first_frid(plain_source_tree):
151
+ return next(get_frids(plain_source_tree), None)
152
+
153
+
154
+ def get_current_frid(section_id: Optional[str], functional_requirement_count: int) -> str:
155
+ if section_id is None:
156
+ return str(functional_requirement_count)
157
+ else:
158
+ return section_id + "." + str(functional_requirement_count)
159
+
160
+
161
+ def get_next_frid(plain_source_tree, frid):
162
+ functional_requirements = get_frids(plain_source_tree)
163
+ for temp_frid in functional_requirements:
164
+ if temp_frid == frid:
165
+ return next(functional_requirements, None)
166
+
167
+ raise Exception(f"Functional requirement {frid} does not exist.")
168
+
169
+
170
+ def get_previous_frid(plain_source_tree, frid):
171
+ previous_frid = None
172
+ for temp_frid in get_frids(plain_source_tree):
173
+ if temp_frid == frid:
174
+ return previous_frid
175
+
176
+ previous_frid = temp_frid
177
+
178
+ raise Exception(f"Functional requirement {frid} does not exist.")
179
+
180
+
181
+ def get_specification_item_markdown(specification_item, code_variables, replace_code_variables):
182
+ markdown = specification_item["markdown"]
183
+ if "code_variables" in specification_item:
184
+ for code_variable in specification_item["code_variables"]:
185
+ if code_variable["name"] in code_variables:
186
+ if code_variables[code_variable["name"]] != code_variable["value"]:
187
+ raise Exception(
188
+ f"Code variable {code_variable['name']} has multiple values: {code_variables[code_variable['name']]} and {code_variable['value']}"
189
+ )
190
+ else:
191
+ code_variables[code_variable["name"]] = code_variable["value"]
192
+
193
+ if replace_code_variables:
194
+ markdown = markdown.replace(f"{{{{ {code_variable['name']} }}}}", code_variable["value"])
195
+
196
+ return markdown
197
+
198
+
199
+ def get_specifications_from_plain_source_tree(
200
+ frid,
201
+ plain_source_tree,
202
+ definitions,
203
+ non_functional_requirements,
204
+ test_requirements,
205
+ functional_requirements,
206
+ acceptance_tests,
207
+ code_variables,
208
+ replace_code_variables,
209
+ section_id=None,
210
+ ):
211
+ return_frid = None
212
+ if FUNCTIONAL_REQUIREMENTS in plain_source_tree and len(plain_source_tree[FUNCTIONAL_REQUIREMENTS]) > 0:
213
+ functional_requirement_count = 0
214
+ for functional_requirement in plain_source_tree[FUNCTIONAL_REQUIREMENTS]:
215
+ functional_requirement_count += 1
216
+ if section_id is None:
217
+ current_frid = str(functional_requirement_count)
218
+ else:
219
+ current_frid = section_id + "." + str(functional_requirement_count)
220
+
221
+ functional_requirements.append(
222
+ get_specification_item_markdown(functional_requirement, code_variables, replace_code_variables)
223
+ )
224
+
225
+ if current_frid == frid:
226
+ if ACCEPTANCE_TESTS in functional_requirement:
227
+ for acceptance_test in functional_requirement[ACCEPTANCE_TESTS]:
228
+ acceptance_tests.append(
229
+ get_specification_item_markdown(acceptance_test, code_variables, replace_code_variables)
230
+ )
231
+
232
+ return_frid = current_frid
233
+ break
234
+
235
+ if "sections" in plain_source_tree:
236
+ for section in plain_source_tree["sections"]:
237
+ sub_frid = get_specifications_from_plain_source_tree(
238
+ frid,
239
+ section,
240
+ definitions,
241
+ non_functional_requirements,
242
+ test_requirements,
243
+ functional_requirements,
244
+ acceptance_tests,
245
+ code_variables,
246
+ replace_code_variables,
247
+ section["ID"],
248
+ )
249
+ if sub_frid is not None:
250
+ return_frid = sub_frid
251
+ break
252
+
253
+ if return_frid is not None:
254
+ if DEFINITIONS in plain_source_tree and plain_source_tree[DEFINITIONS] is not None:
255
+ definitions[0:0] = [
256
+ get_specification_item_markdown(specification, code_variables, replace_code_variables)
257
+ for specification in plain_source_tree[DEFINITIONS]
258
+ ]
259
+ if (
260
+ NON_FUNCTIONAL_REQUIREMENTS in plain_source_tree
261
+ and plain_source_tree[NON_FUNCTIONAL_REQUIREMENTS] is not None
262
+ ):
263
+ non_functional_requirements[0:0] = [
264
+ get_specification_item_markdown(specification, code_variables, replace_code_variables)
265
+ for specification in plain_source_tree[NON_FUNCTIONAL_REQUIREMENTS]
266
+ ]
267
+ if TEST_REQUIREMENTS in plain_source_tree and plain_source_tree[TEST_REQUIREMENTS] is not None:
268
+ test_requirements[0:0] = [
269
+ get_specification_item_markdown(specification, code_variables, replace_code_variables)
270
+ for specification in plain_source_tree[TEST_REQUIREMENTS]
271
+ ]
272
+
273
+ return return_frid
274
+
275
+
276
+ def get_specifications_for_frid(plain_source_tree, frid, replace_code_variables=True):
277
+ definitions = []
278
+ non_functional_requirements = []
279
+ test_requirements = []
280
+ functional_requirements = []
281
+ acceptance_tests = []
282
+
283
+ code_variables = {}
284
+
285
+ result = get_specifications_from_plain_source_tree(
286
+ frid,
287
+ plain_source_tree,
288
+ definitions,
289
+ non_functional_requirements,
290
+ test_requirements,
291
+ functional_requirements,
292
+ acceptance_tests,
293
+ code_variables,
294
+ replace_code_variables,
295
+ )
296
+ if result is None:
297
+ raise Exception(f"Functional requirement {frid} does not exist.")
298
+
299
+ specifications = {
300
+ DEFINITIONS: definitions,
301
+ NON_FUNCTIONAL_REQUIREMENTS: non_functional_requirements,
302
+ TEST_REQUIREMENTS: test_requirements,
303
+ FUNCTIONAL_REQUIREMENTS: functional_requirements,
304
+ }
305
+
306
+ if acceptance_tests:
307
+ specifications[ACCEPTANCE_TESTS] = acceptance_tests
308
+
309
+ if code_variables:
310
+ return specifications, code_variables
311
+ else:
312
+ return specifications, None
313
+
314
+
315
+ @with_context
316
+ def code_variable_liquid_filter(value, *, context):
317
+ if len(context.scope) == 0:
318
+ raise Exception("Invalid use of code_variable filter!")
319
+
320
+ if "code_variables" in context.globals:
321
+ code_variables = context.globals["code_variables"]
322
+
323
+ variable = next(iter(context.scope.items()))
324
+
325
+ unique_str = uuid.uuid4().hex
326
+
327
+ code_variables[unique_str] = {variable[0]: value}
328
+
329
+ return unique_str
330
+ else:
331
+
332
+ return value
333
+
334
+
335
+ @with_context
336
+ def prohibited_chars_liquid_filter(value, prohibited_chars, *, context):
337
+ if not isinstance(value, str):
338
+ value = str(value)
339
+
340
+ if len(context.scope) == 0:
341
+ raise Exception("Invalid use of prohibited_chars filter!")
342
+
343
+ variable = next(iter(context.scope.items()))
344
+ variable_name = variable[0]
345
+
346
+ for char in prohibited_chars:
347
+ if char in value:
348
+ raise InvalidLiquidVariableName(
349
+ f"'{char}' is not a valid character for variable '{variable_name}' (value: '{value}')."
350
+ )
351
+
352
+ return value
353
+
354
+
355
+ def hash_text(text):
356
+ return hashlib.sha256(text.encode()).hexdigest()
357
+
358
+
359
+ def get_hash_value(specifications):
360
+ return hash_text(json.dumps(specifications, indent=4))