ceos-ard-cli 0.3.0__tar.gz → 0.5.0__tar.gz
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.
- {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/.editorconfig +2 -1
- {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/PKG-INFO +16 -8
- {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/README.md +10 -3
- {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/ceos_ard_cli/__init__.py +12 -15
- {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/ceos_ard_cli/compile.py +138 -61
- ceos_ard_cli-0.5.0/ceos_ard_cli/schema.py +172 -0
- {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/ceos_ard_cli/strictyaml/id_reference.py +4 -12
- {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/ceos_ard_cli/strictyaml/markdown.py +0 -3
- {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/ceos_ard_cli/utils/files.py +4 -0
- {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/ceos_ard_cli/validate.py +25 -3
- ceos_ard_cli-0.5.0/ceos_ard_cli/version.py +1 -0
- {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/pixi.lock +692 -588
- {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/pyproject.toml +7 -4
- ceos_ard_cli-0.3.0/ceos_ard_cli/schema.py +0 -114
- ceos_ard_cli-0.3.0/ceos_ard_cli/strictyaml/md_reference.py +0 -25
- ceos_ard_cli-0.3.0/ceos_ard_cli/version.py +0 -1
- {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/.github/workflows/checks.yaml +0 -0
- {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/.github/workflows/publish.yml +0 -0
- {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/.gitignore +0 -0
- {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/.pre-commit-config.yaml +0 -0
- {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/LICENSE +0 -0
- {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/ceos_ard_cli/generate.py +0 -0
- {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/ceos_ard_cli/strictyaml/__init__.py +0 -0
- {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/ceos_ard_cli/utils/__init__.py +0 -0
- {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/ceos_ard_cli/utils/pfs.py +0 -0
- {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/ceos_ard_cli/utils/requirement.py +0 -0
- {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/ceos_ard_cli/utils/template.py +0 -0
- {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/ceos_ard_cli/utils/yaml.py +0 -0
- {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/tests/__init__.py +0 -0
- {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/tests/test_cli.py +0 -0
|
@@ -9,6 +9,7 @@ end_of_line = lf
|
|
|
9
9
|
insert_final_newline = true
|
|
10
10
|
trim_trailing_whitespace = true
|
|
11
11
|
charset = utf-8
|
|
12
|
+
max_line_length = 120
|
|
12
13
|
|
|
13
14
|
# 4 space indentation
|
|
14
15
|
[*.py]
|
|
@@ -23,4 +24,4 @@ indent_size = 2
|
|
|
23
24
|
[*.{md,adoc}]
|
|
24
25
|
trim_trailing_whitespace = false
|
|
25
26
|
indent_style = space
|
|
26
|
-
indent_size = 2
|
|
27
|
+
indent_size = 2
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ceos-ard-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: CLI tools for CEOS-ARD to generate and validate PFS documents
|
|
5
5
|
Project-URL: Homepage, https://github.com/ceos-org/ceos-ard-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/ceos-org/ceos-ard-cli
|
|
@@ -13,10 +13,11 @@ Classifier: License :: OSI Approved :: Apache Software License
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3
|
|
14
14
|
Requires-Python: >=3.9
|
|
15
15
|
Requires-Dist: bibtexparser==2.0.0b8
|
|
16
|
-
Requires-Dist: click
|
|
17
|
-
Requires-Dist: jinja2
|
|
18
|
-
Requires-Dist:
|
|
19
|
-
Requires-Dist:
|
|
16
|
+
Requires-Dist: click<9,>=8.0.0
|
|
17
|
+
Requires-Dist: jinja2<4,>=3.1.0
|
|
18
|
+
Requires-Dist: mergedeep<2,>=1.3.4
|
|
19
|
+
Requires-Dist: playwright<2,>=1.50.0
|
|
20
|
+
Requires-Dist: strictyaml<2,>=1.7.0
|
|
20
21
|
Provides-Extra: dev
|
|
21
22
|
Requires-Dist: build; extra == 'dev'
|
|
22
23
|
Requires-Dist: pre-commit; extra == 'dev'
|
|
@@ -42,12 +43,19 @@ CLI for working with the [CEOS-ARD building blocks and PFSes](https://github.com
|
|
|
42
43
|
|
|
43
44
|
In order to make working with CEOS-ARD easier we have developed command-line interface (CLI) tools.
|
|
44
45
|
|
|
46
|
+
> [!TIP]
|
|
47
|
+
> Most people won't need to install this tool.
|
|
48
|
+
> Instead, you can use the [CEOS-ARD Editor](https://editor.ceos-ard.moregeo.it)
|
|
49
|
+
> to edit and/or generate the CEOS-ARD PFS documents.
|
|
50
|
+
|
|
45
51
|
### Installation
|
|
46
52
|
|
|
47
53
|
[Pixi](https://pixi.sh/) is a modern package management tool that handles both conda and PyPI dependencies.
|
|
48
54
|
|
|
49
|
-
1. Install Pixi by following the [installation instructions](https://pixi.sh/latest/#installation)
|
|
50
|
-
2. Clone this repository
|
|
55
|
+
1. Install Pixi by following the [installation instructions](https://pixi.sh/latest/#installation) (don't follow the "Get Started" instructions, you only need to run the single command provided in the "Installation" section).
|
|
56
|
+
2. Clone this repository and the ceos-ard repository:
|
|
57
|
+
- `git clone https://github.com/ceos-org/ceos-ard`
|
|
58
|
+
- `git clone https://github.com/ceos-org/ceos-ard-cli`
|
|
51
59
|
3. Navigate to the directory: `cd ceos-ard-cli`
|
|
52
60
|
4. Install dependencies: `pixi install`
|
|
53
61
|
5. Install browser for PDF rendering: `pixi run install-browser`
|
|
@@ -55,7 +63,7 @@ In order to make working with CEOS-ARD easier we have developed command-line int
|
|
|
55
63
|
### Run the CLI
|
|
56
64
|
|
|
57
65
|
1. Run `pixi shell`
|
|
58
|
-
2. Switch into the folder that contains the contents of the `ceos-ard` repository
|
|
66
|
+
2. Switch into the folder that contains the contents of the `ceos-ard` repository: `cd ../ceos-ard`
|
|
59
67
|
3. Run `ceos-ard` with the command you want to execute, e.g. `ceos-ard generate-all -o build`
|
|
60
68
|
|
|
61
69
|
See the [available commands](#commands) for further details.
|
|
@@ -16,12 +16,19 @@ CLI for working with the [CEOS-ARD building blocks and PFSes](https://github.com
|
|
|
16
16
|
|
|
17
17
|
In order to make working with CEOS-ARD easier we have developed command-line interface (CLI) tools.
|
|
18
18
|
|
|
19
|
+
> [!TIP]
|
|
20
|
+
> Most people won't need to install this tool.
|
|
21
|
+
> Instead, you can use the [CEOS-ARD Editor](https://editor.ceos-ard.moregeo.it)
|
|
22
|
+
> to edit and/or generate the CEOS-ARD PFS documents.
|
|
23
|
+
|
|
19
24
|
### Installation
|
|
20
25
|
|
|
21
26
|
[Pixi](https://pixi.sh/) is a modern package management tool that handles both conda and PyPI dependencies.
|
|
22
27
|
|
|
23
|
-
1. Install Pixi by following the [installation instructions](https://pixi.sh/latest/#installation)
|
|
24
|
-
2. Clone this repository
|
|
28
|
+
1. Install Pixi by following the [installation instructions](https://pixi.sh/latest/#installation) (don't follow the "Get Started" instructions, you only need to run the single command provided in the "Installation" section).
|
|
29
|
+
2. Clone this repository and the ceos-ard repository:
|
|
30
|
+
- `git clone https://github.com/ceos-org/ceos-ard`
|
|
31
|
+
- `git clone https://github.com/ceos-org/ceos-ard-cli`
|
|
25
32
|
3. Navigate to the directory: `cd ceos-ard-cli`
|
|
26
33
|
4. Install dependencies: `pixi install`
|
|
27
34
|
5. Install browser for PDF rendering: `pixi run install-browser`
|
|
@@ -29,7 +36,7 @@ In order to make working with CEOS-ARD easier we have developed command-line int
|
|
|
29
36
|
### Run the CLI
|
|
30
37
|
|
|
31
38
|
1. Run `pixi shell`
|
|
32
|
-
2. Switch into the folder that contains the contents of the `ceos-ard` repository
|
|
39
|
+
2. Switch into the folder that contains the contents of the `ceos-ard` repository: `cd ../ceos-ard`
|
|
33
40
|
3. Run `ceos-ard` with the command you want to execute, e.g. `ceos-ard generate-all -o build`
|
|
34
41
|
|
|
35
42
|
See the [available commands](#commands) for further details.
|
|
@@ -40,12 +40,12 @@ def cli():
|
|
|
40
40
|
help="Adds an 'Assessment' section to the requirements (for editable Word documents)",
|
|
41
41
|
)
|
|
42
42
|
@click.option(
|
|
43
|
-
"--
|
|
43
|
+
"--debug",
|
|
44
44
|
is_flag=True,
|
|
45
45
|
default=False,
|
|
46
|
-
help="
|
|
46
|
+
help="Enables debugging mode, e.g. outputs a JSON file for debugging purposes and gives a stacktrace",
|
|
47
47
|
)
|
|
48
|
-
def compile(pfs, output, input_dir, editable,
|
|
48
|
+
def compile(pfs, output, input_dir, editable, debug):
|
|
49
49
|
"""
|
|
50
50
|
Compiles the Markdown file for the given PFS.
|
|
51
51
|
"""
|
|
@@ -56,10 +56,13 @@ def compile(pfs, output, input_dir, editable, json):
|
|
|
56
56
|
output = "-".join(pfs)
|
|
57
57
|
|
|
58
58
|
try:
|
|
59
|
-
compile_(pfs, output, input_dir, editable=editable, debug=
|
|
59
|
+
compile_(pfs, output, input_dir, editable=editable, debug=debug)
|
|
60
60
|
except Exception as e:
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
if debug:
|
|
62
|
+
raise e
|
|
63
|
+
else:
|
|
64
|
+
print(e)
|
|
65
|
+
sys.exit(1)
|
|
63
66
|
|
|
64
67
|
|
|
65
68
|
@click.command()
|
|
@@ -84,9 +87,7 @@ def compile(pfs, output, input_dir, editable, json):
|
|
|
84
87
|
help="Generate self-contained HTML files",
|
|
85
88
|
)
|
|
86
89
|
@click.option("--pdf", is_flag=True, default=True, help="Enable/disable PDF generation")
|
|
87
|
-
@click.option(
|
|
88
|
-
"--docx", is_flag=True, default=True, help="Enable/disable Word (docx) generation"
|
|
89
|
-
)
|
|
90
|
+
@click.option("--docx", is_flag=True, default=True, help="Enable/disable Word (docx) generation")
|
|
90
91
|
@click.option(
|
|
91
92
|
"--id",
|
|
92
93
|
default=None,
|
|
@@ -107,9 +108,7 @@ def compile(pfs, output, input_dir, editable, json):
|
|
|
107
108
|
default=None,
|
|
108
109
|
help="Overrides the PFS type of the document",
|
|
109
110
|
)
|
|
110
|
-
def generate(
|
|
111
|
-
pfs, output, input_dir, self_contained, pdf, docx, id, title, version, pfs_type
|
|
112
|
-
):
|
|
111
|
+
def generate(pfs, output, input_dir, self_contained, pdf, docx, id, title, version, pfs_type):
|
|
113
112
|
"""
|
|
114
113
|
Generates the Word and HTML files for the given PFS.
|
|
115
114
|
|
|
@@ -156,9 +155,7 @@ def generate(
|
|
|
156
155
|
help="Generate self-contained HTML files",
|
|
157
156
|
)
|
|
158
157
|
@click.option("--pdf", is_flag=True, default=True, help="Enable/disable PDF generation")
|
|
159
|
-
@click.option(
|
|
160
|
-
"--docx", is_flag=True, default=True, help="Enable/disable Word (docx) generation"
|
|
161
|
-
)
|
|
158
|
+
@click.option("--docx", is_flag=True, default=True, help="Enable/disable Word (docx) generation")
|
|
162
159
|
@click.option(
|
|
163
160
|
"--pfs",
|
|
164
161
|
"-p",
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import shutil
|
|
2
|
+
from collections import defaultdict
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
from typing import Union
|
|
4
|
-
from collections import defaultdict
|
|
5
5
|
|
|
6
|
-
from .schema import REFERENCE_PATH
|
|
7
|
-
from .utils.files import read_file, write_file
|
|
6
|
+
from .schema import REFERENCE_PATH, get_empty_requirement_part
|
|
7
|
+
from .utils.files import fix_path, read_file, write_file
|
|
8
8
|
from .utils.pfs import read_pfs
|
|
9
9
|
from .utils.requirement import slugify
|
|
10
10
|
from .utils.template import read_template
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def topological_sort_requirements(
|
|
14
|
-
requirements_by_pfs: list[list[str]],
|
|
15
|
-
equivalence_groups: dict[str, str] = None
|
|
14
|
+
requirements_by_pfs: list[list[str]], equivalence_groups: dict[str, str] = None
|
|
16
15
|
) -> list[str]:
|
|
17
16
|
"""
|
|
18
17
|
Topologically sort requirements while preserving relative order from each PFS document.
|
|
@@ -88,6 +87,7 @@ def topological_sort_requirements(
|
|
|
88
87
|
in_degree[successor] -= 1
|
|
89
88
|
if in_degree[successor] == 0:
|
|
90
89
|
import bisect
|
|
90
|
+
|
|
91
91
|
bisect.insort(queue, successor)
|
|
92
92
|
|
|
93
93
|
# Check for cycles
|
|
@@ -158,7 +158,7 @@ def combine_pfs(multi_pfs):
|
|
|
158
158
|
"glossary": {},
|
|
159
159
|
"references": set(),
|
|
160
160
|
"annexes": {},
|
|
161
|
-
"authors":
|
|
161
|
+
"authors": "",
|
|
162
162
|
"requirements": [],
|
|
163
163
|
}
|
|
164
164
|
categories = {}
|
|
@@ -179,16 +179,6 @@ def combine_pfs(multi_pfs):
|
|
|
179
179
|
data["references"].update(pfs["references"])
|
|
180
180
|
data["annexes"] = to_id_dict(pfs["annexes"])
|
|
181
181
|
|
|
182
|
-
for organization in pfs["authors"]:
|
|
183
|
-
key = organization["name"] + organization.get("country", "")
|
|
184
|
-
if key not in data["authors"]:
|
|
185
|
-
data["authors"][key] = organization
|
|
186
|
-
else:
|
|
187
|
-
existing = data["authors"][key]["members"]
|
|
188
|
-
for member in organization["members"]:
|
|
189
|
-
if member not in existing:
|
|
190
|
-
existing.append(member)
|
|
191
|
-
|
|
192
182
|
for block in pfs["requirements"]:
|
|
193
183
|
cat_id = block["category"]["id"]
|
|
194
184
|
if cat_id not in categories:
|
|
@@ -230,16 +220,12 @@ def combine_pfs(multi_pfs):
|
|
|
230
220
|
data["glossary"] = list(data["glossary"].values())
|
|
231
221
|
data["references"] = list(data["references"])
|
|
232
222
|
data["annexes"] = list(data["annexes"].values())
|
|
233
|
-
data["authors"] = list(data["authors"].values())
|
|
234
223
|
|
|
235
224
|
for value in categories.values():
|
|
236
225
|
cat_id = value["id"]
|
|
237
226
|
# Use topological sort to merge requirements from all PFS documents
|
|
238
227
|
# Pass equivalence groups so requirements with same title stay together
|
|
239
|
-
sorted_req_ids = topological_sort_requirements(
|
|
240
|
-
requirement_orders[cat_id],
|
|
241
|
-
equivalence_groups.get(cat_id, {})
|
|
242
|
-
)
|
|
228
|
+
sorted_req_ids = topological_sort_requirements(requirement_orders[cat_id], equivalence_groups.get(cat_id, {}))
|
|
243
229
|
sorted_requirements = [requirements[cat_id][req_id] for req_id in sorted_req_ids]
|
|
244
230
|
|
|
245
231
|
data["requirements"].append(
|
|
@@ -252,6 +238,58 @@ def combine_pfs(multi_pfs):
|
|
|
252
238
|
return data
|
|
253
239
|
|
|
254
240
|
|
|
241
|
+
def deep_replace(base, overrides):
|
|
242
|
+
"""Deep merge where override values replace base values."""
|
|
243
|
+
result = base.copy()
|
|
244
|
+
for key, value in overrides.items():
|
|
245
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
246
|
+
result[key] = deep_replace(result[key], value)
|
|
247
|
+
else:
|
|
248
|
+
result[key] = value
|
|
249
|
+
return result
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def deep_append(base, additions):
|
|
253
|
+
"""Deep merge where addition values are appended to base values."""
|
|
254
|
+
result = base.copy()
|
|
255
|
+
for key, value in additions.items():
|
|
256
|
+
if key not in result:
|
|
257
|
+
result[key] = value
|
|
258
|
+
elif isinstance(result[key], str) and isinstance(value, str):
|
|
259
|
+
if len(result[key]) > 0:
|
|
260
|
+
result[key] += "\n\n" + value
|
|
261
|
+
else:
|
|
262
|
+
result[key] = value
|
|
263
|
+
elif isinstance(result[key], list) and isinstance(value, list):
|
|
264
|
+
result[key] = result[key] + value
|
|
265
|
+
elif isinstance(result[key], dict) and isinstance(value, dict):
|
|
266
|
+
result[key] = deep_append(result[key], value)
|
|
267
|
+
else:
|
|
268
|
+
result[key] = value
|
|
269
|
+
return result
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def resolve_ref(item):
|
|
273
|
+
"""Resolve a single ref/replace/append pattern into a flat structure."""
|
|
274
|
+
if isinstance(item, dict) and "ref" in item:
|
|
275
|
+
result = item["ref"]
|
|
276
|
+
if item.get("replace"):
|
|
277
|
+
result = deep_replace(result, item["replace"])
|
|
278
|
+
if item.get("append"):
|
|
279
|
+
result = deep_append(result, item["append"])
|
|
280
|
+
return result
|
|
281
|
+
return item
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def resolve_refs(data):
|
|
285
|
+
"""Resolve all ref/replace/append patterns in PFS data."""
|
|
286
|
+
for block in data["requirements"]:
|
|
287
|
+
block["category"] = resolve_ref(block["category"])
|
|
288
|
+
for idx, req in enumerate(block["requirements"]):
|
|
289
|
+
block["requirements"][idx] = resolve_ref(req)
|
|
290
|
+
return data
|
|
291
|
+
|
|
292
|
+
|
|
255
293
|
def compile(
|
|
256
294
|
pfs: Union[list[str], str],
|
|
257
295
|
out: Union[Path, str],
|
|
@@ -275,15 +313,20 @@ def compile(
|
|
|
275
313
|
|
|
276
314
|
multi_pfs = {}
|
|
277
315
|
for p in pfs:
|
|
278
|
-
# read the PFS information
|
|
316
|
+
# read the PFS information,
|
|
317
|
+
# resolve ref/replace/append patterns and
|
|
279
318
|
# move the glossary and references to the top level
|
|
280
|
-
multi_pfs[p] = bubble_up(read_pfs(p, input_dir))
|
|
319
|
+
multi_pfs[p] = bubble_up(resolve_refs(read_pfs(p, input_dir)))
|
|
281
320
|
|
|
282
321
|
if len(pfs) > 1:
|
|
283
322
|
data = combine_pfs(multi_pfs)
|
|
284
323
|
else:
|
|
285
324
|
data = multi_pfs[pfs[0]]
|
|
286
325
|
|
|
326
|
+
if isinstance(data["authors"], list):
|
|
327
|
+
# Convert list of authors to Markdown
|
|
328
|
+
data["authors"] = "\n".join(map(lambda a: f"- {a}", data["authors"]))
|
|
329
|
+
|
|
287
330
|
# Override metadata if provided
|
|
288
331
|
data["id"] = metadata.get("id") or data["id"]
|
|
289
332
|
data["title"] = metadata.get("title") or data["title"]
|
|
@@ -318,7 +361,34 @@ def compile_bibtex(data, out, input_dir: Path):
|
|
|
318
361
|
|
|
319
362
|
# make uid unique so that it can be used in multiple categories
|
|
320
363
|
def create_uid(block, req_id):
|
|
321
|
-
return slugify(block["category"]["id"] + "
|
|
364
|
+
return slugify(block["category"]["id"] + "-" + req_id)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def path_to_id(filepath, input_dir):
|
|
368
|
+
req_dir = Path(input_dir) / "requirements"
|
|
369
|
+
rel_path = fix_path(Path(filepath).relative_to(req_dir))
|
|
370
|
+
if rel_path.endswith(".yaml"):
|
|
371
|
+
rel_path = rel_path[:-5]
|
|
372
|
+
return rel_path
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _resolve_dep_path(path, cid, local_requirements, all_requirements, input_dir):
|
|
376
|
+
"""Resolve a dependency path to a requirement UID."""
|
|
377
|
+
if path in local_requirements[cid]:
|
|
378
|
+
return local_requirements[cid][path]
|
|
379
|
+
elif path in all_requirements:
|
|
380
|
+
return all_requirements[path]
|
|
381
|
+
else:
|
|
382
|
+
raise ValueError(f"Unmet dependency '{path}' in category '{cid}'")
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def append_requirement(target, req):
|
|
386
|
+
if len(target["description"]) > 0:
|
|
387
|
+
target["description"] += "\n\n" + req["description"]
|
|
388
|
+
else:
|
|
389
|
+
target["description"] = req["description"]
|
|
390
|
+
target["notes"].extend(req["notes"])
|
|
391
|
+
target["metadata"].update(req["metadata"])
|
|
322
392
|
|
|
323
393
|
|
|
324
394
|
def compile_markdown(data, out, editable, input_dir: Path):
|
|
@@ -329,14 +399,32 @@ def compile_markdown(data, out, editable, input_dir: Path):
|
|
|
329
399
|
context["editable"] = editable
|
|
330
400
|
# sort glossary
|
|
331
401
|
context["glossary"] = sorted(context["glossary"], key=lambda x: x["term"].lower())
|
|
332
|
-
# todo:
|
|
333
|
-
# todo: alternatively, add changelog to the individual files with a timestamp and compile it from there
|
|
334
|
-
context["history"] = "Not available yet"
|
|
402
|
+
# todo: Derive changelogs automatically
|
|
335
403
|
|
|
336
404
|
# make a dict of all requirements for efficient dependency lookup
|
|
337
405
|
all_requirements = {}
|
|
338
406
|
# make a dict of the requirements in this category for efficient dependency lookup
|
|
339
407
|
local_requirements = {}
|
|
408
|
+
|
|
409
|
+
# Merge individual requirements into goal and threshold requirements
|
|
410
|
+
for block in context["requirements"]:
|
|
411
|
+
for idx, req in enumerate(block["requirements"]):
|
|
412
|
+
threshold = None
|
|
413
|
+
goal = None
|
|
414
|
+
for reqname in req["requirements"]:
|
|
415
|
+
subreq = req["requirements"][reqname]
|
|
416
|
+
if subreq.get("optional", False):
|
|
417
|
+
if goal is None:
|
|
418
|
+
goal = get_empty_requirement_part()
|
|
419
|
+
append_requirement(goal, subreq)
|
|
420
|
+
else:
|
|
421
|
+
if threshold is None:
|
|
422
|
+
threshold = get_empty_requirement_part()
|
|
423
|
+
append_requirement(threshold, subreq)
|
|
424
|
+
|
|
425
|
+
req["threshold"] = threshold
|
|
426
|
+
req["goal"] = goal
|
|
427
|
+
|
|
340
428
|
# generate uid for each requirement and fill dependency lookups
|
|
341
429
|
for block in context["requirements"]:
|
|
342
430
|
cid = block["category"]["id"]
|
|
@@ -344,28 +432,20 @@ def compile_markdown(data, out, editable, input_dir: Path):
|
|
|
344
432
|
for req in block["requirements"]:
|
|
345
433
|
# make uid unique if it can be used in multiple categories
|
|
346
434
|
req["uid"] = create_uid(block, req["id"])
|
|
347
|
-
|
|
348
|
-
|
|
435
|
+
rel_path = path_to_id(req["filepath"], input_dir)
|
|
436
|
+
local_requirements[cid][rel_path] = req["uid"]
|
|
437
|
+
all_requirements[rel_path] = req["uid"]
|
|
349
438
|
|
|
350
439
|
# resolve dependencies
|
|
351
440
|
for block in context["requirements"]:
|
|
352
441
|
cid = block["category"]["id"]
|
|
353
442
|
for req in block["requirements"]:
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
ref_id = all_requirements[id]
|
|
361
|
-
else:
|
|
362
|
-
raise ValueError(
|
|
363
|
-
f"Unmet dependency {id} for requirement {req['uid']}"
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
req["dependencies"][i] = ref_id
|
|
367
|
-
# Update the requirements in the texts
|
|
368
|
-
update_requirement_references(req, id, ref_id)
|
|
443
|
+
resolved_deps = []
|
|
444
|
+
for alias, path in req["dependencies"].items():
|
|
445
|
+
ref_id = _resolve_dep_path(path, cid, local_requirements, all_requirements, input_dir)
|
|
446
|
+
resolved_deps.append(ref_id)
|
|
447
|
+
update_requirement_references(req, alias, ref_id)
|
|
448
|
+
req["dependencies"] = resolved_deps
|
|
369
449
|
|
|
370
450
|
# read, fill and write the template
|
|
371
451
|
template = read_template(input_dir)
|
|
@@ -375,23 +455,20 @@ def compile_markdown(data, out, editable, input_dir: Path):
|
|
|
375
455
|
|
|
376
456
|
# replace all requirement references in the texts with the resolved references
|
|
377
457
|
def update_requirement_references(req, old_id, new_id):
|
|
378
|
-
req["description"] = update_requirement_reference(
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
)
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
req["goal"]["notes"] = update_requirement_reference(
|
|
393
|
-
req["goal"]["notes"], old_id, new_id
|
|
394
|
-
)
|
|
458
|
+
req["description"] = update_requirement_reference(req["description"], old_id, new_id)
|
|
459
|
+
|
|
460
|
+
for key in req["requirements"]:
|
|
461
|
+
update_requirement_part_references(req["requirements"][key], old_id, new_id)
|
|
462
|
+
|
|
463
|
+
if req.get("threshold"):
|
|
464
|
+
update_requirement_part_references(req["threshold"], old_id, new_id)
|
|
465
|
+
if req.get("goal"):
|
|
466
|
+
update_requirement_part_references(req["goal"], old_id, new_id)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def update_requirement_part_references(part, old_id, new_id):
|
|
470
|
+
part["description"] = update_requirement_reference(part["description"], old_id, new_id)
|
|
471
|
+
part["notes"] = update_requirement_reference(part["notes"], old_id, new_id)
|
|
395
472
|
|
|
396
473
|
|
|
397
474
|
# replace all requirement references in the given texts with the resolved references
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from strictyaml import (
|
|
2
|
+
Bool,
|
|
3
|
+
EmptyDict,
|
|
4
|
+
EmptyList,
|
|
5
|
+
Enum,
|
|
6
|
+
Map,
|
|
7
|
+
MapPattern,
|
|
8
|
+
Optional,
|
|
9
|
+
Regex,
|
|
10
|
+
Seq,
|
|
11
|
+
Str,
|
|
12
|
+
UniqueSeq,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from .strictyaml.id_reference import IdReference
|
|
16
|
+
from .strictyaml.markdown import Markdown
|
|
17
|
+
from .utils.files import fix_path
|
|
18
|
+
|
|
19
|
+
REFERENCE_PATH = "./references/{id}.bib"
|
|
20
|
+
GLOSSARY_PATH = "./glossary/{id}.yaml"
|
|
21
|
+
INTRODUCTION_PATH = "./sections/introduction/{id}.yaml"
|
|
22
|
+
ANNEX_PATH = "./sections/annexes/{id}.yaml"
|
|
23
|
+
REQUIREMENT_CATEGORY_PATH = "./sections/requirement-categories/{id}.yaml"
|
|
24
|
+
REQUIREMENT_PATH = "./requirements/{id}.yaml"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_REFS = lambda path, base_path, schema=None, resolve=False: (
|
|
28
|
+
EmptyList() | UniqueSeq(IdReference(path, base_path, schema, resolve))
|
|
29
|
+
)
|
|
30
|
+
_RESOLVED_REFS = lambda path, base_path, schema: _REFS(path, base_path, schema, resolve=True)
|
|
31
|
+
_RESOLVED_SECTIONS = lambda path, base_path: _RESOLVED_REFS(path, base_path, SECTION)
|
|
32
|
+
_REFERENCE_IDS = lambda base_path: _REFS(REFERENCE_PATH, base_path)
|
|
33
|
+
_DEPS = lambda path, base_path, schema=None: (
|
|
34
|
+
EmptyDict() | MapPattern(Str(), IdReference(path, base_path, schema, resolve=False))
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
_REQUIREMENT_PART = Map(
|
|
38
|
+
{
|
|
39
|
+
"description": Markdown(),
|
|
40
|
+
Optional("notes", default=[]): EmptyList() | Seq(Markdown()),
|
|
41
|
+
Optional("metadata", default={}): EmptyDict(), # todo: add metadata schema
|
|
42
|
+
Optional("optional", default=False): Bool(),
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
_REQUIREMENT_PART_OVERRIDE = Map(
|
|
47
|
+
{
|
|
48
|
+
Optional("description"): Markdown(),
|
|
49
|
+
Optional("notes"): EmptyList() | Seq(Markdown()),
|
|
50
|
+
Optional("metadata"): EmptyDict(), # todo: add metadata schema
|
|
51
|
+
Optional("optional"): Bool(),
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_empty_requirement_part():
|
|
57
|
+
return {
|
|
58
|
+
"description": "",
|
|
59
|
+
"notes": [],
|
|
60
|
+
"metadata": {},
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
_CHANGES = EmptyList() | Seq(
|
|
65
|
+
Map(
|
|
66
|
+
{
|
|
67
|
+
"date": Regex(r"\d{4}-\d{2}-\d{2}"), # ISO date format
|
|
68
|
+
"author": Str(),
|
|
69
|
+
"change": Markdown(),
|
|
70
|
+
"reason": Str(),
|
|
71
|
+
"level": Enum(["major", "minor", "patch"]),
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
GLOSSARY = lambda file, base_path: Map(
|
|
77
|
+
{
|
|
78
|
+
Optional("filepath", default=fix_path(file)): Str(),
|
|
79
|
+
"term": Str(),
|
|
80
|
+
"description": Markdown(),
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
_RESOLVED_GLOSSARY = lambda base_path: _RESOLVED_REFS(GLOSSARY_PATH, base_path, GLOSSARY)
|
|
84
|
+
|
|
85
|
+
SECTION = lambda file, base_path: Map(
|
|
86
|
+
{
|
|
87
|
+
Optional("filepath", default=fix_path(file)): Str(),
|
|
88
|
+
Optional("id", default=""): Str(),
|
|
89
|
+
"title": Str(),
|
|
90
|
+
"description": Markdown(),
|
|
91
|
+
Optional("glossary", default=[]): _RESOLVED_GLOSSARY(base_path),
|
|
92
|
+
Optional("references", default=[]): _REFERENCE_IDS(base_path),
|
|
93
|
+
Optional("changes", default=[]): _CHANGES,
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
PARTIAL_SECTION = lambda file, base_path: Map(
|
|
98
|
+
{
|
|
99
|
+
Optional("title"): Str(),
|
|
100
|
+
Optional("description"): Markdown(),
|
|
101
|
+
Optional("glossary"): _RESOLVED_GLOSSARY(base_path),
|
|
102
|
+
Optional("references"): _REFERENCE_IDS(base_path),
|
|
103
|
+
Optional("changes"): _CHANGES,
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
REQUIREMENT = lambda file, base_path: Map(
|
|
108
|
+
{
|
|
109
|
+
Optional("filepath", default=fix_path(file)): Str(),
|
|
110
|
+
"id": Str(),
|
|
111
|
+
"title": Str(),
|
|
112
|
+
Optional("description", default=""): Markdown(),
|
|
113
|
+
"requirements": MapPattern(Str(), _REQUIREMENT_PART),
|
|
114
|
+
Optional("dependencies", default={}): _DEPS(REQUIREMENT_PATH, base_path, REQUIREMENT),
|
|
115
|
+
Optional("glossary", default=[]): _RESOLVED_GLOSSARY(base_path),
|
|
116
|
+
Optional("references", default=[]): _REFERENCE_IDS(base_path),
|
|
117
|
+
Optional("changes", default=[]): _CHANGES,
|
|
118
|
+
Optional("history", default=[]): EmptyList() | Seq(Str()),
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
PARTIAL_REQUIREMENT = lambda file, base_path: Map(
|
|
123
|
+
{
|
|
124
|
+
Optional("title"): Str(),
|
|
125
|
+
Optional("description"): Markdown(),
|
|
126
|
+
Optional("requirements"): MapPattern(Str(), _REQUIREMENT_PART_OVERRIDE),
|
|
127
|
+
Optional("dependencies"): _DEPS(REQUIREMENT_PATH, base_path, REQUIREMENT),
|
|
128
|
+
Optional("glossary"): _RESOLVED_GLOSSARY(base_path),
|
|
129
|
+
Optional("references"): _REFERENCE_IDS(base_path),
|
|
130
|
+
Optional("changes"): _CHANGES,
|
|
131
|
+
Optional("history"): Seq(Str()),
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
PFS_DOCUMENT = lambda file, base_path: Map(
|
|
136
|
+
{
|
|
137
|
+
"title": Str(),
|
|
138
|
+
"version": Str(),
|
|
139
|
+
"type": Str(),
|
|
140
|
+
"applies_to": Markdown(),
|
|
141
|
+
"authors": Markdown() | Seq(Str()),
|
|
142
|
+
Optional("introduction", default=[]): _RESOLVED_SECTIONS(INTRODUCTION_PATH, base_path),
|
|
143
|
+
"requirements": Seq(
|
|
144
|
+
Map(
|
|
145
|
+
{
|
|
146
|
+
"category": Map(
|
|
147
|
+
{
|
|
148
|
+
"ref": IdReference(REQUIREMENT_CATEGORY_PATH, base_path, SECTION),
|
|
149
|
+
Optional("replace", default={}): EmptyDict() | PARTIAL_SECTION(file, base_path),
|
|
150
|
+
Optional("append", default={}): EmptyDict() | PARTIAL_SECTION(file, base_path),
|
|
151
|
+
}
|
|
152
|
+
)
|
|
153
|
+
| IdReference(REQUIREMENT_CATEGORY_PATH, base_path, SECTION),
|
|
154
|
+
"requirements": Seq(
|
|
155
|
+
Map(
|
|
156
|
+
{
|
|
157
|
+
"ref": IdReference(REQUIREMENT_PATH, base_path, REQUIREMENT),
|
|
158
|
+
Optional("replace", default={}): EmptyDict() | PARTIAL_REQUIREMENT(file, base_path),
|
|
159
|
+
Optional("append", default={}): EmptyDict() | PARTIAL_REQUIREMENT(file, base_path),
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
| IdReference(REQUIREMENT_PATH, base_path, REQUIREMENT)
|
|
163
|
+
),
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
),
|
|
167
|
+
Optional("glossary", default=[]): _RESOLVED_GLOSSARY(base_path),
|
|
168
|
+
Optional("references", default=[]): _REFERENCE_IDS(base_path),
|
|
169
|
+
Optional("annexes", default=[]): _RESOLVED_SECTIONS(ANNEX_PATH, base_path),
|
|
170
|
+
Optional("changes", default=[]): _CHANGES,
|
|
171
|
+
}
|
|
172
|
+
)
|
|
@@ -15,14 +15,10 @@ class IdReference(strictyaml.ScalarValidator):
|
|
|
15
15
|
self._resolve = resolve
|
|
16
16
|
|
|
17
17
|
def validate_scalar(self, chunk):
|
|
18
|
-
file = Path(self._base_path) / Path(
|
|
19
|
-
self._path_template.format(id=chunk.contents)
|
|
20
|
-
)
|
|
18
|
+
file = Path(self._base_path) / Path(self._path_template.format(id=chunk.contents))
|
|
21
19
|
content = None
|
|
22
20
|
if not file.exists():
|
|
23
|
-
chunk.expecting_but_found(
|
|
24
|
-
f"expecting an existing file at {file} for id '{chunk.contents}'"
|
|
25
|
-
)
|
|
21
|
+
chunk.expecting_but_found(f"expecting an existing file at {file} for id '{chunk.contents}'")
|
|
26
22
|
elif file.suffix == ".yaml":
|
|
27
23
|
content = read_yaml(file, self._schema, self._base_path)
|
|
28
24
|
if "id" not in content or len(content["id"]) == 0:
|
|
@@ -34,13 +30,9 @@ class IdReference(strictyaml.ScalarValidator):
|
|
|
34
30
|
if len(library.failed_blocks) > 0:
|
|
35
31
|
chunk.expecting_but_found(f"expecting a valid bibtex entry at {file}")
|
|
36
32
|
elif len(library.entries) != 1:
|
|
37
|
-
chunk.expecting_but_found(
|
|
38
|
-
f"expecting a single bibtex entry per file in {file}, found {count}"
|
|
39
|
-
)
|
|
33
|
+
chunk.expecting_but_found(f"expecting a single bibtex entry per file in {file}, found {count}")
|
|
40
34
|
elif library.entries[0].key != file.stem:
|
|
41
|
-
chunk.expecting_but_found(
|
|
42
|
-
f"expecting bibtex identifier to match file name in {file}"
|
|
43
|
-
)
|
|
35
|
+
chunk.expecting_but_found(f"expecting bibtex identifier to match file name in {file}")
|
|
44
36
|
else:
|
|
45
37
|
content = read_file(file)
|
|
46
38
|
|