plain-parser 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.
- plain_parser/__init__.py +27 -0
- plain_parser/concept_utils.py +221 -0
- plain_parser/exceptions.py +39 -0
- plain_parser/liquid_nodes.py +109 -0
- plain_parser/loaders.py +101 -0
- plain_parser/plain_file.py +861 -0
- plain_parser/plain_spec.py +414 -0
- plain_parser-0.1.0.dist-info/METADATA +62 -0
- plain_parser-0.1.0.dist-info/RECORD +11 -0
- plain_parser-0.1.0.dist-info/WHEEL +4 -0
- plain_parser-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from liquid2.filter import with_context
|
|
7
|
+
|
|
8
|
+
from plain_parser.exceptions import InvalidFridArgument, InvalidLiquidVariableName
|
|
9
|
+
|
|
10
|
+
DEFINITIONS = "definitions"
|
|
11
|
+
NON_FUNCTIONAL_REQUIREMENTS = "implementation reqs"
|
|
12
|
+
TEST_REQUIREMENTS = "test reqs"
|
|
13
|
+
FUNCTIONAL_REQUIREMENTS = "functional specs"
|
|
14
|
+
ACCEPTANCE_TESTS = "acceptance_tests"
|
|
15
|
+
ACCEPTANCE_TEST_HEADING = "acceptance tests"
|
|
16
|
+
|
|
17
|
+
ALLOWED_SPECIFICATION_HEADINGS = [
|
|
18
|
+
DEFINITIONS,
|
|
19
|
+
NON_FUNCTIONAL_REQUIREMENTS,
|
|
20
|
+
TEST_REQUIREMENTS,
|
|
21
|
+
FUNCTIONAL_REQUIREMENTS,
|
|
22
|
+
ACCEPTANCE_TEST_HEADING,
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
ALLOWED_IMPORT_SPECIFICATION_HEADINGS = [DEFINITIONS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def has_acceptance_tests(plain_source_tree) -> bool:
|
|
29
|
+
"""Return True if any functional requirement in the plain source defines acceptance tests."""
|
|
30
|
+
functional_requirements = plain_source_tree.get(FUNCTIONAL_REQUIREMENTS) or []
|
|
31
|
+
for functional_requirement in functional_requirements:
|
|
32
|
+
if functional_requirement.get(ACCEPTANCE_TESTS):
|
|
33
|
+
return True
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def collect_specification_linked_resources(specification, specification_heading, linked_resources_list):
|
|
38
|
+
linked_resources = []
|
|
39
|
+
if "linked_resources" in specification:
|
|
40
|
+
linked_resources.extend(specification["linked_resources"])
|
|
41
|
+
|
|
42
|
+
for resource in linked_resources:
|
|
43
|
+
resource_found = False
|
|
44
|
+
for resource_map in linked_resources_list:
|
|
45
|
+
if resource["text"] == resource_map["text"]:
|
|
46
|
+
if resource["target"] != resource_map["target"]:
|
|
47
|
+
raise Exception(
|
|
48
|
+
f"The file {resource['target']} is linked to multiple linked resources with the same text: {resource['text']}"
|
|
49
|
+
)
|
|
50
|
+
elif resource["target"] == resource_map["target"]:
|
|
51
|
+
if resource["text"] != resource_map["text"]:
|
|
52
|
+
raise Exception(
|
|
53
|
+
f"The linked resource {resource['text']} is linked to multiple files: {resource_map['target']}"
|
|
54
|
+
)
|
|
55
|
+
else:
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
if resource_found:
|
|
59
|
+
raise Exception(
|
|
60
|
+
"Duplicate linked resource found: " + resource["text"] + " (" + resource["target"] + ")"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
resource_found = True
|
|
64
|
+
resource_map["sections"].append(specification_heading)
|
|
65
|
+
|
|
66
|
+
if not resource_found:
|
|
67
|
+
linked_resources_list.append(
|
|
68
|
+
{"text": resource["text"], "target": resource["target"], "sections": [specification_heading]}
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def collect_linked_resources_in_section(
|
|
73
|
+
section, linked_resources_list, specifications_list, include_acceptance_tests, frid=Optional[str]
|
|
74
|
+
):
|
|
75
|
+
# When should we collect specification headings in the current section. Either when one of the following holds true:
|
|
76
|
+
# - frid wasn't specified
|
|
77
|
+
# - section has no ID (and that's exactly when it's the root section)
|
|
78
|
+
# - section has ID, frid was specified and the specified frid inside the current section tree
|
|
79
|
+
should_collect_specification_headings_in_current_section = (
|
|
80
|
+
frid is None or "ID" not in section or frid.startswith(section["ID"])
|
|
81
|
+
)
|
|
82
|
+
if should_collect_specification_headings_in_current_section:
|
|
83
|
+
specifications_to_collect = list(set([DEFINITIONS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS]))
|
|
84
|
+
if specifications_list:
|
|
85
|
+
specifications_to_collect = list(set(specifications_to_collect) & set(specifications_list))
|
|
86
|
+
for specification_heading in specifications_to_collect:
|
|
87
|
+
if specification_heading in section:
|
|
88
|
+
for requirement in section[specification_heading]:
|
|
89
|
+
collect_specification_linked_resources(requirement, specification_heading, linked_resources_list)
|
|
90
|
+
|
|
91
|
+
if FUNCTIONAL_REQUIREMENTS in section and (
|
|
92
|
+
not specifications_list or FUNCTIONAL_REQUIREMENTS in specifications_list
|
|
93
|
+
):
|
|
94
|
+
functional_requirement_count = 0
|
|
95
|
+
for requirement in section[FUNCTIONAL_REQUIREMENTS]:
|
|
96
|
+
collect_specification_linked_resources(requirement, FUNCTIONAL_REQUIREMENTS, linked_resources_list)
|
|
97
|
+
|
|
98
|
+
functional_requirement_count += 1
|
|
99
|
+
section_id = section.get("ID", None)
|
|
100
|
+
current_frid = get_current_frid(section_id, functional_requirement_count)
|
|
101
|
+
|
|
102
|
+
if ACCEPTANCE_TESTS in requirement and include_acceptance_tests and (frid is None or frid == current_frid):
|
|
103
|
+
for acceptance_test in requirement[ACCEPTANCE_TESTS]:
|
|
104
|
+
collect_specification_linked_resources(
|
|
105
|
+
acceptance_test, FUNCTIONAL_REQUIREMENTS, linked_resources_list
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if frid is not None and current_frid == frid:
|
|
109
|
+
# here, we rely on the fact that FRIDs are incrementing. And if we have reached the current FRID, we should
|
|
110
|
+
# not collect any FRIDs after the current one.
|
|
111
|
+
return True
|
|
112
|
+
|
|
113
|
+
if "sections" in section:
|
|
114
|
+
for subsection in section["sections"]:
|
|
115
|
+
if collect_linked_resources_in_section(
|
|
116
|
+
subsection, linked_resources_list, specifications_list, include_acceptance_tests, frid
|
|
117
|
+
):
|
|
118
|
+
return True
|
|
119
|
+
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# TODO: check if this function can be refactored to return the list of linked resources instead of modifying it in place
|
|
124
|
+
def collect_linked_resources(
|
|
125
|
+
plain_source_tree, linked_resources_list, specifications_list, include_acceptance_tests, frid=None
|
|
126
|
+
):
|
|
127
|
+
|
|
128
|
+
if not isinstance(plain_source_tree, dict):
|
|
129
|
+
raise ValueError("[plain_source_tree must be a dictionary.")
|
|
130
|
+
|
|
131
|
+
if frid is not None:
|
|
132
|
+
functional_requirements = get_frids(plain_source_tree)
|
|
133
|
+
if frid not in functional_requirements:
|
|
134
|
+
raise ValueError(f"frid {frid} does not exist.")
|
|
135
|
+
|
|
136
|
+
result = collect_linked_resources_in_section(
|
|
137
|
+
plain_source_tree, linked_resources_list, specifications_list, include_acceptance_tests, frid
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Sort linked_resources_list by the "text" field
|
|
141
|
+
linked_resources_list.sort(key=lambda x: x["text"])
|
|
142
|
+
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def get_frids(plain_source_tree):
|
|
147
|
+
if FUNCTIONAL_REQUIREMENTS in plain_source_tree:
|
|
148
|
+
for functional_requirement_id in range(1, len(plain_source_tree[FUNCTIONAL_REQUIREMENTS]) + 1):
|
|
149
|
+
if "ID" in plain_source_tree:
|
|
150
|
+
yield plain_source_tree["ID"] + "." + str(functional_requirement_id)
|
|
151
|
+
else:
|
|
152
|
+
yield str(functional_requirement_id)
|
|
153
|
+
|
|
154
|
+
if "sections" in plain_source_tree:
|
|
155
|
+
for section in plain_source_tree["sections"]:
|
|
156
|
+
yield from get_frids(section)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def get_first_frid(plain_source_tree):
|
|
160
|
+
return next(get_frids(plain_source_tree), None)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def get_current_frid(section_id: Optional[str], functional_requirement_count: int) -> str:
|
|
164
|
+
if section_id is None:
|
|
165
|
+
return str(functional_requirement_count)
|
|
166
|
+
else:
|
|
167
|
+
return section_id + "." + str(functional_requirement_count)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def get_next_frid(plain_source_tree, frid):
|
|
171
|
+
functional_requirements = get_frids(plain_source_tree)
|
|
172
|
+
for temp_frid in functional_requirements:
|
|
173
|
+
if temp_frid == frid:
|
|
174
|
+
return next(functional_requirements, None)
|
|
175
|
+
|
|
176
|
+
raise Exception(f"Functionality {frid} does not exist.")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def get_previous_frid(plain_source_tree, frid):
|
|
180
|
+
previous_frid = None
|
|
181
|
+
for temp_frid in get_frids(plain_source_tree):
|
|
182
|
+
if temp_frid == frid:
|
|
183
|
+
return previous_frid
|
|
184
|
+
|
|
185
|
+
previous_frid = temp_frid
|
|
186
|
+
|
|
187
|
+
raise Exception(f"Functionality {frid} does not exist.")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def get_frids_before(plain_source_tree, target_frid: str) -> list[str]:
|
|
191
|
+
"""
|
|
192
|
+
Get all FRIDs that appear before the target FRID in the specification.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
plain_source_tree: The plain source tree
|
|
196
|
+
target_frid: The FRID to find predecessors for
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
List of FRIDs that appear before target_frid, in order
|
|
200
|
+
"""
|
|
201
|
+
previous_frids = []
|
|
202
|
+
for frid in get_frids(plain_source_tree):
|
|
203
|
+
if frid == target_frid:
|
|
204
|
+
break
|
|
205
|
+
previous_frids.append(frid)
|
|
206
|
+
|
|
207
|
+
return previous_frids
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def get_specification_item_markdown(specification_item, code_variables, replace_code_variables):
|
|
211
|
+
markdown = specification_item["markdown"]
|
|
212
|
+
if "code_variables" in specification_item:
|
|
213
|
+
for code_variable in specification_item["code_variables"]:
|
|
214
|
+
if code_variable["name"] in code_variables:
|
|
215
|
+
if code_variables[code_variable["name"]] != code_variable["value"]:
|
|
216
|
+
raise Exception(
|
|
217
|
+
f"Code variable {code_variable['name']} has multiple values: {code_variables[code_variable['name']]} and {code_variable['value']}"
|
|
218
|
+
)
|
|
219
|
+
else:
|
|
220
|
+
code_variables[code_variable["name"]] = code_variable["value"]
|
|
221
|
+
|
|
222
|
+
if replace_code_variables:
|
|
223
|
+
markdown = markdown.replace(f"{{{{ {code_variable['name']} }}}}", code_variable["value"])
|
|
224
|
+
|
|
225
|
+
return markdown
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def get_specifications_from_plain_source_tree(
|
|
229
|
+
frid,
|
|
230
|
+
plain_source_tree,
|
|
231
|
+
definitions,
|
|
232
|
+
non_functional_requirements,
|
|
233
|
+
test_requirements,
|
|
234
|
+
functional_requirements,
|
|
235
|
+
acceptance_tests,
|
|
236
|
+
code_variables,
|
|
237
|
+
replace_code_variables,
|
|
238
|
+
section_id=None,
|
|
239
|
+
):
|
|
240
|
+
return_frid = None
|
|
241
|
+
if FUNCTIONAL_REQUIREMENTS in plain_source_tree and len(plain_source_tree[FUNCTIONAL_REQUIREMENTS]) > 0:
|
|
242
|
+
functional_requirement_count = 0
|
|
243
|
+
for functional_requirement in plain_source_tree[FUNCTIONAL_REQUIREMENTS]:
|
|
244
|
+
functional_requirement_count += 1
|
|
245
|
+
if section_id is None:
|
|
246
|
+
current_frid = str(functional_requirement_count)
|
|
247
|
+
else:
|
|
248
|
+
current_frid = section_id + "." + str(functional_requirement_count)
|
|
249
|
+
|
|
250
|
+
functional_requirements.append(
|
|
251
|
+
get_specification_item_markdown(functional_requirement, code_variables, replace_code_variables)
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
if current_frid == frid:
|
|
255
|
+
if ACCEPTANCE_TESTS in functional_requirement:
|
|
256
|
+
for acceptance_test in functional_requirement[ACCEPTANCE_TESTS]:
|
|
257
|
+
acceptance_tests.append(
|
|
258
|
+
get_specification_item_markdown(acceptance_test, code_variables, replace_code_variables)
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return_frid = current_frid
|
|
262
|
+
break
|
|
263
|
+
|
|
264
|
+
if "sections" in plain_source_tree:
|
|
265
|
+
for section in plain_source_tree["sections"]:
|
|
266
|
+
sub_frid = get_specifications_from_plain_source_tree(
|
|
267
|
+
frid,
|
|
268
|
+
section,
|
|
269
|
+
definitions,
|
|
270
|
+
non_functional_requirements,
|
|
271
|
+
test_requirements,
|
|
272
|
+
functional_requirements,
|
|
273
|
+
acceptance_tests,
|
|
274
|
+
code_variables,
|
|
275
|
+
replace_code_variables,
|
|
276
|
+
section["ID"],
|
|
277
|
+
)
|
|
278
|
+
if sub_frid is not None:
|
|
279
|
+
return_frid = sub_frid
|
|
280
|
+
break
|
|
281
|
+
|
|
282
|
+
if return_frid is not None:
|
|
283
|
+
if DEFINITIONS in plain_source_tree and plain_source_tree[DEFINITIONS] is not None:
|
|
284
|
+
definitions[0:0] = [
|
|
285
|
+
get_specification_item_markdown(specification, code_variables, replace_code_variables)
|
|
286
|
+
for specification in plain_source_tree[DEFINITIONS]
|
|
287
|
+
]
|
|
288
|
+
if (
|
|
289
|
+
NON_FUNCTIONAL_REQUIREMENTS in plain_source_tree
|
|
290
|
+
and plain_source_tree[NON_FUNCTIONAL_REQUIREMENTS] is not None
|
|
291
|
+
):
|
|
292
|
+
non_functional_requirements[0:0] = [
|
|
293
|
+
get_specification_item_markdown(specification, code_variables, replace_code_variables)
|
|
294
|
+
for specification in plain_source_tree[NON_FUNCTIONAL_REQUIREMENTS]
|
|
295
|
+
]
|
|
296
|
+
if TEST_REQUIREMENTS in plain_source_tree and plain_source_tree[TEST_REQUIREMENTS] is not None:
|
|
297
|
+
test_requirements[0:0] = [
|
|
298
|
+
get_specification_item_markdown(specification, code_variables, replace_code_variables)
|
|
299
|
+
for specification in plain_source_tree[TEST_REQUIREMENTS]
|
|
300
|
+
]
|
|
301
|
+
|
|
302
|
+
return return_frid
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def get_specifications_for_frid(plain_source_tree, frid, replace_code_variables=True):
|
|
306
|
+
definitions = []
|
|
307
|
+
non_functional_requirements = []
|
|
308
|
+
test_requirements = []
|
|
309
|
+
functional_requirements = []
|
|
310
|
+
acceptance_tests = []
|
|
311
|
+
|
|
312
|
+
code_variables = {}
|
|
313
|
+
|
|
314
|
+
result = get_specifications_from_plain_source_tree(
|
|
315
|
+
frid,
|
|
316
|
+
plain_source_tree,
|
|
317
|
+
definitions,
|
|
318
|
+
non_functional_requirements,
|
|
319
|
+
test_requirements,
|
|
320
|
+
functional_requirements,
|
|
321
|
+
acceptance_tests,
|
|
322
|
+
code_variables,
|
|
323
|
+
replace_code_variables,
|
|
324
|
+
)
|
|
325
|
+
if result is None:
|
|
326
|
+
raise Exception(f"Functionality {frid} does not exist.")
|
|
327
|
+
|
|
328
|
+
specifications = {
|
|
329
|
+
DEFINITIONS: definitions,
|
|
330
|
+
NON_FUNCTIONAL_REQUIREMENTS: non_functional_requirements,
|
|
331
|
+
TEST_REQUIREMENTS: test_requirements,
|
|
332
|
+
FUNCTIONAL_REQUIREMENTS: functional_requirements,
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if acceptance_tests:
|
|
336
|
+
specifications[ACCEPTANCE_TESTS] = acceptance_tests
|
|
337
|
+
|
|
338
|
+
if code_variables:
|
|
339
|
+
return specifications, code_variables
|
|
340
|
+
else:
|
|
341
|
+
return specifications, None
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
@with_context
|
|
345
|
+
def code_variable_liquid_filter(value, *, context):
|
|
346
|
+
if len(context.scope) == 0:
|
|
347
|
+
raise Exception("Invalid use of code_variable filter!")
|
|
348
|
+
|
|
349
|
+
if "code_variables" in context.globals:
|
|
350
|
+
code_variables = context.globals["code_variables"]
|
|
351
|
+
|
|
352
|
+
variable = next(iter(context.scope.items()))
|
|
353
|
+
|
|
354
|
+
unique_str = uuid.uuid4().hex
|
|
355
|
+
|
|
356
|
+
code_variables[unique_str] = {variable[0]: value}
|
|
357
|
+
|
|
358
|
+
return unique_str
|
|
359
|
+
else:
|
|
360
|
+
|
|
361
|
+
return value
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
@with_context
|
|
365
|
+
def prohibited_chars_liquid_filter(value, prohibited_chars, *, context):
|
|
366
|
+
if not isinstance(value, str):
|
|
367
|
+
value = str(value)
|
|
368
|
+
|
|
369
|
+
if len(context.scope) == 0:
|
|
370
|
+
raise Exception("Invalid use of prohibited_chars filter!")
|
|
371
|
+
|
|
372
|
+
variable = next(iter(context.scope.items()))
|
|
373
|
+
variable_name = variable[0]
|
|
374
|
+
|
|
375
|
+
for char in prohibited_chars:
|
|
376
|
+
if char in value:
|
|
377
|
+
raise InvalidLiquidVariableName(
|
|
378
|
+
f"'{char}' is not a valid character for variable '{variable_name}' (value: '{value}')."
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
return value
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def hash_text(text):
|
|
385
|
+
return hashlib.sha256(text.encode()).hexdigest()
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def get_hash_value(specifications):
|
|
389
|
+
return hash_text(json.dumps(specifications, indent=4))
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def get_frids_range(plain_source, start, end=None):
|
|
393
|
+
"""Return the FRIDs from ``start`` through ``end`` inclusive, or through the last FRID when ``end`` is None."""
|
|
394
|
+
frids = list(get_frids(plain_source))
|
|
395
|
+
|
|
396
|
+
start = str(start)
|
|
397
|
+
|
|
398
|
+
if start not in frids:
|
|
399
|
+
raise InvalidFridArgument(f"Invalid start functionality ID: {start}. Valid IDs are: {frids}.")
|
|
400
|
+
|
|
401
|
+
if end is not None:
|
|
402
|
+
end = str(end)
|
|
403
|
+
if end not in frids:
|
|
404
|
+
raise InvalidFridArgument(f"Invalid end functionality ID: {end}. Valid IDs are: {frids}.")
|
|
405
|
+
|
|
406
|
+
end_idx = frids.index(end) + 1
|
|
407
|
+
else:
|
|
408
|
+
end_idx = len(frids)
|
|
409
|
+
|
|
410
|
+
start_idx = frids.index(start)
|
|
411
|
+
if start_idx >= end_idx:
|
|
412
|
+
raise InvalidFridArgument(f"Start functionality ID: {start} must be before end functionality ID: {end}.")
|
|
413
|
+
|
|
414
|
+
return frids[start_idx:end_idx]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: plain-parser
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Parser for ***plain specification files
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Classifier: Intended Audience :: Developers
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Topic :: Software Development :: Compilers
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Requires-Dist: mistletoe<2,>=1.6.0
|
|
13
|
+
Requires-Dist: networkx<4,>=3.6.1
|
|
14
|
+
Requires-Dist: python-frontmatter<2,>=1.3.0
|
|
15
|
+
Requires-Dist: python-liquid2<0.5,>=0.4.0
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: black==26.5.1; extra == 'dev'
|
|
18
|
+
Requires-Dist: flake8==7.3.0; extra == 'dev'
|
|
19
|
+
Requires-Dist: isort==8.0.1; extra == 'dev'
|
|
20
|
+
Requires-Dist: mypy==2.3.0; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest==9.1.1; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# plain-parser
|
|
25
|
+
|
|
26
|
+
Python parser for [`***plain`](https://www.plainlang.org/docs/) specification files.
|
|
27
|
+
|
|
28
|
+
Reads a `.plain` module and its `import` / `requires` chain, resolves Liquid templates,
|
|
29
|
+
validates concepts and linked resources, and returns the marshalled specification tree
|
|
30
|
+
plus the ordered list of functionalities (FRIDs).
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install plain-parser
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from plain_parser import plain_file_parser
|
|
42
|
+
|
|
43
|
+
module_name, plain_source_tree, required_modules = plain_file_parser("my_spec.plain", ["."])
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`plain_source_tree` is a dict keyed by section (`definitions`, `implementation reqs`,
|
|
47
|
+
`test reqs`, `functional specs`), each a list of `{"markdown": ..., "linked_resources": [...]}`
|
|
48
|
+
entries. `plain_parser.plain_spec` has helpers for walking functionalities
|
|
49
|
+
(`get_frids`, `get_specifications_for_frid`, `collect_linked_resources`).
|
|
50
|
+
|
|
51
|
+
## Development
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uv venv --python 3.11 && source .venv/bin/activate
|
|
55
|
+
uv pip install -e ".[dev]"
|
|
56
|
+
pytest
|
|
57
|
+
black . --check && isort . --check-only && flake8 . && mypy src
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
plain_parser/__init__.py,sha256=r9ld0v4MDuoNA4pv2vY9qEAVcwo6QWS6f5AkYeHdJFQ,675
|
|
2
|
+
plain_parser/concept_utils.py,sha256=BJzddKxrwg2KbV-58R74ihIJKcv-CGA4B5NZyhOixQ8,8100
|
|
3
|
+
plain_parser/exceptions.py,sha256=WcHK5g9K6bsE-ZZBKWl0lZtiMTQ6fif8-zdEx9qjmRw,795
|
|
4
|
+
plain_parser/liquid_nodes.py,sha256=CYmujdPJMK9hLjfFPmnSoKL3Ps4sb-2OOhH-fZUssis,3917
|
|
5
|
+
plain_parser/loaders.py,sha256=5drZ7OOPJQjEitjWL5W5NfJ0CUatxKHU1UOwm_xhKKE,3850
|
|
6
|
+
plain_parser/plain_file.py,sha256=XzLH59y8_Ffv_PzrReSnBkoKF1WIvIyZrZAgOY55zfQ,37104
|
|
7
|
+
plain_parser/plain_spec.py,sha256=31mLXDldi9zOXT9aK3QhD2mbxbph4ArFU_qgWYd2nwc,15309
|
|
8
|
+
plain_parser-0.1.0.dist-info/METADATA,sha256=tY9e9zRf_YpfhtAM4OT25RaRT6Rne5VnimR9Av4mOSE,1899
|
|
9
|
+
plain_parser-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
10
|
+
plain_parser-0.1.0.dist-info/licenses/LICENSE,sha256=QClWKncxgRjNUVOW8Axi7zK9HtsMdJEtUSMA4LEe8fo,1071
|
|
11
|
+
plain_parser-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Codeplain Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|