Gen3SchemaDev 2.4.0__tar.gz → 2.6.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.
- {gen3schemadev-2.4.0 → gen3schemadev-2.6.0}/PKG-INFO +14 -1
- {gen3schemadev-2.4.0 → gen3schemadev-2.6.0}/README.md +13 -0
- {gen3schemadev-2.4.0 → gen3schemadev-2.6.0}/pyproject.toml +1 -1
- {gen3schemadev-2.4.0 → gen3schemadev-2.6.0}/src/gen3schemadev/cli.py +96 -1
- {gen3schemadev-2.4.0 → gen3schemadev-2.6.0}/src/gen3schemadev/converter.py +18 -8
- gen3schemadev-2.6.0/src/gen3schemadev/refs.py +262 -0
- {gen3schemadev-2.4.0 → gen3schemadev-2.6.0}/src/gen3schemadev/schema/schema_templates/program.yaml +2 -1
- {gen3schemadev-2.4.0 → gen3schemadev-2.6.0}/src/gen3schemadev/schema/schema_templates/project.yaml +4 -2
- {gen3schemadev-2.4.0 → gen3schemadev-2.6.0}/src/gen3schemadev/validators/rule_validator.py +6 -3
- {gen3schemadev-2.4.0 → gen3schemadev-2.6.0}/LICENSE +0 -0
- {gen3schemadev-2.4.0 → gen3schemadev-2.6.0}/src/gen3schemadev/__init__.py +0 -0
- {gen3schemadev-2.4.0 → gen3schemadev-2.6.0}/src/gen3schemadev/ddvis.py +0 -0
- {gen3schemadev-2.4.0 → gen3schemadev-2.6.0}/src/gen3schemadev/schema/__init__.py +0 -0
- {gen3schemadev-2.4.0 → gen3schemadev-2.6.0}/src/gen3schemadev/schema/gen3_template.py +0 -0
- {gen3schemadev-2.4.0 → gen3schemadev-2.6.0}/src/gen3schemadev/schema/input_schema.py +0 -0
- {gen3schemadev-2.4.0 → gen3schemadev-2.6.0}/src/gen3schemadev/schema/schema_templates/_definitions.yaml +0 -0
- {gen3schemadev-2.4.0 → gen3schemadev-2.6.0}/src/gen3schemadev/schema/schema_templates/_settings.yaml +0 -0
- {gen3schemadev-2.4.0 → gen3schemadev-2.6.0}/src/gen3schemadev/schema/schema_templates/_terms.yaml +0 -0
- {gen3schemadev-2.4.0 → gen3schemadev-2.6.0}/src/gen3schemadev/schema/schema_templates/core_metadata_collection.yaml +0 -0
- {gen3schemadev-2.4.0 → gen3schemadev-2.6.0}/src/gen3schemadev/schema/schema_templates/gen3_metaschema.yml +0 -0
- {gen3schemadev-2.4.0 → gen3schemadev-2.6.0}/src/gen3schemadev/utils.py +0 -0
- {gen3schemadev-2.4.0 → gen3schemadev-2.6.0}/src/gen3schemadev/validators/input_validator.py +0 -0
- {gen3schemadev-2.4.0 → gen3schemadev-2.6.0}/src/gen3schemadev/validators/metaschema_validator.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Gen3SchemaDev
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.0
|
|
4
4
|
Summary: Tool for data modelling in Gen3
|
|
5
5
|
License: Apache 2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -35,6 +35,19 @@ This repository aims to provide the documentation, learning materials, and softw
|
|
|
35
35
|
- [Guide to creating your first dictionary](docs/gen3schemadev/first_dictionary.md)
|
|
36
36
|
- [Troubleshooting](docs/gen3schemadev/troubleshooting.md)
|
|
37
37
|
|
|
38
|
+
### Fixing `$ref` properties in an existing dictionary
|
|
39
|
+
|
|
40
|
+
JSON Schema draft-04 (which Gen3 uses) ignores any keyword sitting as a direct sibling of `$ref`, so properties written as `{description: ..., $ref: _definitions.yaml#/X}` lose their description in the data-dictionary viewer. The `fix-refs` command rewrites such properties in place, moving the `$ref` into an `allOf` list so annotations survive resolution:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
gen3schemadev fix-refs -y path/to/dictionary --dry-run # preview changes
|
|
44
|
+
gen3schemadev fix-refs -y path/to/dictionary # rewrite in place
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The command is idempotent and never touches bare refs, refs already inside `allOf`/`anyOf`/`oneOf`, the `properties: {$ref: ...}` merge construct, or `_definitions.yaml`/`_terms.yaml`/`_settings.yaml`.
|
|
48
|
+
|
|
49
|
+
Both `fix-refs` and `validate` also warn about `description: null` placeholders anywhere in the dictionary (commonly in `_definitions.yaml`): the Gen3 metaschema requires descriptions to be strings, and such placeholders fail metaschema validation once exposed through bare or `allOf`-wrapped refs. Remove the null keys to resolve the warning.
|
|
50
|
+
|
|
38
51
|
|
|
39
52
|
## Deep dive into Gen3 Data Modelling
|
|
40
53
|
*Special Thanks to Marion Shadbolt for providing the [source material](https://github.com/AustralianBioCommons/umccr-dictionary/tree/main/docs/schemas)*
|
|
@@ -12,6 +12,19 @@ This repository aims to provide the documentation, learning materials, and softw
|
|
|
12
12
|
- [Guide to creating your first dictionary](docs/gen3schemadev/first_dictionary.md)
|
|
13
13
|
- [Troubleshooting](docs/gen3schemadev/troubleshooting.md)
|
|
14
14
|
|
|
15
|
+
### Fixing `$ref` properties in an existing dictionary
|
|
16
|
+
|
|
17
|
+
JSON Schema draft-04 (which Gen3 uses) ignores any keyword sitting as a direct sibling of `$ref`, so properties written as `{description: ..., $ref: _definitions.yaml#/X}` lose their description in the data-dictionary viewer. The `fix-refs` command rewrites such properties in place, moving the `$ref` into an `allOf` list so annotations survive resolution:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
gen3schemadev fix-refs -y path/to/dictionary --dry-run # preview changes
|
|
21
|
+
gen3schemadev fix-refs -y path/to/dictionary # rewrite in place
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The command is idempotent and never touches bare refs, refs already inside `allOf`/`anyOf`/`oneOf`, the `properties: {$ref: ...}` merge construct, or `_definitions.yaml`/`_terms.yaml`/`_settings.yaml`.
|
|
25
|
+
|
|
26
|
+
Both `fix-refs` and `validate` also warn about `description: null` placeholders anywhere in the dictionary (commonly in `_definitions.yaml`): the Gen3 metaschema requires descriptions to be strings, and such placeholders fail metaschema validation once exposed through bare or `allOf`-wrapped refs. Remove the null keys to resolve the warning.
|
|
27
|
+
|
|
15
28
|
|
|
16
29
|
## Deep dive into Gen3 Data Modelling
|
|
17
30
|
*Special Thanks to Marion Shadbolt for providing the [source material](https://github.com/AustralianBioCommons/umccr-dictionary/tree/main/docs/schemas)*
|
|
@@ -20,6 +20,31 @@ from gen3schemadev.validators.metaschema_validator import validate_schema_with_m
|
|
|
20
20
|
from importlib.metadata import version
|
|
21
21
|
from gen3schemadev.ddvis import visualise_with_docker
|
|
22
22
|
from gen3schemadev.validators.rule_validator import RuleValidator
|
|
23
|
+
from gen3schemadev.refs import (
|
|
24
|
+
WRAPPED,
|
|
25
|
+
SKIP_REASONS,
|
|
26
|
+
fix_yaml_dir,
|
|
27
|
+
find_null_descriptions,
|
|
28
|
+
scan_dir_null_descriptions,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def print_null_description_warning(hits):
|
|
33
|
+
"""
|
|
34
|
+
Print a warning block listing every null-valued 'description' key found
|
|
35
|
+
in the dictionary ("file: dotted.path" strings). No-op when hits is empty.
|
|
36
|
+
"""
|
|
37
|
+
if not hits:
|
|
38
|
+
return
|
|
39
|
+
print(
|
|
40
|
+
"\nWARNING: found 'description: null' placeholders. The Gen3 metaschema "
|
|
41
|
+
"requires 'description' to be a string, and null placeholders in shared "
|
|
42
|
+
"definitions fail metaschema validation once exposed through bare or "
|
|
43
|
+
"allOf-wrapped $refs. Remove the null 'description' keys (removing adds "
|
|
44
|
+
"no shared description, so nothing leaks onto referencing properties):"
|
|
45
|
+
)
|
|
46
|
+
for hit in hits:
|
|
47
|
+
print(f" - {hit}")
|
|
23
48
|
|
|
24
49
|
|
|
25
50
|
def main():
|
|
@@ -142,7 +167,28 @@ def main():
|
|
|
142
167
|
action="store_true",
|
|
143
168
|
help="Set logging level to DEBUG"
|
|
144
169
|
)
|
|
145
|
-
|
|
170
|
+
|
|
171
|
+
# Create 'fix-refs' subcommand
|
|
172
|
+
fixrefs_parser = subparsers.add_parser(
|
|
173
|
+
"fix-refs",
|
|
174
|
+
help="Rewrite properties with '$ref' siblings into draft-04-safe allOf form (in place)"
|
|
175
|
+
)
|
|
176
|
+
fixrefs_parser.add_argument(
|
|
177
|
+
"-y", "--yamldir",
|
|
178
|
+
required=True,
|
|
179
|
+
help="Directory of Gen3 YAML files to rewrite in place (recursive)"
|
|
180
|
+
)
|
|
181
|
+
fixrefs_parser.add_argument(
|
|
182
|
+
"--dry-run",
|
|
183
|
+
action="store_true",
|
|
184
|
+
help="Report what would change without writing files"
|
|
185
|
+
)
|
|
186
|
+
fixrefs_parser.add_argument(
|
|
187
|
+
"--debug",
|
|
188
|
+
action="store_true",
|
|
189
|
+
help="Set logging level to DEBUG"
|
|
190
|
+
)
|
|
191
|
+
|
|
146
192
|
args = parser.parse_args()
|
|
147
193
|
|
|
148
194
|
# Handle case where no command is provided
|
|
@@ -220,6 +266,15 @@ def main():
|
|
|
220
266
|
elif args.yamls:
|
|
221
267
|
schema_dict = bundle_yamls(args.yamls)
|
|
222
268
|
|
|
269
|
+
# Pre-resolution diagnostic: report every null 'description' up front,
|
|
270
|
+
# because the metaschema stage fails on the first resolved node schema,
|
|
271
|
+
# far away from the definition that carries the null.
|
|
272
|
+
null_hits = []
|
|
273
|
+
for schema_name, schema in schema_dict.items():
|
|
274
|
+
for hit in find_null_descriptions(schema):
|
|
275
|
+
null_hits.append(f"{schema_name}: {hit}")
|
|
276
|
+
print_null_description_warning(null_hits)
|
|
277
|
+
|
|
223
278
|
for schema_name, schema in schema_dict.items():
|
|
224
279
|
|
|
225
280
|
if '.' in schema_name:
|
|
@@ -306,5 +361,45 @@ def main():
|
|
|
306
361
|
output_path = args.output or "input_example.yaml"
|
|
307
362
|
write_yaml(init_yaml, output_path)
|
|
308
363
|
|
|
364
|
+
elif args.command == "fix-refs":
|
|
365
|
+
if not os.path.isdir(args.yamldir):
|
|
366
|
+
print(f"ERROR: Directory not found: {args.yamldir}", file=sys.stderr)
|
|
367
|
+
sys.exit(1)
|
|
368
|
+
|
|
369
|
+
if args.dry_run:
|
|
370
|
+
print(f"Dry run: scanning {args.yamldir} (no files will be written)")
|
|
371
|
+
else:
|
|
372
|
+
print(f"Fixing $ref sibling annotations in: {args.yamldir}")
|
|
373
|
+
|
|
374
|
+
reports = fix_yaml_dir(args.yamldir, dry_run=args.dry_run)
|
|
375
|
+
|
|
376
|
+
total_wrapped = 0
|
|
377
|
+
files_changed = 0
|
|
378
|
+
for report in reports:
|
|
379
|
+
print(report["path"])
|
|
380
|
+
if report["skipped_file"]:
|
|
381
|
+
print(f" skipped file ({report['skipped_file']})")
|
|
382
|
+
continue
|
|
383
|
+
if not report["changes"]:
|
|
384
|
+
print(" no $ref properties found")
|
|
385
|
+
continue
|
|
386
|
+
for prop_name, action, preserved in report["changes"]:
|
|
387
|
+
if action == WRAPPED:
|
|
388
|
+
total_wrapped += 1
|
|
389
|
+
print(f" wrapped: {prop_name} (preserved: {', '.join(preserved)})")
|
|
390
|
+
else:
|
|
391
|
+
print(f" skipped ({SKIP_REASONS[action]}): {prop_name}")
|
|
392
|
+
if report["rewritten"] or (args.dry_run and any(c[1] == WRAPPED for c in report["changes"])):
|
|
393
|
+
files_changed += 1
|
|
394
|
+
|
|
395
|
+
files_unchanged = len(reports) - files_changed
|
|
396
|
+
verb = "would be wrapped" if args.dry_run else "wrapped"
|
|
397
|
+
print(f"\nSummary: {total_wrapped} properties {verb} in {files_changed} files; "
|
|
398
|
+
f"{files_unchanged} files unchanged.")
|
|
399
|
+
|
|
400
|
+
# Wrapping is what exposes null 'description' placeholders in shared
|
|
401
|
+
# definitions to metaschema validation, so surface them here too.
|
|
402
|
+
print_null_description_warning(scan_dir_null_descriptions(args.yamldir))
|
|
403
|
+
|
|
309
404
|
if __name__ == "__main__":
|
|
310
405
|
main()
|
|
@@ -17,6 +17,8 @@ from dataclasses import dataclass, asdict
|
|
|
17
17
|
from typing import Protocol, runtime_checkable, Dict, Any, List
|
|
18
18
|
import logging
|
|
19
19
|
|
|
20
|
+
from gen3schemadev.refs import wrap_ref_siblings
|
|
21
|
+
|
|
20
22
|
|
|
21
23
|
logger = logging.getLogger(__name__)
|
|
22
24
|
|
|
@@ -488,7 +490,11 @@ def format_datetime(prop_dict: dict) -> dict:
|
|
|
488
490
|
Formats a property dictionary with a 'type' of 'datetime' to use a $ref
|
|
489
491
|
to the Gen3 _definitions.yaml#/datetime definition.
|
|
490
492
|
|
|
491
|
-
|
|
493
|
+
Annotations such as 'description' are preserved by wrapping the $ref in
|
|
494
|
+
an allOf list: under JSON Schema draft-04, keywords sitting as direct
|
|
495
|
+
siblings of $ref are ignored during resolution, so they must live beside
|
|
496
|
+
allOf instead. A datetime property with no annotations becomes a bare
|
|
497
|
+
$ref. If the property is not of type 'datetime', it is returned unchanged.
|
|
492
498
|
|
|
493
499
|
Example input:
|
|
494
500
|
{
|
|
@@ -501,7 +507,10 @@ def format_datetime(prop_dict: dict) -> dict:
|
|
|
501
507
|
Example output:
|
|
502
508
|
{
|
|
503
509
|
"collection_date": {
|
|
504
|
-
"
|
|
510
|
+
"description": "Date and time of collection (datetime)",
|
|
511
|
+
"allOf": [
|
|
512
|
+
{"$ref": "_definitions.yaml#/datetime"}
|
|
513
|
+
]
|
|
505
514
|
}
|
|
506
515
|
}
|
|
507
516
|
|
|
@@ -518,16 +527,17 @@ def format_datetime(prop_dict: dict) -> dict:
|
|
|
518
527
|
try:
|
|
519
528
|
if len(prop_dict) != 1:
|
|
520
529
|
raise ValueError("Expected a single property dictionary")
|
|
521
|
-
|
|
530
|
+
|
|
522
531
|
first_key = next(iter(prop_dict))
|
|
523
532
|
value = prop_dict[first_key]
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
533
|
+
|
|
534
|
+
if isinstance(value, dict) and value.get('type') == 'datetime':
|
|
535
|
+
ref_prop = {k: v for k, v in value.items() if k != 'type'}
|
|
536
|
+
ref_prop['$ref'] = "_definitions.yaml#/datetime"
|
|
537
|
+
formatted_props, _ = wrap_ref_siblings(ref_prop, first_key)
|
|
528
538
|
else:
|
|
529
539
|
formatted_props = value
|
|
530
|
-
|
|
540
|
+
|
|
531
541
|
output = {first_key: formatted_props}
|
|
532
542
|
return output
|
|
533
543
|
except Exception as e:
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""Utilities for making ``$ref`` properties safe under JSON Schema draft-04.
|
|
2
|
+
|
|
3
|
+
Gen3 compiles data dictionaries with draft-04 semantics, where any keyword
|
|
4
|
+
sitting as a direct sibling of ``$ref`` is ignored during resolution. A
|
|
5
|
+
property written as::
|
|
6
|
+
|
|
7
|
+
atrial_fibrillation:
|
|
8
|
+
description: "Self-reported atrial fibrillation."
|
|
9
|
+
$ref: "_definitions.yaml#/enum_yes_no"
|
|
10
|
+
|
|
11
|
+
loses its ``description`` (and ``termDef``, ``term``, etc.) when resolved by
|
|
12
|
+
strict tooling such as dictionaryutils, so the data-dictionary viewer shows
|
|
13
|
+
"No Description". The fix is structural: keep the annotations at the top
|
|
14
|
+
level and move the ``$ref`` into an ``allOf`` list::
|
|
15
|
+
|
|
16
|
+
atrial_fibrillation:
|
|
17
|
+
description: "Self-reported atrial fibrillation."
|
|
18
|
+
allOf:
|
|
19
|
+
- $ref: "_definitions.yaml#/enum_yes_no"
|
|
20
|
+
|
|
21
|
+
This module is the single source of truth for that transformation. It is
|
|
22
|
+
used by the schema generator (``converter.format_datetime``), the rule
|
|
23
|
+
validator, and the ``gen3schemadev fix-refs`` CLI command.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import fnmatch
|
|
29
|
+
import logging
|
|
30
|
+
import os
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
# Actions recorded for each property examined by the transform.
|
|
35
|
+
WRAPPED = "wrapped"
|
|
36
|
+
BARE_REF = "bare-ref"
|
|
37
|
+
ALREADY_WRAPPED = "already-wrapped"
|
|
38
|
+
NO_REF = "no-ref"
|
|
39
|
+
PROPERTIES_MERGE = "properties-merge"
|
|
40
|
+
|
|
41
|
+
# Human-readable reasons for skipped properties, keyed by action.
|
|
42
|
+
SKIP_REASONS = {
|
|
43
|
+
BARE_REF: "bare $ref, no siblings to preserve",
|
|
44
|
+
ALREADY_WRAPPED: "already wrapped in allOf/anyOf/oneOf",
|
|
45
|
+
PROPERTIES_MERGE: "$ref key in 'properties' is a Gen3 properties-merge",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_COMBINATORS = ("allOf", "anyOf", "oneOf")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def has_ref(prop) -> bool:
|
|
52
|
+
"""
|
|
53
|
+
Return True if a property definition references another schema, either
|
|
54
|
+
via a top-level ``$ref`` or via a ``$ref`` inside an allOf/anyOf/oneOf
|
|
55
|
+
list item.
|
|
56
|
+
"""
|
|
57
|
+
if not isinstance(prop, dict):
|
|
58
|
+
return False
|
|
59
|
+
if "$ref" in prop:
|
|
60
|
+
return True
|
|
61
|
+
for combinator in _COMBINATORS:
|
|
62
|
+
items = prop.get(combinator)
|
|
63
|
+
if isinstance(items, list):
|
|
64
|
+
if any(isinstance(item, dict) and "$ref" in item for item in items):
|
|
65
|
+
return True
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def wrap_ref_siblings(prop, prop_name: str = "") -> "tuple":
|
|
70
|
+
"""
|
|
71
|
+
Rewrite a single property definition so sibling annotations survive
|
|
72
|
+
draft-04 ``$ref`` resolution.
|
|
73
|
+
|
|
74
|
+
Returns a ``(new_prop, action)`` tuple. The transformation only fires
|
|
75
|
+
when the property dict has a top-level ``$ref`` alongside at least one
|
|
76
|
+
other key; in that case the siblings keep their original order and the
|
|
77
|
+
``$ref`` moves into an ``allOf`` list appended last. All other shapes
|
|
78
|
+
are returned unchanged:
|
|
79
|
+
|
|
80
|
+
- not a dict, or no ``$ref`` anywhere -> ``NO_REF``
|
|
81
|
+
- ``$ref`` only inside allOf/anyOf/oneOf -> ``ALREADY_WRAPPED`` (idempotent)
|
|
82
|
+
- ``{"$ref": ...}`` with no siblings -> ``BARE_REF`` (nothing is lost)
|
|
83
|
+
"""
|
|
84
|
+
if not isinstance(prop, dict) or not has_ref(prop):
|
|
85
|
+
return prop, NO_REF
|
|
86
|
+
|
|
87
|
+
if "$ref" not in prop:
|
|
88
|
+
return prop, ALREADY_WRAPPED
|
|
89
|
+
|
|
90
|
+
if len(prop) == 1:
|
|
91
|
+
return prop, BARE_REF
|
|
92
|
+
|
|
93
|
+
new_prop = {k: v for k, v in prop.items() if k != "$ref"}
|
|
94
|
+
if isinstance(new_prop.get("allOf"), list):
|
|
95
|
+
# Rare edge: the property already has an allOf next to the $ref.
|
|
96
|
+
# Append rather than overwrite so existing constraints are kept.
|
|
97
|
+
new_prop["allOf"] = new_prop["allOf"] + [{"$ref": prop["$ref"]}]
|
|
98
|
+
else:
|
|
99
|
+
new_prop["allOf"] = [{"$ref": prop["$ref"]}]
|
|
100
|
+
logger.debug(f"Wrapped $ref of property '{prop_name}' in allOf.")
|
|
101
|
+
return new_prop, WRAPPED
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def fix_schema(schema: dict) -> "tuple":
|
|
105
|
+
"""
|
|
106
|
+
Apply ``wrap_ref_siblings`` to every property of a node schema.
|
|
107
|
+
|
|
108
|
+
Only the direct values of ``schema["properties"]`` are examined:
|
|
109
|
+
sibling-``$ref`` annotations occur at the property level in Gen3 node
|
|
110
|
+
schemas, and a shallow walk cannot accidentally rewrite the Gen3
|
|
111
|
+
properties-merge construct (a ``$ref`` *key* directly inside the
|
|
112
|
+
``properties`` block), which is recorded as ``PROPERTIES_MERGE`` and
|
|
113
|
+
left untouched.
|
|
114
|
+
|
|
115
|
+
Returns ``(new_schema, changes)`` where changes is a list of
|
|
116
|
+
``(prop_name, action, preserved_keys)`` tuples. ``preserved_keys`` names
|
|
117
|
+
the sibling annotation keys kept for ``WRAPPED`` entries and is an empty
|
|
118
|
+
tuple otherwise.
|
|
119
|
+
"""
|
|
120
|
+
changes = []
|
|
121
|
+
props = schema.get("properties") if isinstance(schema, dict) else None
|
|
122
|
+
if not isinstance(props, dict):
|
|
123
|
+
return schema, changes
|
|
124
|
+
|
|
125
|
+
new_props = {}
|
|
126
|
+
for prop_name, prop in props.items():
|
|
127
|
+
if prop_name == "$ref":
|
|
128
|
+
new_props[prop_name] = prop
|
|
129
|
+
changes.append((prop_name, PROPERTIES_MERGE, ()))
|
|
130
|
+
continue
|
|
131
|
+
new_prop, action = wrap_ref_siblings(prop, prop_name)
|
|
132
|
+
new_props[prop_name] = new_prop
|
|
133
|
+
preserved = ()
|
|
134
|
+
if action == WRAPPED:
|
|
135
|
+
preserved = tuple(k for k in prop.keys() if k != "$ref")
|
|
136
|
+
if action != NO_REF:
|
|
137
|
+
changes.append((prop_name, action, preserved))
|
|
138
|
+
|
|
139
|
+
new_schema = dict(schema)
|
|
140
|
+
new_schema["properties"] = new_props
|
|
141
|
+
return new_schema, changes
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def find_null_descriptions(node, path: str = "") -> list:
|
|
145
|
+
"""
|
|
146
|
+
Recursively find every ``description`` key whose value is null.
|
|
147
|
+
|
|
148
|
+
The Gen3 metaschema requires ``description`` to be a string, so a
|
|
149
|
+
``description: null`` placeholder (common in generated shared
|
|
150
|
+
definitions) is invalid. With a direct ``$ref`` the resolver happens to
|
|
151
|
+
mask it — the referencing property's own description merges over the
|
|
152
|
+
definition's null — but a bare or allOf-wrapped ref exposes the null in
|
|
153
|
+
the resolved node schema, where metaschema validation fails far away
|
|
154
|
+
from the definition that caused it.
|
|
155
|
+
|
|
156
|
+
Returns a list of dotted paths to each offender, with list items shown
|
|
157
|
+
as ``[i]`` (e.g. ``"enum_yes_no.description"``,
|
|
158
|
+
``"properties.status.anyOf[0].description"``). Non-dict/list input
|
|
159
|
+
yields an empty list.
|
|
160
|
+
"""
|
|
161
|
+
hits = []
|
|
162
|
+
if isinstance(node, dict):
|
|
163
|
+
for key, value in node.items():
|
|
164
|
+
key_path = f"{path}.{key}" if path else str(key)
|
|
165
|
+
if key == "description" and value is None:
|
|
166
|
+
hits.append(key_path)
|
|
167
|
+
else:
|
|
168
|
+
hits.extend(find_null_descriptions(value, key_path))
|
|
169
|
+
elif isinstance(node, list):
|
|
170
|
+
for i, item in enumerate(node):
|
|
171
|
+
hits.extend(find_null_descriptions(item, f"{path}[{i}]"))
|
|
172
|
+
return hits
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def scan_dir_null_descriptions(yaml_dir: str) -> list:
|
|
176
|
+
"""
|
|
177
|
+
Scan every YAML file under ``yaml_dir`` (recursively) for null-valued
|
|
178
|
+
``description`` keys.
|
|
179
|
+
|
|
180
|
+
Unlike ``fix_yaml_dir``, underscore files (``_definitions.yaml``,
|
|
181
|
+
``_terms.yaml``, ...) are INCLUDED — shared definitions are where the
|
|
182
|
+
null placeholders usually live, and where they do the most damage
|
|
183
|
+
because every referencing property inherits them on resolution.
|
|
184
|
+
|
|
185
|
+
Returns ``"relpath: dotted.path"`` strings, one per offender.
|
|
186
|
+
"""
|
|
187
|
+
from gen3schemadev.utils import load_yaml
|
|
188
|
+
|
|
189
|
+
hits = []
|
|
190
|
+
for root, _dirs, files in os.walk(yaml_dir):
|
|
191
|
+
for fname in sorted(files):
|
|
192
|
+
if not (fnmatch.fnmatch(fname, "*.yaml") or fnmatch.fnmatch(fname, "*.yml")):
|
|
193
|
+
continue
|
|
194
|
+
path = os.path.join(root, fname)
|
|
195
|
+
rel_path = os.path.relpath(path, yaml_dir)
|
|
196
|
+
schema = load_yaml(path)
|
|
197
|
+
for hit in find_null_descriptions(schema):
|
|
198
|
+
hits.append(f"{rel_path}: {hit}")
|
|
199
|
+
return hits
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def fix_yaml_dir(yaml_dir: str, dry_run: bool = False) -> list:
|
|
203
|
+
"""
|
|
204
|
+
Rewrite every YAML node schema under ``yaml_dir`` (recursively) so that
|
|
205
|
+
``$ref`` sibling annotations survive draft-04 resolution.
|
|
206
|
+
|
|
207
|
+
Files whose basename starts with an underscore (``_definitions.yaml``,
|
|
208
|
+
``_terms.yaml``, ``_settings.yaml``) are skipped entirely: adding
|
|
209
|
+
structure or descriptions to shared definitions would force them onto
|
|
210
|
+
every referencing property. Files with no changes are never rewritten,
|
|
211
|
+
which keeps the operation idempotent. With ``dry_run=True`` nothing is
|
|
212
|
+
written at all.
|
|
213
|
+
|
|
214
|
+
Returns a list of per-file report dicts::
|
|
215
|
+
|
|
216
|
+
{"path": <relative path>, "skipped_file": <reason or None>,
|
|
217
|
+
"changes": [(prop_name, action, preserved_keys), ...],
|
|
218
|
+
"rewritten": <bool>}
|
|
219
|
+
"""
|
|
220
|
+
from gen3schemadev.utils import load_yaml, write_yaml
|
|
221
|
+
|
|
222
|
+
reports = []
|
|
223
|
+
for root, _dirs, files in os.walk(yaml_dir):
|
|
224
|
+
for fname in sorted(files):
|
|
225
|
+
if not (fnmatch.fnmatch(fname, "*.yaml") or fnmatch.fnmatch(fname, "*.yml")):
|
|
226
|
+
continue
|
|
227
|
+
path = os.path.join(root, fname)
|
|
228
|
+
rel_path = os.path.relpath(path, yaml_dir)
|
|
229
|
+
|
|
230
|
+
if fname.startswith("_"):
|
|
231
|
+
reports.append({
|
|
232
|
+
"path": rel_path,
|
|
233
|
+
"skipped_file": "definitions/terms/settings file",
|
|
234
|
+
"changes": [],
|
|
235
|
+
"rewritten": False,
|
|
236
|
+
})
|
|
237
|
+
continue
|
|
238
|
+
|
|
239
|
+
schema = load_yaml(path)
|
|
240
|
+
if not isinstance(schema, dict):
|
|
241
|
+
reports.append({
|
|
242
|
+
"path": rel_path,
|
|
243
|
+
"skipped_file": "not a mapping, no properties to fix",
|
|
244
|
+
"changes": [],
|
|
245
|
+
"rewritten": False,
|
|
246
|
+
})
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
new_schema, changes = fix_schema(schema)
|
|
250
|
+
wrapped = [c for c in changes if c[1] == WRAPPED]
|
|
251
|
+
rewritten = False
|
|
252
|
+
if wrapped and not dry_run:
|
|
253
|
+
write_yaml(new_schema, path)
|
|
254
|
+
rewritten = True
|
|
255
|
+
|
|
256
|
+
reports.append({
|
|
257
|
+
"path": rel_path,
|
|
258
|
+
"skipped_file": None,
|
|
259
|
+
"changes": changes,
|
|
260
|
+
"rewritten": rewritten,
|
|
261
|
+
})
|
|
262
|
+
return reports
|
{gen3schemadev-2.4.0 → gen3schemadev-2.6.0}/src/gen3schemadev/schema/schema_templates/project.yaml
RENAMED
|
@@ -35,9 +35,10 @@ properties:
|
|
|
35
35
|
type:
|
|
36
36
|
type: string
|
|
37
37
|
id:
|
|
38
|
-
$ref: _definitions.yaml#/UUID
|
|
39
38
|
systemAlias: node_id
|
|
40
39
|
description: UUID for the project.
|
|
40
|
+
allOf:
|
|
41
|
+
- $ref: _definitions.yaml#/UUID
|
|
41
42
|
name:
|
|
42
43
|
type: string
|
|
43
44
|
description: Display name/brief description for the project.
|
|
@@ -62,10 +63,11 @@ properties:
|
|
|
62
63
|
description: The ID of the source providing support/grant resources.
|
|
63
64
|
type: string
|
|
64
65
|
programs:
|
|
65
|
-
$ref: _definitions.yaml#/to_one
|
|
66
66
|
description: 'Indicates that the Study is logically part of the indicated Study.
|
|
67
67
|
|
|
68
68
|
'
|
|
69
|
+
allOf:
|
|
70
|
+
- $ref: _definitions.yaml#/to_one
|
|
69
71
|
state:
|
|
70
72
|
description: 'The possible states a project can be in. All but `open` are
|
|
71
73
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# validates gen3 bundled jsonschema (.json) by testing gen3 specific business rules
|
|
2
2
|
import logging
|
|
3
3
|
from gen3schemadev.converter import link_suffix
|
|
4
|
+
from gen3schemadev.refs import has_ref
|
|
4
5
|
|
|
5
6
|
logger = logging.getLogger(__name__)
|
|
6
7
|
|
|
@@ -188,10 +189,12 @@ class RuleValidator:
|
|
|
188
189
|
)
|
|
189
190
|
continue
|
|
190
191
|
|
|
191
|
-
# Skip if property definition contains a $ref (i.e., is an alias/reference)
|
|
192
|
-
|
|
192
|
+
# Skip if property definition contains a $ref (i.e., is an alias/reference),
|
|
193
|
+
# either top-level or wrapped in allOf/anyOf/oneOf
|
|
194
|
+
if has_ref(value):
|
|
193
195
|
logger.debug(
|
|
194
|
-
f"Skipping property '{key}' because it contains a '$ref'
|
|
196
|
+
f"Skipping property '{key}' because it contains a '$ref' "
|
|
197
|
+
"(top-level or inside allOf/anyOf/oneOf)."
|
|
195
198
|
)
|
|
196
199
|
continue
|
|
197
200
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gen3schemadev-2.4.0 → gen3schemadev-2.6.0}/src/gen3schemadev/schema/schema_templates/_settings.yaml
RENAMED
|
File without changes
|
{gen3schemadev-2.4.0 → gen3schemadev-2.6.0}/src/gen3schemadev/schema/schema_templates/_terms.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gen3schemadev-2.4.0 → gen3schemadev-2.6.0}/src/gen3schemadev/validators/metaschema_validator.py
RENAMED
|
File without changes
|