linkml 1.5.5__py3-none-any.whl → 1.5.7__py3-none-any.whl
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.
- linkml/__init__.py +2 -6
- linkml/_version.py +1 -1
- linkml/generators/PythonGenNotes.md +4 -4
- linkml/generators/__init__.py +26 -5
- linkml/generators/common/type_designators.py +27 -22
- linkml/generators/csvgen.py +4 -10
- linkml/generators/docgen/class.md.jinja2 +7 -0
- linkml/generators/docgen/class_diagram.md.jinja2 +0 -6
- linkml/generators/docgen/subset.md.jinja2 +54 -13
- linkml/generators/docgen.py +94 -92
- linkml/generators/dotgen.py +5 -9
- linkml/generators/erdiagramgen.py +58 -53
- linkml/generators/excelgen.py +10 -16
- linkml/generators/golanggen.py +11 -21
- linkml/generators/golrgen.py +4 -13
- linkml/generators/graphqlgen.py +3 -11
- linkml/generators/javagen.py +8 -15
- linkml/generators/jsonldcontextgen.py +7 -36
- linkml/generators/jsonldgen.py +14 -12
- linkml/generators/jsonschemagen.py +183 -136
- linkml/generators/linkmlgen.py +1 -1
- linkml/generators/markdowngen.py +40 -89
- linkml/generators/namespacegen.py +1 -2
- linkml/generators/oocodegen.py +22 -25
- linkml/generators/owlgen.py +48 -49
- linkml/generators/prefixmapgen.py +6 -14
- linkml/generators/projectgen.py +7 -14
- linkml/generators/protogen.py +3 -5
- linkml/generators/pydanticgen.py +85 -73
- linkml/generators/pythongen.py +89 -157
- linkml/generators/rdfgen.py +5 -11
- linkml/generators/shaclgen.py +32 -18
- linkml/generators/shexgen.py +19 -24
- linkml/generators/sparqlgen.py +5 -13
- linkml/generators/sqlalchemy/__init__.py +3 -2
- linkml/generators/sqlalchemy/sqlalchemy_declarative_template.py +7 -7
- linkml/generators/sqlalchemy/sqlalchemy_imperative_template.py +3 -3
- linkml/generators/sqlalchemygen.py +29 -27
- linkml/generators/sqlddlgen.py +34 -26
- linkml/generators/sqltablegen.py +21 -21
- linkml/generators/sssomgen.py +11 -13
- linkml/generators/summarygen.py +2 -4
- linkml/generators/terminusdbgen.py +7 -19
- linkml/generators/typescriptgen.py +10 -18
- linkml/generators/yamlgen.py +0 -2
- linkml/generators/yumlgen.py +23 -71
- linkml/linter/cli.py +4 -11
- linkml/linter/config/datamodel/config.py +17 -47
- linkml/linter/linter.py +2 -4
- linkml/linter/rules.py +34 -48
- linkml/reporting/__init__.py +2 -0
- linkml/reporting/model.py +9 -24
- linkml/transformers/relmodel_transformer.py +20 -33
- linkml/transformers/schema_renamer.py +14 -10
- linkml/utils/converter.py +15 -15
- linkml/utils/datautils.py +9 -24
- linkml/utils/datavalidator.py +2 -2
- linkml/utils/execute_tutorial.py +10 -12
- linkml/utils/generator.py +74 -92
- linkml/utils/helpers.py +4 -2
- linkml/utils/ifabsent_functions.py +23 -15
- linkml/utils/mergeutils.py +19 -35
- linkml/utils/rawloader.py +2 -6
- linkml/utils/schema_builder.py +31 -19
- linkml/utils/schema_fixer.py +28 -18
- linkml/utils/schemaloader.py +44 -89
- linkml/utils/schemasynopsis.py +50 -73
- linkml/utils/sqlutils.py +40 -30
- linkml/utils/typereferences.py +9 -6
- linkml/utils/validation.py +4 -5
- linkml/validators/__init__.py +2 -0
- linkml/validators/jsonschemavalidator.py +104 -53
- linkml/validators/sparqlvalidator.py +5 -15
- linkml/workspaces/datamodel/workspaces.py +13 -30
- linkml/workspaces/example_runner.py +75 -68
- {linkml-1.5.5.dist-info → linkml-1.5.7.dist-info}/METADATA +2 -2
- linkml-1.5.7.dist-info/RECORD +109 -0
- linkml-1.5.5.dist-info/RECORD +0 -109
- {linkml-1.5.5.dist-info → linkml-1.5.7.dist-info}/LICENSE +0 -0
- {linkml-1.5.5.dist-info → linkml-1.5.7.dist-info}/WHEEL +0 -0
- {linkml-1.5.5.dist-info → linkml-1.5.7.dist-info}/entry_points.txt +0 -0
@@ -7,13 +7,17 @@ from dataclasses import dataclass, field
|
|
7
7
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
8
8
|
|
9
9
|
import click
|
10
|
-
from linkml_runtime.linkml_model.meta import (
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
10
|
+
from linkml_runtime.linkml_model.meta import (
|
11
|
+
AnonymousClassExpression,
|
12
|
+
AnonymousSlotExpression,
|
13
|
+
ClassDefinition,
|
14
|
+
ClassDefinitionName,
|
15
|
+
EnumDefinition,
|
16
|
+
PermissibleValue,
|
17
|
+
PermissibleValueText,
|
18
|
+
SlotDefinition,
|
19
|
+
metamodel_version,
|
20
|
+
)
|
17
21
|
from linkml_runtime.utils.formatutils import be, camelcase, underscore
|
18
22
|
|
19
23
|
from linkml._version import __version__
|
@@ -36,8 +40,8 @@ json_schema_types: Dict[str, Tuple[str, Optional[str]]] = {
|
|
36
40
|
"xsdtime": ("string", "time"),
|
37
41
|
}
|
38
42
|
|
39
|
-
class JsonSchema(UserDict):
|
40
43
|
|
44
|
+
class JsonSchema(UserDict):
|
41
45
|
OPTIONAL_IDENTIFIER_SUFFIX = "__identifier_optional"
|
42
46
|
|
43
47
|
def __init__(self, *args, **kwargs):
|
@@ -47,13 +51,13 @@ class JsonSchema(UserDict):
|
|
47
51
|
def add_def(self, name: str, subschema: "JsonSchema") -> None:
|
48
52
|
canonical_name = camelcase(name)
|
49
53
|
|
50
|
-
if
|
51
|
-
self[
|
54
|
+
if "$defs" not in self:
|
55
|
+
self["$defs"] = {}
|
52
56
|
|
53
|
-
if
|
54
|
-
subschema[
|
57
|
+
if "title" not in subschema:
|
58
|
+
subschema["title"] = canonical_name
|
55
59
|
|
56
|
-
self[
|
60
|
+
self["$defs"][canonical_name] = subschema
|
57
61
|
|
58
62
|
if canonical_name in self._lax_forward_refs:
|
59
63
|
identifier_name = self._lax_forward_refs.pop(canonical_name)
|
@@ -70,36 +74,36 @@ class JsonSchema(UserDict):
|
|
70
74
|
self._lax_forward_refs[canonical_name] = identifier_name
|
71
75
|
else:
|
72
76
|
lax_cls = deepcopy(self["$defs"][canonical_name])
|
73
|
-
lax_cls[
|
77
|
+
lax_cls["required"].remove(identifier_name)
|
74
78
|
self["$defs"][canonical_name + self.OPTIONAL_IDENTIFIER_SUFFIX] = lax_cls
|
75
79
|
|
76
80
|
def add_property(self, name: str, subschema: "JsonSchema", required: bool = False) -> None:
|
77
81
|
canonical_name = underscore(name)
|
78
|
-
|
79
|
-
if
|
80
|
-
self[
|
81
|
-
|
82
|
-
self[
|
82
|
+
|
83
|
+
if "properties" not in self:
|
84
|
+
self["properties"] = {}
|
85
|
+
|
86
|
+
self["properties"][canonical_name] = subschema
|
83
87
|
|
84
88
|
if required:
|
85
|
-
if
|
86
|
-
self[
|
87
|
-
|
88
|
-
self[
|
89
|
+
if "required" not in self:
|
90
|
+
self["required"] = []
|
91
|
+
|
92
|
+
self["required"].append(canonical_name)
|
89
93
|
|
90
94
|
def add_keyword(self, keyword: str, value: Any):
|
91
95
|
if value is None:
|
92
96
|
return
|
93
|
-
|
97
|
+
|
94
98
|
self[keyword] = value
|
95
99
|
|
96
100
|
@property
|
97
101
|
def is_array(self):
|
98
|
-
return self.get(
|
102
|
+
return self.get("type") == "array"
|
99
103
|
|
100
104
|
@property
|
101
105
|
def is_object(self):
|
102
|
-
return self.get(
|
106
|
+
return self.get("type") == "object"
|
103
107
|
|
104
108
|
def to_json(self, **kwargs) -> str:
|
105
109
|
return json.dumps(self.data, default=lambda d: d.data, **kwargs)
|
@@ -107,23 +111,18 @@ class JsonSchema(UserDict):
|
|
107
111
|
@classmethod
|
108
112
|
def ref_for(cls, class_name: Union[str, List[str]], identifier_optional: bool = False):
|
109
113
|
def _ref(class_name):
|
110
|
-
|
111
|
-
|
112
|
-
})
|
113
|
-
|
114
|
+
def_name = camelcase(class_name)
|
115
|
+
def_suffix = cls.OPTIONAL_IDENTIFIER_SUFFIX if identifier_optional else ""
|
116
|
+
return JsonSchema({"$ref": f"#/$defs/{def_name}{def_suffix}"})
|
117
|
+
|
114
118
|
if isinstance(class_name, list):
|
115
|
-
return JsonSchema({
|
116
|
-
"anyOf": [_ref(name) for name in class_name]
|
117
|
-
})
|
119
|
+
return JsonSchema({"anyOf": [_ref(name) for name in class_name]})
|
118
120
|
else:
|
119
121
|
return _ref(class_name)
|
120
122
|
|
121
123
|
@classmethod
|
122
124
|
def array_of(cls, subschema: "JsonSchema") -> "JsonSchema":
|
123
|
-
schema = {
|
124
|
-
"type": "array",
|
125
|
-
"items": subschema
|
126
|
-
}
|
125
|
+
schema = {"type": "array", "items": subschema}
|
127
126
|
|
128
127
|
return JsonSchema(schema)
|
129
128
|
|
@@ -145,8 +144,9 @@ class JsonSchemaGenerator(Generator):
|
|
145
144
|
generatorversion = "0.0.3"
|
146
145
|
valid_formats = ["json"]
|
147
146
|
uses_schemaloader = False
|
147
|
+
file_extension = "schema.json"
|
148
148
|
|
149
|
-
|
149
|
+
# @deprecated("Use top_class")
|
150
150
|
topClass: Optional[str] = None
|
151
151
|
|
152
152
|
not_closed: Optional[bool] = field(default_factory=lambda: True)
|
@@ -155,7 +155,7 @@ class JsonSchemaGenerator(Generator):
|
|
155
155
|
indent: int = 4
|
156
156
|
|
157
157
|
inline: bool = False
|
158
|
-
top_class: Optional[ClassDefinitionName] = None
|
158
|
+
top_class: Optional[ClassDefinitionName] = None # JSON object is one instance of this
|
159
159
|
"""Class instantiated by the root node of the document tree"""
|
160
160
|
|
161
161
|
include_range_class_descendants: bool = field(default_factory=lambda: False)
|
@@ -164,7 +164,7 @@ class JsonSchemaGenerator(Generator):
|
|
164
164
|
|
165
165
|
def __post_init__(self):
|
166
166
|
if self.topClass:
|
167
|
-
logging.warning(
|
167
|
+
logging.warning("topClass is deprecated - use top_class")
|
168
168
|
self.top_class = self.topClass
|
169
169
|
|
170
170
|
super().__post_init__()
|
@@ -172,36 +172,36 @@ class JsonSchemaGenerator(Generator):
|
|
172
172
|
def start_schema(self, inline: bool = False) -> JsonSchema:
|
173
173
|
self.inline = inline
|
174
174
|
|
175
|
-
self.top_level_schema = JsonSchema(
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
175
|
+
self.top_level_schema = JsonSchema(
|
176
|
+
{
|
177
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
178
|
+
"$id": self.schema.id,
|
179
|
+
"metamodel_version": metamodel_version,
|
180
|
+
"version": self.schema.version if self.schema.version else None,
|
181
|
+
"title": self.schema.name,
|
182
|
+
"type": "object",
|
183
|
+
"additionalProperties": self.not_closed,
|
184
|
+
}
|
185
|
+
)
|
184
186
|
|
185
187
|
def handle_class(self, cls: ClassDefinition) -> None:
|
186
188
|
if cls.mixin or cls.abstract:
|
187
189
|
return
|
188
|
-
|
190
|
+
|
189
191
|
additional_properties = False
|
190
192
|
if self.is_class_unconstrained(cls):
|
191
193
|
additional_properties = True
|
192
|
-
|
193
|
-
class_subschema = JsonSchema(
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
194
|
+
|
195
|
+
class_subschema = JsonSchema(
|
196
|
+
{
|
197
|
+
"type": "object",
|
198
|
+
"additionalProperties": additional_properties,
|
199
|
+
"description": be(cls.description),
|
200
|
+
}
|
201
|
+
)
|
198
202
|
|
199
203
|
for slot_definition in self.schemaview.class_induced_slots(cls.name):
|
200
|
-
self.handle_class_slot(
|
201
|
-
subschema=class_subschema,
|
202
|
-
cls=cls,
|
203
|
-
slot=slot_definition
|
204
|
-
)
|
204
|
+
self.handle_class_slot(subschema=class_subschema, cls=cls, slot=slot_definition)
|
205
205
|
|
206
206
|
rule_subschemas = []
|
207
207
|
for rule in cls.rules:
|
@@ -211,16 +211,22 @@ class JsonSchemaGenerator(Generator):
|
|
211
211
|
if open_world is None:
|
212
212
|
open_world = False
|
213
213
|
|
214
|
-
if_subschema = self.get_subschema_for_anonymous_class(
|
214
|
+
if_subschema = self.get_subschema_for_anonymous_class(
|
215
|
+
rule.preconditions, properties_required=True
|
216
|
+
)
|
215
217
|
if if_subschema:
|
216
218
|
subschema["if"] = if_subschema
|
217
|
-
|
218
|
-
then_subschema = self.get_subschema_for_anonymous_class(
|
219
|
+
|
220
|
+
then_subschema = self.get_subschema_for_anonymous_class(
|
221
|
+
rule.postconditions, properties_required=not open_world
|
222
|
+
)
|
219
223
|
if then_subschema:
|
220
224
|
subschema["then"] = then_subschema
|
221
225
|
|
222
226
|
# same as required requirements as postconditions?
|
223
|
-
else_subschema = self.get_subschema_for_anonymous_class(
|
227
|
+
else_subschema = self.get_subschema_for_anonymous_class(
|
228
|
+
rule.elseconditions, properties_required=not open_world
|
229
|
+
)
|
224
230
|
if else_subschema:
|
225
231
|
subschema["else"] = else_subschema
|
226
232
|
|
@@ -231,7 +237,7 @@ class JsonSchemaGenerator(Generator):
|
|
231
237
|
|
232
238
|
if then_subschema:
|
233
239
|
inverse_subschema["if"] = then_subschema
|
234
|
-
|
240
|
+
|
235
241
|
if if_subschema:
|
236
242
|
inverse_subschema["then"] = if_subschema
|
237
243
|
|
@@ -246,17 +252,19 @@ class JsonSchemaGenerator(Generator):
|
|
246
252
|
|
247
253
|
self.top_level_schema.add_def(cls.name, class_subschema)
|
248
254
|
|
249
|
-
if (
|
250
|
-
self.top_class is
|
251
|
-
)
|
255
|
+
if (self.top_class is not None and camelcase(self.top_class) == camelcase(cls.name)) or (
|
256
|
+
self.top_class is None and cls.tree_root
|
257
|
+
):
|
252
258
|
for key, value in class_subschema.items():
|
253
259
|
# check this first to ensure we don't overwrite things like additionalProperties
|
254
|
-
# or description on the root. But we do want to copy over properties, required,
|
260
|
+
# or description on the root. But we do want to copy over properties, required,
|
255
261
|
# if, then, etc.
|
256
262
|
if key not in self.top_level_schema:
|
257
263
|
self.top_level_schema[key] = value
|
258
264
|
|
259
|
-
def get_subschema_for_anonymous_class(
|
265
|
+
def get_subschema_for_anonymous_class(
|
266
|
+
self, cls: AnonymousClassExpression, properties_required: bool = False
|
267
|
+
) -> Union[None, JsonSchema]:
|
260
268
|
if not cls:
|
261
269
|
return None
|
262
270
|
|
@@ -266,32 +274,41 @@ class JsonSchemaGenerator(Generator):
|
|
266
274
|
subschema.add_property(self.aliased_slot_name(slot), prop, properties_required)
|
267
275
|
|
268
276
|
if cls.any_of is not None and len(cls.any_of) > 0:
|
269
|
-
subschema["anyOf"] = [
|
277
|
+
subschema["anyOf"] = [
|
278
|
+
self.get_subschema_for_anonymous_class(c, properties_required) for c in cls.any_of
|
279
|
+
]
|
270
280
|
|
271
281
|
if cls.all_of is not None and len(cls.all_of) > 0:
|
272
|
-
subschema["allOf"] = [
|
282
|
+
subschema["allOf"] = [
|
283
|
+
self.get_subschema_for_anonymous_class(c, properties_required) for c in cls.all_of
|
284
|
+
]
|
273
285
|
|
274
286
|
if cls.exactly_one_of is not None and len(cls.exactly_one_of) > 0:
|
275
|
-
subschema["oneOf"] = [
|
287
|
+
subschema["oneOf"] = [
|
288
|
+
self.get_subschema_for_anonymous_class(c, properties_required)
|
289
|
+
for c in cls.exactly_one_of
|
290
|
+
]
|
276
291
|
|
277
292
|
if cls.none_of is not None and len(cls.none_of) > 0:
|
278
293
|
subschema["not"] = {
|
279
|
-
"anyOf": [
|
294
|
+
"anyOf": [
|
295
|
+
self.get_subschema_for_anonymous_class(c, properties_required)
|
296
|
+
for c in cls.any_of
|
297
|
+
]
|
280
298
|
}
|
281
299
|
|
282
300
|
return subschema
|
283
301
|
|
284
|
-
|
285
302
|
def handle_enum(self, enum: EnumDefinition) -> None:
|
286
303
|
# TODO: this only works with explicitly permitted values. It will need to be extended to
|
287
304
|
# support other pv_formula
|
288
305
|
|
289
306
|
def extract_permissible_text(pv):
|
290
|
-
if
|
307
|
+
if isinstance(pv, str):
|
291
308
|
return pv
|
292
|
-
if
|
309
|
+
if isinstance(pv, PermissibleValue):
|
293
310
|
return pv.text.code
|
294
|
-
if
|
311
|
+
if isinstance(pv, PermissibleValueText):
|
295
312
|
return pv
|
296
313
|
raise ValueError(f"Invalid permissible value in enum {enum}: {pv}")
|
297
314
|
|
@@ -299,17 +316,25 @@ class JsonSchemaGenerator(Generator):
|
|
299
316
|
map(extract_permissible_text, enum.permissible_values or [])
|
300
317
|
)
|
301
318
|
|
302
|
-
enum_schema = JsonSchema(
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
319
|
+
enum_schema = JsonSchema(
|
320
|
+
{
|
321
|
+
"type": "string",
|
322
|
+
"description": be(enum.description),
|
323
|
+
}
|
324
|
+
)
|
325
|
+
if permissible_values_texts:
|
326
|
+
enum_schema["enum"] = permissible_values_texts
|
307
327
|
self.top_level_schema.add_def(enum.name, enum_schema)
|
308
328
|
|
309
|
-
def get_type_info_for_slot_subschema(
|
310
|
-
|
311
|
-
|
312
|
-
|
329
|
+
def get_type_info_for_slot_subschema(
|
330
|
+
self, slot: AnonymousSlotExpression
|
331
|
+
) -> Tuple[str, str, Union[str, List[str]]]:
|
332
|
+
# JSON Schema type (https://json-schema.org/understanding-json-schema/reference/type.html)
|
333
|
+
typ = None
|
334
|
+
# Reference to a JSON schema entity (https://json-schema.org/understanding-json-schema/structuring.html#ref)
|
335
|
+
reference = None
|
336
|
+
# JSON Schema format (https://json-schema.org/understanding-json-schema/reference/string.html#format)
|
337
|
+
fmt = None
|
313
338
|
|
314
339
|
slot_is_inlined = self.schemaview.is_inlined(slot)
|
315
340
|
if slot.range in self.schemaview.all_types().keys():
|
@@ -318,41 +343,50 @@ class JsonSchemaGenerator(Generator):
|
|
318
343
|
elif slot.range in self.schemaview.all_enums().keys():
|
319
344
|
reference = slot.range
|
320
345
|
elif slot.range in self.schemaview.all_classes().keys():
|
321
|
-
if slot_is_inlined
|
322
|
-
descendants = [
|
323
|
-
|
346
|
+
if slot_is_inlined:
|
347
|
+
descendants = [
|
348
|
+
desc
|
349
|
+
for desc in self.schemaview.class_descendants(slot.range)
|
350
|
+
if not self.schemaview.get_class(desc).abstract
|
351
|
+
]
|
324
352
|
if descendants and self.include_range_class_descendants:
|
325
353
|
reference = descendants
|
326
354
|
else:
|
327
355
|
reference = slot.range
|
328
356
|
else:
|
329
357
|
id_slot = self.schemaview.get_identifier_slot(slot.range)
|
330
|
-
|
358
|
+
return self.get_type_info_for_slot_subschema(id_slot)
|
331
359
|
|
332
360
|
return (typ, fmt, reference)
|
333
|
-
|
334
|
-
def get_value_constraints_for_slot(
|
361
|
+
|
362
|
+
def get_value_constraints_for_slot(
|
363
|
+
self, slot: Union[AnonymousSlotExpression, None]
|
364
|
+
) -> JsonSchema:
|
335
365
|
if slot is None:
|
336
366
|
return JsonSchema()
|
337
|
-
|
367
|
+
|
338
368
|
constraints = JsonSchema()
|
339
|
-
constraints.add_keyword(
|
340
|
-
constraints.add_keyword(
|
341
|
-
constraints.add_keyword(
|
342
|
-
constraints.add_keyword(
|
343
|
-
constraints.add_keyword(
|
369
|
+
constraints.add_keyword("pattern", slot.pattern)
|
370
|
+
constraints.add_keyword("minimum", slot.minimum_value)
|
371
|
+
constraints.add_keyword("maximum", slot.maximum_value)
|
372
|
+
constraints.add_keyword("const", slot.equals_string)
|
373
|
+
constraints.add_keyword("const", slot.equals_number)
|
344
374
|
return constraints
|
345
375
|
|
346
376
|
def get_subschema_for_slot(self, slot: SlotDefinition, omit_type: bool = False) -> JsonSchema:
|
347
377
|
prop = JsonSchema()
|
348
|
-
slot_is_multivalued =
|
378
|
+
slot_is_multivalued = "multivalued" in slot and slot.multivalued
|
349
379
|
slot_is_inlined = self.schemaview.is_inlined(slot)
|
350
380
|
if not omit_type:
|
351
|
-
typ, fmt, reference = self.get_type_info_for_slot_subschema(slot
|
381
|
+
typ, fmt, reference = self.get_type_info_for_slot_subschema(slot)
|
352
382
|
if slot_is_inlined:
|
353
383
|
# If inline we have to include redefined slots
|
354
384
|
if slot_is_multivalued:
|
355
|
-
|
385
|
+
(
|
386
|
+
range_id_slot,
|
387
|
+
range_simple_dict_value_slot,
|
388
|
+
range_required_slots,
|
389
|
+
) = self._get_range_associated_slots(slot)
|
356
390
|
# if the range class has an ID and the slot is not inlined as a list, then we need to consider
|
357
391
|
# various inlined as dict formats
|
358
392
|
if range_id_slot is not None and not slot.inlined_as_list:
|
@@ -363,7 +397,9 @@ class JsonSchemaGenerator(Generator):
|
|
363
397
|
# If the range can be collected as a simple dict, then we can also accept the value
|
364
398
|
# of that simple dict directly.
|
365
399
|
if range_simple_dict_value_slot is not None:
|
366
|
-
additionalProps.append(
|
400
|
+
additionalProps.append(
|
401
|
+
self.get_subschema_for_slot(range_simple_dict_value_slot)
|
402
|
+
)
|
367
403
|
|
368
404
|
# If the range has no required slots, then null is acceptable
|
369
405
|
if len(range_required_slots) == 0:
|
@@ -374,14 +410,13 @@ class JsonSchemaGenerator(Generator):
|
|
374
410
|
if len(additionalProps) == 1:
|
375
411
|
additionalProps = additionalProps[0]
|
376
412
|
else:
|
377
|
-
additionalProps = JsonSchema({
|
378
|
-
|
379
|
-
}
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
self.top_level_schema.add_lax_def(reference, self.aliased_slot_name(range_id_slot))
|
413
|
+
additionalProps = JsonSchema({"anyOf": additionalProps})
|
414
|
+
prop = JsonSchema(
|
415
|
+
{"type": "object", "additionalProperties": additionalProps}
|
416
|
+
)
|
417
|
+
self.top_level_schema.add_lax_def(
|
418
|
+
reference, self.aliased_slot_name(range_id_slot)
|
419
|
+
)
|
385
420
|
else:
|
386
421
|
prop = JsonSchema.array_of(JsonSchema.ref_for(reference))
|
387
422
|
else:
|
@@ -397,15 +432,15 @@ class JsonSchemaGenerator(Generator):
|
|
397
432
|
if slot_is_multivalued:
|
398
433
|
prop = JsonSchema.array_of(prop)
|
399
434
|
|
400
|
-
prop.add_keyword(
|
435
|
+
prop.add_keyword("description", slot.description)
|
401
436
|
|
402
437
|
own_constraints = self.get_value_constraints_for_slot(slot)
|
403
438
|
|
404
439
|
if prop.is_array:
|
405
440
|
all_element_constraints = self.get_value_constraints_for_slot(slot.all_members)
|
406
441
|
any_element_constraints = self.get_value_constraints_for_slot(slot.has_member)
|
407
|
-
prop.add_keyword(
|
408
|
-
prop.add_keyword(
|
442
|
+
prop.add_keyword("minItems", slot.minimum_cardinality)
|
443
|
+
prop.add_keyword("maxItems", slot.maximum_cardinality)
|
409
444
|
prop["items"].update(own_constraints)
|
410
445
|
prop["items"].update(all_element_constraints)
|
411
446
|
if any_element_constraints:
|
@@ -414,37 +449,38 @@ class JsonSchemaGenerator(Generator):
|
|
414
449
|
prop.update(own_constraints)
|
415
450
|
|
416
451
|
if prop.is_object:
|
417
|
-
prop.add_keyword(
|
418
|
-
prop.add_keyword(
|
452
|
+
prop.add_keyword("minProperties", slot.minimum_cardinality)
|
453
|
+
prop.add_keyword("maxProperties", slot.maximum_cardinality)
|
419
454
|
|
420
455
|
bool_subschema = JsonSchema()
|
421
456
|
if slot.any_of is not None and len(slot.any_of) > 0:
|
422
|
-
bool_subschema[
|
457
|
+
bool_subschema["anyOf"] = [self.get_subschema_for_slot(s) for s in slot.any_of]
|
423
458
|
|
424
459
|
if slot.all_of is not None and len(slot.all_of) > 0:
|
425
|
-
bool_subschema[
|
460
|
+
bool_subschema["allOf"] = [self.get_subschema_for_slot(s) for s in slot.all_of]
|
426
461
|
|
427
462
|
if slot.exactly_one_of is not None and len(slot.exactly_one_of) > 0:
|
428
|
-
bool_subschema[
|
463
|
+
bool_subschema["oneOf"] = [self.get_subschema_for_slot(s) for s in slot.exactly_one_of]
|
429
464
|
|
430
465
|
if slot.none_of is not None and len(slot.none_of) > 0:
|
431
|
-
bool_subschema[
|
432
|
-
|
466
|
+
bool_subschema["not"] = {
|
467
|
+
"anyOf": [self.get_subschema_for_slot(s) for s in slot.none_of]
|
433
468
|
}
|
434
469
|
|
435
470
|
if bool_subschema:
|
436
471
|
if prop.is_array:
|
437
|
-
if
|
438
|
-
prop[
|
472
|
+
if "items" not in prop:
|
473
|
+
prop["items"] = {}
|
439
474
|
prop["type"] = "array"
|
440
|
-
prop[
|
475
|
+
prop["items"].update(bool_subschema)
|
441
476
|
else:
|
442
477
|
prop.update(bool_subschema)
|
443
478
|
|
444
479
|
return prop
|
445
480
|
|
446
|
-
|
447
|
-
|
481
|
+
def handle_class_slot(
|
482
|
+
self, subschema: JsonSchema, cls: ClassDefinition, slot: SlotDefinition
|
483
|
+
) -> None:
|
448
484
|
class_id_slot = self.schemaview.get_identifier_slot(cls.name, use_key=True)
|
449
485
|
slot_is_required = slot.required or slot == class_id_slot
|
450
486
|
|
@@ -452,7 +488,7 @@ class JsonSchemaGenerator(Generator):
|
|
452
488
|
prop = self.get_subschema_for_slot(slot)
|
453
489
|
subschema.add_property(aliased_slot_name, prop, slot_is_required)
|
454
490
|
|
455
|
-
def
|
491
|
+
def generate(self) -> dict:
|
456
492
|
self.start_schema()
|
457
493
|
for enum_definition in self.schemaview.all_enums().values():
|
458
494
|
self.handle_enum(enum_definition)
|
@@ -460,26 +496,37 @@ class JsonSchemaGenerator(Generator):
|
|
460
496
|
for class_definition in self.schemaview.all_classes().values():
|
461
497
|
self.handle_class(class_definition)
|
462
498
|
|
463
|
-
return self.top_level_schema
|
464
|
-
|
465
|
-
def
|
499
|
+
return self.top_level_schema
|
500
|
+
|
501
|
+
def serialize(self, **kwargs) -> str:
|
502
|
+
return self.generate().to_json(
|
503
|
+
sort_keys=True, indent=self.indent if self.indent > 0 else None
|
504
|
+
)
|
505
|
+
|
506
|
+
def _get_range_associated_slots(
|
507
|
+
self, slot: SlotDefinition
|
508
|
+
) -> Tuple[
|
509
|
+
Union[SlotDefinition, None], Union[SlotDefinition, None], Union[List[SlotDefinition], None]
|
510
|
+
]:
|
466
511
|
range_class = self.schemaview.get_class(slot.range)
|
467
512
|
if range_class is None:
|
468
513
|
return None, None, None
|
469
|
-
|
514
|
+
|
470
515
|
range_class_id_slot = self.schemaview.get_identifier_slot(range_class.name, use_key=True)
|
471
516
|
if range_class_id_slot is None:
|
472
517
|
return None, None, None
|
473
|
-
|
518
|
+
|
474
519
|
non_id_slots = [
|
475
|
-
s
|
520
|
+
s
|
521
|
+
for s in self.schemaview.class_induced_slots(range_class.name)
|
522
|
+
if s.name != range_class_id_slot.name
|
476
523
|
]
|
477
524
|
non_id_required_slots = [s for s in non_id_slots if s.required]
|
478
525
|
|
479
526
|
range_simple_dict_value_slot = None
|
480
527
|
if len(non_id_slots) == 1:
|
481
528
|
range_simple_dict_value_slot = non_id_slots[0]
|
482
|
-
|
529
|
+
|
483
530
|
return range_class_id_slot, range_simple_dict_value_slot, non_id_required_slots
|
484
531
|
|
485
532
|
|
@@ -524,7 +571,7 @@ When handling range constraints, include all descendants of the range class inst
|
|
524
571
|
help="""
|
525
572
|
If this is a positive number the resulting JSON will be pretty-printed with that indent level. Set to 0 to
|
526
573
|
disable pretty-printing and return the most compact JSON representation
|
527
|
-
"""
|
574
|
+
""",
|
528
575
|
)
|
529
576
|
@click.version_option(__version__, "-V", "--version")
|
530
577
|
def cli(yamlfile, **kwargs):
|
linkml/generators/linkmlgen.py
CHANGED
@@ -39,7 +39,7 @@ class LinkmlGenerator(Generator):
|
|
39
39
|
self.schemaview = SchemaView(self.schema, merge_imports=self.mergeimports)
|
40
40
|
|
41
41
|
def materialize_classes(self) -> None:
|
42
|
-
"""Materialize class slots from schema as
|
42
|
+
"""Materialize class slots from schema as attributes, in place"""
|
43
43
|
all_classes = self.schemaview.all_classes()
|
44
44
|
|
45
45
|
for c_name, c_def in all_classes.items():
|