Gen3SchemaDev 2.3.6__tar.gz → 2.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 (23) hide show
  1. {gen3schemadev-2.3.6 → gen3schemadev-2.5.0}/PKG-INFO +16 -2
  2. {gen3schemadev-2.3.6 → gen3schemadev-2.5.0}/README.md +11 -0
  3. {gen3schemadev-2.3.6 → gen3schemadev-2.5.0}/pyproject.toml +2 -2
  4. {gen3schemadev-2.3.6 → gen3schemadev-2.5.0}/src/gen3schemadev/cli.py +59 -1
  5. {gen3schemadev-2.3.6 → gen3schemadev-2.5.0}/src/gen3schemadev/converter.py +20 -8
  6. gen3schemadev-2.5.0/src/gen3schemadev/refs.py +204 -0
  7. {gen3schemadev-2.3.6 → gen3schemadev-2.5.0}/src/gen3schemadev/schema/schema_templates/program.yaml +2 -1
  8. {gen3schemadev-2.3.6 → gen3schemadev-2.5.0}/src/gen3schemadev/schema/schema_templates/project.yaml +4 -2
  9. {gen3schemadev-2.3.6 → gen3schemadev-2.5.0}/src/gen3schemadev/validators/rule_validator.py +6 -3
  10. {gen3schemadev-2.3.6 → gen3schemadev-2.5.0}/LICENSE +0 -0
  11. {gen3schemadev-2.3.6 → gen3schemadev-2.5.0}/src/gen3schemadev/__init__.py +0 -0
  12. {gen3schemadev-2.3.6 → gen3schemadev-2.5.0}/src/gen3schemadev/ddvis.py +0 -0
  13. {gen3schemadev-2.3.6 → gen3schemadev-2.5.0}/src/gen3schemadev/schema/__init__.py +0 -0
  14. {gen3schemadev-2.3.6 → gen3schemadev-2.5.0}/src/gen3schemadev/schema/gen3_template.py +0 -0
  15. {gen3schemadev-2.3.6 → gen3schemadev-2.5.0}/src/gen3schemadev/schema/input_schema.py +0 -0
  16. {gen3schemadev-2.3.6 → gen3schemadev-2.5.0}/src/gen3schemadev/schema/schema_templates/_definitions.yaml +0 -0
  17. {gen3schemadev-2.3.6 → gen3schemadev-2.5.0}/src/gen3schemadev/schema/schema_templates/_settings.yaml +0 -0
  18. {gen3schemadev-2.3.6 → gen3schemadev-2.5.0}/src/gen3schemadev/schema/schema_templates/_terms.yaml +0 -0
  19. {gen3schemadev-2.3.6 → gen3schemadev-2.5.0}/src/gen3schemadev/schema/schema_templates/core_metadata_collection.yaml +0 -0
  20. {gen3schemadev-2.3.6 → gen3schemadev-2.5.0}/src/gen3schemadev/schema/schema_templates/gen3_metaschema.yml +0 -0
  21. {gen3schemadev-2.3.6 → gen3schemadev-2.5.0}/src/gen3schemadev/utils.py +0 -0
  22. {gen3schemadev-2.3.6 → gen3schemadev-2.5.0}/src/gen3schemadev/validators/input_validator.py +0 -0
  23. {gen3schemadev-2.3.6 → gen3schemadev-2.5.0}/src/gen3schemadev/validators/metaschema_validator.py +0 -0
@@ -1,14 +1,17 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Gen3SchemaDev
3
- Version: 2.3.6
3
+ Version: 2.5.0
4
4
  Summary: Tool for data modelling in Gen3
5
5
  License: Apache 2.0
6
6
  License-File: LICENSE
7
7
  Author: JoshuaHarris391
8
8
  Author-email: harjo391@gmail.com
9
- Requires-Python: >=3.12.10,<4.0
9
+ Requires-Python: >=3.9.5,<4.0
10
10
  Classifier: License :: Other/Proprietary License
11
11
  Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
12
15
  Classifier: Programming Language :: Python :: 3.13
13
16
  Classifier: Programming Language :: Python :: 3.14
14
17
  Requires-Dist: check-jsonschema (>=0.34.1,<0.35.0)
@@ -32,6 +35,17 @@ This repository aims to provide the documentation, learning materials, and softw
32
35
  - [Guide to creating your first dictionary](docs/gen3schemadev/first_dictionary.md)
33
36
  - [Troubleshooting](docs/gen3schemadev/troubleshooting.md)
34
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
+
35
49
 
36
50
  ## Deep dive into Gen3 Data Modelling
