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.
Files changed (30) hide show
  1. {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/.editorconfig +2 -1
  2. {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/PKG-INFO +16 -8
  3. {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/README.md +10 -3
  4. {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/ceos_ard_cli/__init__.py +12 -15
  5. {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/ceos_ard_cli/compile.py +138 -61
  6. ceos_ard_cli-0.5.0/ceos_ard_cli/schema.py +172 -0
  7. {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/ceos_ard_cli/strictyaml/id_reference.py +4 -12
  8. {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/ceos_ard_cli/strictyaml/markdown.py +0 -3
  9. {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/ceos_ard_cli/utils/files.py +4 -0
  10. {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/ceos_ard_cli/validate.py +25 -3
  11. ceos_ard_cli-0.5.0/ceos_ard_cli/version.py +1 -0
  12. {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/pixi.lock +692 -588
  13. {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/pyproject.toml +7 -4
  14. ceos_ard_cli-0.3.0/ceos_ard_cli/schema.py +0 -114
  15. ceos_ard_cli-0.3.0/ceos_ard_cli/strictyaml/md_reference.py +0 -25
  16. ceos_ard_cli-0.3.0/ceos_ard_cli/version.py +0 -1
  17. {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/.github/workflows/checks.yaml +0 -0
  18. {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/.github/workflows/publish.yml +0 -0
  19. {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/.gitignore +0 -0
  20. {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/.pre-commit-config.yaml +0 -0
  21. {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/LICENSE +0 -0
  22. {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/ceos_ard_cli/generate.py +0 -0
  23. {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/ceos_ard_cli/strictyaml/__init__.py +0 -0
  24. {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/ceos_ard_cli/utils/__init__.py +0 -0
  25. {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/ceos_ard_cli/utils/pfs.py +0 -0
  26. {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/ceos_ard_cli/utils/requirement.py +0 -0
  27. {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/ceos_ard_cli/utils/template.py +0 -0
  28. {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/ceos_ard_cli/utils/yaml.py +0 -0
  29. {ceos_ard_cli-0.3.0 → ceos_ard_cli-0.5.0}/tests/__init__.py +0 -0
  30. {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.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>=8.0.0
17
- Requires-Dist: jinja2>=3.1.0
18
- Requires-Dist: playwright>=1.50.0
19
- Requires-Dist: strictyaml>=1.7.0
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: `git clone https://github.com/ceos-org/ceos-ard-cli`
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: `git clone https://github.com/ceos-org/ceos-ard-cli`
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
- "--json",
43
+ "--debug",
44
44
  is_flag=True,
45
45
  default=False,
46
- help="Outputs a JSON file for debugging purposes",
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, json):
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=json)
59
+ compile_(pfs, output, input_dir, editable=editable, debug=debug)
60
60
  except Exception as e:
61
- print(e)
62
- sys.exit(1)
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 and
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"] + "." + req_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: implement automatic creation of history based on git logs?
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
- local_requirements[cid][req["id"]] = req["uid"]
348
- all_requirements[req["id"]] = req["uid"]
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
- for i, id in enumerate(req["dependencies"]):
355
- # 1. Check to the requirement in the same requirement category.
356
- if id in local_requirements[cid]:
357
- ref_id = local_requirements[cid][id]
358
- # 2. Refers to the requirement in any other category.
359
- elif id in all_requirements:
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
- req["description"], old_id, new_id
380
- )
381
- if req["threshold"] is not None:
382
- req["threshold"]["description"] = update_requirement_reference(
383
- req["threshold"]["description"], old_id, new_id
384
- )
385
- req["threshold"]["notes"] = update_requirement_reference(
386
- req["threshold"]["notes"], old_id, new_id
387
- )
388
- if req["goal"] is not None:
389
- req["goal"]["description"] = update_requirement_reference(
390
- req["goal"]["description"], old_id, new_id
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