Gen3SchemaDev 2.6.0__tar.gz → 2.6.2__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.6.0 → gen3schemadev-2.6.2}/PKG-INFO +3 -12
- {gen3schemadev-2.6.0 → gen3schemadev-2.6.2}/README.md +2 -11
- {gen3schemadev-2.6.0 → gen3schemadev-2.6.2}/pyproject.toml +1 -1
- {gen3schemadev-2.6.0 → gen3schemadev-2.6.2}/src/gen3schemadev/cli.py +1 -68
- {gen3schemadev-2.6.0 → gen3schemadev-2.6.2}/src/gen3schemadev/converter.py +8 -13
- gen3schemadev-2.6.2/src/gen3schemadev/refs.py +74 -0
- {gen3schemadev-2.6.0 → gen3schemadev-2.6.2}/src/gen3schemadev/schema/schema_templates/program.yaml +1 -2
- {gen3schemadev-2.6.0 → gen3schemadev-2.6.2}/src/gen3schemadev/schema/schema_templates/project.yaml +2 -4
- gen3schemadev-2.6.0/src/gen3schemadev/refs.py +0 -262
- {gen3schemadev-2.6.0 → gen3schemadev-2.6.2}/LICENSE +0 -0
- {gen3schemadev-2.6.0 → gen3schemadev-2.6.2}/src/gen3schemadev/__init__.py +0 -0
- {gen3schemadev-2.6.0 → gen3schemadev-2.6.2}/src/gen3schemadev/ddvis.py +0 -0
- {gen3schemadev-2.6.0 → gen3schemadev-2.6.2}/src/gen3schemadev/schema/__init__.py +0 -0
- {gen3schemadev-2.6.0 → gen3schemadev-2.6.2}/src/gen3schemadev/schema/gen3_template.py +0 -0
- {gen3schemadev-2.6.0 → gen3schemadev-2.6.2}/src/gen3schemadev/schema/input_schema.py +0 -0
- {gen3schemadev-2.6.0 → gen3schemadev-2.6.2}/src/gen3schemadev/schema/schema_templates/_definitions.yaml +0 -0
- {gen3schemadev-2.6.0 → gen3schemadev-2.6.2}/src/gen3schemadev/schema/schema_templates/_settings.yaml +0 -0
- {gen3schemadev-2.6.0 → gen3schemadev-2.6.2}/src/gen3schemadev/schema/schema_templates/_terms.yaml +0 -0
- {gen3schemadev-2.6.0 → gen3schemadev-2.6.2}/src/gen3schemadev/schema/schema_templates/core_metadata_collection.yaml +0 -0
- {gen3schemadev-2.6.0 → gen3schemadev-2.6.2}/src/gen3schemadev/schema/schema_templates/gen3_metaschema.yml +0 -0
- {gen3schemadev-2.6.0 → gen3schemadev-2.6.2}/src/gen3schemadev/utils.py +0 -0
- {gen3schemadev-2.6.0 → gen3schemadev-2.6.2}/src/gen3schemadev/validators/input_validator.py +0 -0
- {gen3schemadev-2.6.0 → gen3schemadev-2.6.2}/src/gen3schemadev/validators/metaschema_validator.py +0 -0
- {gen3schemadev-2.6.0 → gen3schemadev-2.6.2}/src/gen3schemadev/validators/rule_validator.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Gen3SchemaDev
|
|
3
|
-
Version: 2.6.
|
|
3
|
+
Version: 2.6.2
|
|
4
4
|
Summary: Tool for data modelling in Gen3
|
|
5
5
|
License: Apache 2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -35,18 +35,9 @@ 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
|
-
###
|
|
38
|
+
### Null description placeholders
|
|
39
39
|
|
|
40
|
-
|
|
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.
|
|
40
|
+
`gen3schemadev validate` warns about `description: null` placeholders anywhere in the dictionary (commonly in `_definitions.yaml`'s enum definitions). The Gen3 metaschema requires descriptions to be strings; null placeholders cause "No Description" in the data-dictionary viewer and metaschema validation failures that surface on resolved node schemas, far from the offending definition. Remove the null `description` keys to resolve the warning.
|
|
50
41
|
|
|
51
42
|
|
|
52
43
|
## Deep dive into Gen3 Data Modelling
|
|
@@ -12,18 +12,9 @@ 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
|
-
###
|
|
15
|
+
### Null description placeholders
|
|
16
16
|
|
|
17
|
-
|
|
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.
|
|
17
|
+
`gen3schemadev validate` warns about `description: null` placeholders anywhere in the dictionary (commonly in `_definitions.yaml`'s enum definitions). The Gen3 metaschema requires descriptions to be strings; null placeholders cause "No Description" in the data-dictionary viewer and metaschema validation failures that surface on resolved node schemas, far from the offending definition. Remove the null `description` keys to resolve the warning.
|
|
27
18
|
|
|
28
19
|
|
|
29
20
|
## Deep dive into Gen3 Data Modelling
|
|
@@ -20,13 +20,7 @@ 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
|
-
)
|
|
23
|
+
from gen3schemadev.refs import find_null_descriptions
|
|
30
24
|
|
|
31
25
|
|
|
32
26
|
def print_null_description_warning(hits):
|
|
@@ -168,27 +162,6 @@ def main():
|
|
|
168
162
|
help="Set logging level to DEBUG"
|
|
169
163
|
)
|
|
170
164
|
|
|
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
|
-
|
|
192
165
|
args = parser.parse_args()
|
|
193
166
|
|
|
194
167
|
# Handle case where no command is provided
|
|
@@ -361,45 +334,5 @@ def main():
|
|
|
361
334
|
output_path = args.output or "input_example.yaml"
|
|
362
335
|
write_yaml(init_yaml, output_path)
|
|
363
336
|
|
|
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
|
-
|
|
404
337
|
if __name__ == "__main__":
|
|
405
338
|
main()
|
|
@@ -17,8 +17,6 @@ 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
|
-
|
|
22
20
|
|
|
23
21
|
logger = logging.getLogger(__name__)
|
|
24
22
|
|
|
@@ -490,11 +488,11 @@ def format_datetime(prop_dict: dict) -> dict:
|
|
|
490
488
|
Formats a property dictionary with a 'type' of 'datetime' to use a $ref
|
|
491
489
|
to the Gen3 _definitions.yaml#/datetime definition.
|
|
492
490
|
|
|
493
|
-
Annotations such as 'description' are preserved
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
491
|
+
Annotations such as 'description' are preserved as siblings of the $ref:
|
|
492
|
+
Gen3's resolver merges sibling keys over the referenced definition, so
|
|
493
|
+
the property's own description survives resolution. A datetime property
|
|
494
|
+
with no annotations becomes a bare $ref. If the property is not of type
|
|
495
|
+
'datetime', it is returned unchanged.
|
|
498
496
|
|
|
499
497
|
Example input:
|
|
500
498
|
{
|
|
@@ -508,9 +506,7 @@ def format_datetime(prop_dict: dict) -> dict:
|
|
|
508
506
|
{
|
|
509
507
|
"collection_date": {
|
|
510
508
|
"description": "Date and time of collection (datetime)",
|
|
511
|
-
"
|
|
512
|
-
{"$ref": "_definitions.yaml#/datetime"}
|
|
513
|
-
]
|
|
509
|
+
"$ref": "_definitions.yaml#/datetime"
|
|
514
510
|
}
|
|
515
511
|
}
|
|
516
512
|
|
|
@@ -532,9 +528,8 @@ def format_datetime(prop_dict: dict) -> dict:
|
|
|
532
528
|
value = prop_dict[first_key]
|
|
533
529
|
|
|
534
530
|
if isinstance(value, dict) and value.get('type') == 'datetime':
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
formatted_props, _ = wrap_ref_siblings(ref_prop, first_key)
|
|
531
|
+
formatted_props = {k: v for k, v in value.items() if k != 'type'}
|
|
532
|
+
formatted_props['$ref'] = "_definitions.yaml#/datetime"
|
|
538
533
|
else:
|
|
539
534
|
formatted_props = value
|
|
540
535
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Diagnostics for ``$ref`` usage in Gen3 data dictionaries.
|
|
2
|
+
|
|
3
|
+
Gen3's resolver merges a property's sibling keys over the referenced
|
|
4
|
+
definition, so writing annotations next to a ``$ref`` is the normal,
|
|
5
|
+
working form::
|
|
6
|
+
|
|
7
|
+
atrial_fibrillation:
|
|
8
|
+
description: "Self-reported atrial fibrillation."
|
|
9
|
+
$ref: "_definitions.yaml#/enum_yes_no"
|
|
10
|
+
|
|
11
|
+
The real hazard is a ``description: null`` placeholder inside a shared
|
|
12
|
+
definition (commonly the enum definitions in ``_definitions.yaml``): the
|
|
13
|
+
Gen3 metaschema requires ``description`` to be a string, and the null shows
|
|
14
|
+
up as "No Description" in the data-dictionary viewer and fails metaschema
|
|
15
|
+
validation on resolved node schemas — far from the definition that carries
|
|
16
|
+
it. ``find_null_descriptions`` reports every such placeholder by path so
|
|
17
|
+
the root cause is named up front.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
_COMBINATORS = ("allOf", "anyOf", "oneOf")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def has_ref(prop) -> bool:
|
|
30
|
+
"""
|
|
31
|
+
Return True if a property definition references another schema, either
|
|
32
|
+
via a top-level ``$ref`` or via a ``$ref`` inside an allOf/anyOf/oneOf
|
|
33
|
+
list item.
|
|
34
|
+
"""
|
|
35
|
+
if not isinstance(prop, dict):
|
|
36
|
+
return False
|
|
37
|
+
if "$ref" in prop:
|
|
38
|
+
return True
|
|
39
|
+
for combinator in _COMBINATORS:
|
|
40
|
+
items = prop.get(combinator)
|
|
41
|
+
if isinstance(items, list):
|
|
42
|
+
if any(isinstance(item, dict) and "$ref" in item for item in items):
|
|
43
|
+
return True
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def find_null_descriptions(node, path: str = "") -> list:
|
|
48
|
+
"""
|
|
49
|
+
Recursively find every ``description`` key whose value is null.
|
|
50
|
+
|
|
51
|
+
The Gen3 metaschema requires ``description`` to be a string, so a
|
|
52
|
+
``description: null`` placeholder (common in generated shared
|
|
53
|
+
definitions) is invalid. Because the property's own description merges
|
|
54
|
+
over the definition's during resolution, the null often stays hidden
|
|
55
|
+
until it surfaces on some resolved node schema — where the resulting
|
|
56
|
+
metaschema failure points far away from the definition that caused it.
|
|
57
|
+
|
|
58
|
+
Returns a list of dotted paths to each offender, with list items shown
|
|
59
|
+
as ``[i]`` (e.g. ``"enum_yes_no.description"``,
|
|
60
|
+
``"properties.status.anyOf[0].description"``). Non-dict/list input
|
|
61
|
+
yields an empty list.
|
|
62
|
+
"""
|
|
63
|
+
hits = []
|
|
64
|
+
if isinstance(node, dict):
|
|
65
|
+
for key, value in node.items():
|
|
66
|
+
key_path = f"{path}.{key}" if path else str(key)
|
|
67
|
+
if key == "description" and value is None:
|
|
68
|
+
hits.append(key_path)
|
|
69
|
+
else:
|
|
70
|
+
hits.extend(find_null_descriptions(value, key_path))
|
|
71
|
+
elif isinstance(node, list):
|
|
72
|
+
for i, item in enumerate(node):
|
|
73
|
+
hits.extend(find_null_descriptions(item, f"{path}[{i}]"))
|
|
74
|
+
return hits
|
{gen3schemadev-2.6.0 → gen3schemadev-2.6.2}/src/gen3schemadev/schema/schema_templates/project.yaml
RENAMED
|
@@ -35,10 +35,9 @@ properties:
|
|
|
35
35
|
type:
|
|
36
36
|
type: string
|
|
37
37
|
id:
|
|
38
|
+
$ref: _definitions.yaml#/UUID
|
|
38
39
|
systemAlias: node_id
|
|
39
40
|
description: UUID for the project.
|
|
40
|
-
allOf:
|
|
41
|
-
- $ref: _definitions.yaml#/UUID
|
|
42
41
|
name:
|
|
43
42
|
type: string
|
|
44
43
|
description: Display name/brief description for the project.
|
|
@@ -63,11 +62,10 @@ properties:
|
|
|
63
62
|
description: The ID of the source providing support/grant resources.
|
|
64
63
|
type: string
|
|
65
64
|
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
|
|
71
69
|
state:
|
|
72
70
|
description: 'The possible states a project can be in. All but `open` are
|
|
73
71
|
|
|
@@ -1,262 +0,0 @@
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gen3schemadev-2.6.0 → gen3schemadev-2.6.2}/src/gen3schemadev/schema/schema_templates/_settings.yaml
RENAMED
|
File without changes
|
{gen3schemadev-2.6.0 → gen3schemadev-2.6.2}/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.6.0 → gen3schemadev-2.6.2}/src/gen3schemadev/validators/metaschema_validator.py
RENAMED
|
File without changes
|
|
File without changes
|