37
51
  *Special Thanks to Marion Shadbolt for providing the [source material](https://github.com/AustralianBioCommons/umccr-dictionary/tree/main/docs/schemas)*
@@ -12,6 +12,17 @@ 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
+
15
26
 
16
27
  ## Deep dive into Gen3 Data Modelling
17
28
  *Special Thanks to Marion Shadbolt for providing the [source material](https://github.com/AustralianBioCommons/umccr-dictionary/tree/main/docs/schemas)*
@@ -1,13 +1,13 @@
1
1
  [project]
2
2
  name = "Gen3SchemaDev"
3
- version = "2.3.6"
3
+ version = "2.5.0"
4
4
  description = "Tool for data modelling in Gen3"
5
5
  authors = [
6
6
  {name = "JoshuaHarris391",email = "harjo391@gmail.com"}
7
7
  ]
8
8
  license = {text = "Apache 2.0"}
9
9
  readme = "README.md"
10
- requires-python = ">=3.12.10,<4.0"
10
+ requires-python = ">=3.9.5,<4.0"
11
11
  dependencies = [
12
12
  "setuptools>=70.0",
13
13
  "gen3-validator (>=2.0.0,<3.0.0)",
@@ -20,6 +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 WRAPPED, SKIP_REASONS, fix_yaml_dir
23
24
 
24
25
 
25
26
  def main():
@@ -142,7 +143,28 @@ def main():
142
143
  action="store_true",
143
144
  help="Set logging level to DEBUG"
144
145
  )
145
-
146
+
147
+ # Create 'fix-refs' subcommand
148
+ fixrefs_parser = subparsers.add_parser(
149
+ "fix-refs",
150
+ help="Rewrite properties with '$ref' siblings into draft-04-safe allOf form (in place)"
151
+ )
152
+ fixrefs_parser.add_argument(
153
+ "-y", "--yamldir",
154
+ required=True,
155
+ help="Directory of Gen3 YAML files to rewrite in place (recursive)"
156
+ )
157
+ fixrefs_parser.add_argument(
158
+ "--dry-run",
159
+ action="store_true",
160
+ help="Report what would change without writing files"
161
+ )
162
+ fixrefs_parser.add_argument(
163
+ "--debug",
164
+ action="store_true",
165
+ help="Set logging level to DEBUG"
166
+ )
167
+
146
168
  args = parser.parse_args()
147
169
 
148
170
  # Handle case where no command is provided
@@ -306,5 +328,41 @@ def main():
306
328
  output_path = args.output or "input_example.yaml"
307
329
  write_yaml(init_yaml, output_path)
308
330
 
331
+ elif args.command == "fix-refs":
332
+ if not os.path.isdir(args.yamldir):
333
+ print(f"ERROR: Directory not found: {args.yamldir}", file=sys.stderr)
334
+ sys.exit(1)
335
+
336
+ if args.dry_run:
337
+ print(f"Dry run: scanning {args.yamldir} (no files will be written)")
338
+ else:
339
+ print(f"Fixing $ref sibling annotations in: {args.yamldir}")
340
+
341
+ reports = fix_yaml_dir(args.yamldir, dry_run=args.dry_run)
342
+
343
+ total_wrapped = 0
344
+ files_changed = 0
345
+ for report in reports:
346
+ print(report["path"])
347
+ if report["skipped_file"]:
348
+ print(f" skipped file ({report['skipped_file']})")
349
+ continue
350
+ if not report["changes"]:
351
+ print(" no $ref properties found")
352
+ continue
353
+ for prop_name, action, preserved in report["changes"]:
354
+ if action == WRAPPED:
355
+ total_wrapped += 1
356
+ print(f" wrapped: {prop_name} (preserved: {', '.join(preserved)})")
357
+ else:
358
+ print(f" skipped ({SKIP_REASONS[action]}): {prop_name}")
359
+ if report["rewritten"] or (args.dry_run and any(c[1] == WRAPPED for c in report["changes"])):
360
+ files_changed += 1
361
+
362
+ files_unchanged = len(reports) - files_changed
363
+ verb = "would be wrapped" if args.dry_run else "wrapped"
364
+ print(f"\nSummary: {total_wrapped} properties {verb} in {files_changed} files; "
365
+ f"{files_unchanged} files unchanged.")
366
+
309
367
  if __name__ == "__main__":
310
368
  main()
@@ -11,10 +11,14 @@ Expected Data Structure:
11
11
  - links: List of link objects with attributes: child, parent, multiplicity
12
12
  """
13
13
 
14
+ from __future__ import annotations
15
+
14
16
  from dataclasses import dataclass, asdict
15
17
  from typing import Protocol, runtime_checkable, Dict, Any, List
16
18
  import logging
17
19
 
20
+ from gen3schemadev.refs import wrap_ref_siblings
21
+
18
22
 
19
23
  logger = logging.getLogger(__name__)
20
24
 
@@ -486,7 +490,11 @@ def format_datetime(prop_dict: dict) -> dict:
486
490
  Formats a property dictionary with a 'type' of 'datetime' to use a $ref
487
491
  to the Gen3 _definitions.yaml#/datetime definition.
488
492
 
489
- If the property is not of type 'datetime', returns the property unchanged.
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.
490
498
 
491
499
  Example input:
492
500
  {
@@ -499,7 +507,10 @@ def format_datetime(prop_dict: dict) -> dict:
499
507
  Example output:
500
508
  {
501
509
  "collection_date": {
502
- "$ref": "_definitions.yaml#/datetime"
510
+ "description": "Date and time of collection (datetime)",
511
+ "allOf": [
512
+ {"$ref": "_definitions.yaml#/datetime"}
513
+ ]
503
514
  }
504
515
  }
505
516
 
@@ -516,16 +527,17 @@ def format_datetime(prop_dict: dict) -> dict:
516
527
  try:
517
528
  if len(prop_dict) != 1:
518
529
  raise ValueError("Expected a single property dictionary")
519
-
530
+
520
531
  first_key = next(iter(prop_dict))
521
532
  value = prop_dict[first_key]
522
- formatted_props = {}
523
-
524
- if 'type' in value and value['type'] == 'datetime':
525
- formatted_props['$ref'] = "_definitions.yaml#/datetime"
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)
526
538
  else:
527
539
  formatted_props = value
528
-
540
+
529
541
  output = {first_key: formatted_props}
530
542
  return output
531
543
  except Exception as e:
@@ -0,0 +1,204 @@
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 fix_yaml_dir(yaml_dir: str, dry_run: bool = False) -> list:
145
+ """
146
+ Rewrite every YAML node schema under ``yaml_dir`` (recursively) so that
147
+ ``$ref`` sibling annotations survive draft-04 resolution.
148
+
149
+ Files whose basename starts with an underscore (``_definitions.yaml``,
150
+ ``_terms.yaml``, ``_settings.yaml``) are skipped entirely: adding
151
+ structure or descriptions to shared definitions would force them onto
152
+ every referencing property. Files with no changes are never rewritten,
153
+ which keeps the operation idempotent. With ``dry_run=True`` nothing is
154
+ written at all.
155
+
156
+ Returns a list of per-file report dicts::
157
+
158
+ {"path": <relative path>, "skipped_file": <reason or None>,
159
+ "changes": [(prop_name, action, preserved_keys), ...],
160
+ "rewritten": <bool>}
161
+ """
162
+ from gen3schemadev.utils import load_yaml, write_yaml
163
+
164
+ reports = []
165
+ for root, _dirs, files in os.walk(yaml_dir):
166
+ for fname in sorted(files):
167
+ if not (fnmatch.fnmatch(fname, "*.yaml") or fnmatch.fnmatch(fname, "*.yml")):
168
+ continue
169
+ path = os.path.join(root, fname)
170
+ rel_path = os.path.relpath(path, yaml_dir)
171
+
172
+ if fname.startswith("_"):
173
+ reports.append({
174
+ "path": rel_path,
175
+ "skipped_file": "definitions/terms/settings file",
176
+ "changes": [],
177
+ "rewritten": False,
178
+ })
179
+ continue
180
+
181
+ schema = load_yaml(path)
182
+ if not isinstance(schema, dict):
183
+ reports.append({
184
+ "path": rel_path,
185
+ "skipped_file": "not a mapping, no properties to fix",
186
+ "changes": [],
187
+ "rewritten": False,
188
+ })
189
+ continue
190
+
191
+ new_schema, changes = fix_schema(schema)
192
+ wrapped = [c for c in changes if c[1] == WRAPPED]
193
+ rewritten = False
194
+ if wrapped and not dry_run:
195
+ write_yaml(new_schema, path)
196
+ rewritten = True
197
+
198
+ reports.append({
199
+ "path": rel_path,
200
+ "skipped_file": None,
201
+ "changes": changes,
202
+ "rewritten": rewritten,
203
+ })
204
+ return reports
@@ -24,8 +24,9 @@ properties:
24
24
  type:
25
25
  type: string
26
26
  id:
27
- $ref: _definitions.yaml#/UUID
28
27
  systemAlias: node_id
28
+ allOf:
29
+ - $ref: _definitions.yaml#/UUID
29
30
  name:
30
31
  type: string
31
32
  description: Full name/title of the program.
@@ -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
- if "$ref" in value:
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