linkml 1.8.4__py3-none-any.whl → 1.8.6__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/cli/main.py +2 -0
- linkml/generators/common/build.py +1 -2
- linkml/generators/common/ifabsent_processor.py +98 -21
- linkml/generators/common/lifecycle.py +18 -2
- linkml/generators/common/naming.py +106 -0
- linkml/generators/dbmlgen.py +173 -0
- linkml/generators/docgen.py +16 -7
- linkml/generators/erdiagramgen.py +1 -0
- linkml/generators/graphqlgen.py +34 -2
- linkml/generators/jsonldcontextgen.py +7 -1
- linkml/generators/jsonschemagen.py +73 -53
- linkml/generators/linkmlgen.py +13 -1
- linkml/generators/owlgen.py +11 -1
- linkml/generators/plantumlgen.py +17 -10
- linkml/generators/pydanticgen/array.py +21 -61
- linkml/generators/pydanticgen/template.py +12 -1
- linkml/generators/pydanticgen/templates/attribute.py.jinja +1 -1
- linkml/generators/python/python_ifabsent_processor.py +1 -1
- linkml/generators/pythongen.py +123 -21
- linkml/generators/shaclgen.py +16 -5
- linkml/generators/typescriptgen.py +3 -1
- linkml/linter/rules.py +3 -1
- linkml/utils/converter.py +17 -0
- linkml/utils/deprecation.py +10 -0
- linkml/utils/helpers.py +65 -0
- linkml/utils/validation.py +2 -1
- linkml/validator/__init__.py +2 -2
- linkml/validator/plugins/jsonschema_validation_plugin.py +1 -0
- linkml/validator/report.py +4 -1
- linkml/validator/validator.py +4 -4
- linkml/validators/jsonschemavalidator.py +10 -0
- linkml/validators/sparqlvalidator.py +7 -0
- linkml/workspaces/example_runner.py +20 -1
- {linkml-1.8.4.dist-info → linkml-1.8.6.dist-info}/METADATA +2 -2
- {linkml-1.8.4.dist-info → linkml-1.8.6.dist-info}/RECORD +38 -36
- {linkml-1.8.4.dist-info → linkml-1.8.6.dist-info}/entry_points.txt +1 -0
- {linkml-1.8.4.dist-info → linkml-1.8.6.dist-info}/LICENSE +0 -0
- {linkml-1.8.4.dist-info → linkml-1.8.6.dist-info}/WHEEL +0 -0
linkml/generators/graphqlgen.py
CHANGED
@@ -1,11 +1,14 @@
|
|
1
|
+
import logging
|
1
2
|
import os
|
3
|
+
import re
|
2
4
|
from dataclasses import dataclass
|
3
5
|
|
4
6
|
import click
|
5
|
-
from linkml_runtime.linkml_model.meta import ClassDefinition, SlotDefinition
|
7
|
+
from linkml_runtime.linkml_model.meta import ClassDefinition, EnumDefinition, SlotDefinition
|
6
8
|
from linkml_runtime.utils.formatutils import camelcase, lcamelcase
|
7
9
|
|
8
10
|
from linkml._version import __version__
|
11
|
+
from linkml.generators.common.naming import NameCompatibility, NamingProfiles
|
9
12
|
from linkml.utils.generator import Generator, shared_arguments
|
10
13
|
|
11
14
|
|
@@ -19,6 +22,13 @@ class GraphqlGenerator(Generator):
|
|
19
22
|
uses_schemaloader = True
|
20
23
|
requires_metamodel = False
|
21
24
|
|
25
|
+
strict_naming: bool = False
|
26
|
+
_permissible_value_valid_characters = re.compile("^[_A-Za-z][_0-9A-Za-z]*?$")
|
27
|
+
|
28
|
+
def __post_init__(self):
|
29
|
+
self.name_compatiblity = NameCompatibility(profile=NamingProfiles.graphql, do_not_fix=self.strict_naming)
|
30
|
+
super().__post_init__()
|
31
|
+
|
22
32
|
def visit_schema(self, **kwargs) -> str:
|
23
33
|
return self.generate_header()
|
24
34
|
|
@@ -50,13 +60,35 @@ class GraphqlGenerator(Generator):
|
|
50
60
|
slotrange = slotrange + "!"
|
51
61
|
return f"\n {lcamelcase(aliased_slot_name)}: {slotrange}"
|
52
62
|
|
63
|
+
def visit_enum(self, enum: EnumDefinition):
|
64
|
+
if enum.permissible_values:
|
65
|
+
permissible_values = []
|
66
|
+
for value in enum.permissible_values:
|
67
|
+
permissible_values.append(self.name_compatiblity.compatible(value))
|
68
|
+
values = "\n ".join(permissible_values)
|
69
|
+
return f"enum {camelcase(enum.name).replace(' ','')}\n {{\n {values}\n }}\n\n"
|
70
|
+
else:
|
71
|
+
logging.warning(
|
72
|
+
f"Enumeration {enum.name} using `reachable_from` instead of `permissible_values` "
|
73
|
+
+ "to specify permissible values is not supported yet."
|
74
|
+
+ "Enumeration {enum.name} will be silently ignored!!"
|
75
|
+
)
|
76
|
+
return ""
|
77
|
+
|
53
78
|
|
54
79
|
@shared_arguments(GraphqlGenerator)
|
55
80
|
@click.command(name="graphql")
|
81
|
+
@click.option(
|
82
|
+
"--strict-naming",
|
83
|
+
is_flag=True,
|
84
|
+
show_default=True,
|
85
|
+
help="Treat warnings about invalid names or schema elements as errors.",
|
86
|
+
)
|
56
87
|
@click.version_option(__version__, "-V", "--version")
|
57
88
|
def cli(yamlfile, **args):
|
58
89
|
"""Generate graphql representation of a LinkML model"""
|
59
|
-
|
90
|
+
generator = GraphqlGenerator(yamlfile, **args)
|
91
|
+
print(generator.serialize(**args))
|
60
92
|
|
61
93
|
|
62
94
|
if __name__ == "__main__":
|
@@ -146,7 +146,13 @@ class ContextGenerator(Generator):
|
|
146
146
|
slot_def = {}
|
147
147
|
if not slot.usage_slot_name:
|
148
148
|
any_of_ranges = [any_of_el.range for any_of_el in slot.any_of]
|
149
|
-
if slot.range in self.schema.classes
|
149
|
+
if slot.range in self.schema.classes:
|
150
|
+
range_class_uri = self.schema.classes[slot.range].class_uri
|
151
|
+
if range_class_uri and slot.inlined:
|
152
|
+
slot_def["@type"] = range_class_uri
|
153
|
+
else:
|
154
|
+
slot_def["@type"] = "@id"
|
155
|
+
elif any(rng in self.schema.classes for rng in any_of_ranges):
|
150
156
|
slot_def["@type"] = "@id"
|
151
157
|
elif slot.range in self.schema.enums:
|
152
158
|
slot_def["@context"] = ENUM_CONTEXT
|
@@ -21,8 +21,11 @@ from linkml_runtime.linkml_model.meta import (
|
|
21
21
|
from linkml_runtime.utils.formatutils import be, camelcase, underscore
|
22
22
|
|
23
23
|
from linkml._version import __version__
|
24
|
+
from linkml.generators.common import build
|
25
|
+
from linkml.generators.common.lifecycle import LifecycleMixin
|
24
26
|
from linkml.generators.common.type_designators import get_type_designator_value
|
25
27
|
from linkml.utils.generator import Generator, shared_arguments
|
28
|
+
from linkml.utils.helpers import get_range_associated_slots
|
26
29
|
|
27
30
|
logger = logging.getLogger(__name__)
|
28
31
|
|
@@ -177,8 +180,32 @@ class JsonSchema(dict):
|
|
177
180
|
return JsonSchema(schema)
|
178
181
|
|
179
182
|
|
183
|
+
class SchemaResult(build.SchemaResult):
|
184
|
+
"""Top-level result of building a json schema"""
|
185
|
+
|
186
|
+
schema_: JsonSchema
|
187
|
+
|
188
|
+
|
189
|
+
class EnumResult(build.EnumResult):
|
190
|
+
"""A single built enum"""
|
191
|
+
|
192
|
+
schema_: JsonSchema
|
193
|
+
|
194
|
+
|
195
|
+
class ClassResult(build.ClassResult):
|
196
|
+
"""A single built class"""
|
197
|
+
|
198
|
+
schema_: JsonSchema
|
199
|
+
|
200
|
+
|
201
|
+
class SlotResult(build.SlotResult):
|
202
|
+
"""A slot within the context of a class"""
|
203
|
+
|
204
|
+
schema_: JsonSchema
|
205
|
+
|
206
|
+
|
180
207
|
@dataclass
|
181
|
-
class JsonSchemaGenerator(Generator):
|
208
|
+
class JsonSchemaGenerator(Generator, LifecycleMixin):
|
182
209
|
"""
|
183
210
|
Generates JSONSchema documents from a LinkML SchemaDefinition
|
184
211
|
|
@@ -187,6 +214,21 @@ class JsonSchemaGenerator(Generator):
|
|
187
214
|
- Composition not yet implemented
|
188
215
|
- Enumerations treated as strings
|
189
216
|
- Foreign key references are treated as semantics-free strings
|
217
|
+
|
218
|
+
This generator implements the following :class:`.LifecycleMixin` methods:
|
219
|
+
|
220
|
+
* :meth:`.LifecycleMixin.before_generate_schema`
|
221
|
+
* :meth:`.LifecycleMixin.after_generate_schema`
|
222
|
+
* :meth:`.LifecycleMixin.before_generate_classes`
|
223
|
+
* :meth:`.LifecycleMixin.before_generate_enums`
|
224
|
+
* :meth:`.LifecycleMixin.before_generate_class_slots`
|
225
|
+
* :meth:`.LifecycleMixin.before_generate_class`
|
226
|
+
* :meth:`.LifecycleMixin.after_generate_class`
|
227
|
+
* :meth:`.LifecycleMixin.before_generate_class_slot`
|
228
|
+
* :meth:`.LifecycleMixin.after_generate_class_slot`
|
229
|
+
* :meth:`.LifecycleMixin.before_generate_enum`
|
230
|
+
* :meth:`.LifecycleMixin.after_generate_enum`
|
231
|
+
|
190
232
|
"""
|
191
233
|
|
192
234
|
# ClassVars
|
@@ -233,7 +275,7 @@ class JsonSchemaGenerator(Generator):
|
|
233
275
|
if self.schemaview.get_class(self.top_class) is None:
|
234
276
|
logger.warning(f"No class in schema named {self.top_class}")
|
235
277
|
|
236
|
-
def start_schema(self, inline: bool = False)
|
278
|
+
def start_schema(self, inline: bool = False):
|
237
279
|
self.inline = inline
|
238
280
|
|
239
281
|
self.top_level_schema = JsonSchema(
|
@@ -249,6 +291,8 @@ class JsonSchemaGenerator(Generator):
|
|
249
291
|
)
|
250
292
|
|
251
293
|
def handle_class(self, cls: ClassDefinition) -> None:
|
294
|
+
cls = self.before_generate_class(cls, self.schemaview)
|
295
|
+
|
252
296
|
if cls.mixin or cls.abstract:
|
253
297
|
return
|
254
298
|
|
@@ -268,7 +312,10 @@ class JsonSchemaGenerator(Generator):
|
|
268
312
|
if self.title_from == "title" and cls.title:
|
269
313
|
class_subschema["title"] = cls.title
|
270
314
|
|
271
|
-
|
315
|
+
class_slots = self.before_generate_class_slots(
|
316
|
+
self.schemaview.class_induced_slots(cls.name), cls, self.schemaview
|
317
|
+
)
|
318
|
+
for slot_definition in class_slots:
|
272
319
|
self.handle_class_slot(subschema=class_subschema, cls=cls, slot=slot_definition)
|
273
320
|
|
274
321
|
rule_subschemas = []
|
@@ -318,6 +365,10 @@ class JsonSchemaGenerator(Generator):
|
|
318
365
|
class_subschema["allOf"] = []
|
319
366
|
class_subschema["allOf"].extend(rule_subschemas)
|
320
367
|
|
368
|
+
class_subschema = self.after_generate_class(
|
369
|
+
ClassResult.model_construct(schema_=class_subschema, source=cls), self.schemaview
|
370
|
+
).schema_
|
371
|
+
|
321
372
|
self.top_level_schema.add_def(cls.name, class_subschema)
|
322
373
|
|
323
374
|
if (self.top_class is not None and camelcase(self.top_class) == camelcase(cls.name)) or (
|
@@ -375,6 +426,7 @@ class JsonSchemaGenerator(Generator):
|
|
375
426
|
def handle_enum(self, enum: EnumDefinition) -> None:
|
376
427
|
# TODO: this only works with explicitly permitted values. It will need to be extended to
|
377
428
|
# support other pv_formula
|
429
|
+
enum = self.before_generate_enum(enum, self.schemaview)
|
378
430
|
|
379
431
|
def extract_permissible_text(pv):
|
380
432
|
if isinstance(pv, str):
|
@@ -398,6 +450,10 @@ class JsonSchemaGenerator(Generator):
|
|
398
450
|
|
399
451
|
if permissible_values_texts:
|
400
452
|
enum_schema["enum"] = permissible_values_texts
|
453
|
+
|
454
|
+
enum_schema = self.after_generate_enum(
|
455
|
+
EnumResult.model_construct(schema_=enum_schema, source=enum), self.schemaview
|
456
|
+
).schema_
|
401
457
|
self.top_level_schema.add_def(enum.name, enum_schema)
|
402
458
|
|
403
459
|
def get_type_info_for_slot_subschema(
|
@@ -490,7 +546,7 @@ class JsonSchemaGenerator(Generator):
|
|
490
546
|
range_id_slot,
|
491
547
|
range_simple_dict_value_slot,
|
492
548
|
range_required_slots,
|
493
|
-
) = self.
|
549
|
+
) = get_range_associated_slots(self.schemaview, slot.range)
|
494
550
|
# if the range class has an ID and the slot is not inlined as a list, then we need to consider
|
495
551
|
# various inlined as dict formats
|
496
552
|
if range_id_slot is not None and not slot.inlined_as_list:
|
@@ -596,6 +652,7 @@ class JsonSchemaGenerator(Generator):
|
|
596
652
|
return prop
|
597
653
|
|
598
654
|
def handle_class_slot(self, subschema: JsonSchema, cls: ClassDefinition, slot: SlotDefinition) -> None:
|
655
|
+
slot = self.before_generate_class_slot(slot, cls, self.schemaview)
|
599
656
|
class_id_slot = self.schemaview.get_identifier_slot(cls.name, use_key=True)
|
600
657
|
value_required = (
|
601
658
|
slot.required or slot == class_id_slot or slot.value_presence == PresenceEnum(PresenceEnum.PRESENT)
|
@@ -604,6 +661,9 @@ class JsonSchemaGenerator(Generator):
|
|
604
661
|
|
605
662
|
aliased_slot_name = self.aliased_slot_name(slot)
|
606
663
|
prop = self.get_subschema_for_slot(slot, include_null=self.include_null)
|
664
|
+
prop = self.after_generate_class_slot(
|
665
|
+
SlotResult.model_construct(schema_=prop, source=slot), cls, self.schemaview
|
666
|
+
).schema_
|
607
667
|
subschema.add_property(
|
608
668
|
aliased_slot_name, prop, value_required=value_required, value_disallowed=value_disallowed
|
609
669
|
)
|
@@ -613,65 +673,25 @@ class JsonSchemaGenerator(Generator):
|
|
613
673
|
prop["enum"] = [type_value]
|
614
674
|
|
615
675
|
def generate(self) -> JsonSchema:
|
676
|
+
self.schema = self.before_generate_schema(self.schema, self.schemaview)
|
616
677
|
self.start_schema()
|
617
|
-
|
678
|
+
|
679
|
+
all_enums = self.before_generate_enums(self.schemaview.all_enums().values(), self.schemaview)
|
680
|
+
for enum_definition in all_enums:
|
618
681
|
self.handle_enum(enum_definition)
|
619
682
|
|
620
|
-
|
683
|
+
all_classes = self.before_generate_classes(self.schemaview.all_classes().values(), self.schemaview)
|
684
|
+
for class_definition in all_classes:
|
621
685
|
self.handle_class(class_definition)
|
622
686
|
|
687
|
+
self.top_level_schema = self.after_generate_schema(
|
688
|
+
SchemaResult.model_construct(schema_=self.top_level_schema, source=self.schema), self.schemaview
|
689
|
+
).schema_
|
623
690
|
return self.top_level_schema
|
624
691
|
|
625
692
|
def serialize(self, **kwargs) -> str:
|
626
693
|
return self.generate().to_json(sort_keys=True, indent=self.indent if self.indent > 0 else None)
|
627
694
|
|
628
|
-
def _get_range_associated_slots(
|
629
|
-
self, slot: SlotDefinition
|
630
|
-
) -> Tuple[Union[SlotDefinition, None], Union[SlotDefinition, None], Union[List[SlotDefinition], None]]:
|
631
|
-
range_class = self.schemaview.get_class(slot.range)
|
632
|
-
if range_class is None:
|
633
|
-
return None, None, None
|
634
|
-
|
635
|
-
range_class_id_slot = self.schemaview.get_identifier_slot(range_class.name, use_key=True)
|
636
|
-
if range_class_id_slot is None:
|
637
|
-
return None, None, None
|
638
|
-
|
639
|
-
non_id_slots = [
|
640
|
-
s for s in self.schemaview.class_induced_slots(range_class.name) if s.name != range_class_id_slot.name
|
641
|
-
]
|
642
|
-
non_id_required_slots = [s for s in non_id_slots if s.required]
|
643
|
-
|
644
|
-
# Some lists of objects can be serialized as SimpleDicts.
|
645
|
-
# A SimpleDict is serialized as simple key-value pairs where the value is atomic.
|
646
|
-
# The key must be declared as a key, and the value must satisfy one of the following conditions:
|
647
|
-
# 1. The value slot is the only other slot in the object other than the key
|
648
|
-
# 2. The value slot is explicitly annotated as a simple_dict_value
|
649
|
-
# 3. The value slot is the only non-key that is required
|
650
|
-
# See also: https://github.com/linkml/linkml/issues/1250
|
651
|
-
range_simple_dict_value_slot = None
|
652
|
-
if len(non_id_slots) == 1:
|
653
|
-
range_simple_dict_value_slot = non_id_slots[0]
|
654
|
-
elif len(non_id_slots) > 1:
|
655
|
-
candidate_non_id_slots = []
|
656
|
-
for non_id_slot in non_id_slots:
|
657
|
-
if isinstance(non_id_slot.annotations, dict):
|
658
|
-
is_simple_dict_value = non_id_slot.annotations.get("simple_dict_value", False)
|
659
|
-
else:
|
660
|
-
is_simple_dict_value = getattr(non_id_slot.annotations, "simple_dict_value", False)
|
661
|
-
if is_simple_dict_value:
|
662
|
-
candidate_non_id_slots.append(non_id_slot)
|
663
|
-
if len(candidate_non_id_slots) == 1:
|
664
|
-
range_simple_dict_value_slot = candidate_non_id_slots[0]
|
665
|
-
else:
|
666
|
-
candidate_non_id_slots = []
|
667
|
-
for non_id_slot in non_id_slots:
|
668
|
-
if non_id_slot.required:
|
669
|
-
candidate_non_id_slots.append(non_id_slot)
|
670
|
-
if len(candidate_non_id_slots) == 1:
|
671
|
-
range_simple_dict_value_slot = candidate_non_id_slots[0]
|
672
|
-
|
673
|
-
return range_class_id_slot, range_simple_dict_value_slot, non_id_required_slots
|
674
|
-
|
675
695
|
|
676
696
|
@shared_arguments(JsonSchemaGenerator)
|
677
697
|
@click.command(name="json-schema")
|
linkml/generators/linkmlgen.py
CHANGED
@@ -78,6 +78,12 @@ class LinkmlGenerator(Generator):
|
|
78
78
|
|
79
79
|
|
80
80
|
@shared_arguments(LinkmlGenerator)
|
81
|
+
@click.option(
|
82
|
+
"--materialize/--no-materialize",
|
83
|
+
default=True,
|
84
|
+
show_default=True,
|
85
|
+
help="Materialize both, induced slots as attributes and structured patterns as patterns",
|
86
|
+
)
|
81
87
|
@click.option(
|
82
88
|
"--materialize-attributes/--no-materialize-attributes",
|
83
89
|
default=True,
|
@@ -86,7 +92,7 @@ class LinkmlGenerator(Generator):
|
|
86
92
|
)
|
87
93
|
@click.option(
|
88
94
|
"--materialize-patterns/--no-materialize-patterns",
|
89
|
-
default=
|
95
|
+
default=True,
|
90
96
|
show_default=True,
|
91
97
|
help="Materialize structured patterns as patterns",
|
92
98
|
)
|
@@ -100,11 +106,17 @@ class LinkmlGenerator(Generator):
|
|
100
106
|
@click.command(name="linkml")
|
101
107
|
def cli(
|
102
108
|
yamlfile,
|
109
|
+
materialize: bool,
|
103
110
|
materialize_attributes: bool,
|
104
111
|
materialize_patterns: bool,
|
105
112
|
output: FILE_TYPE = None,
|
106
113
|
**kwargs,
|
107
114
|
):
|
115
|
+
# You can use the `--materialize` / `--no-materialize` for control
|
116
|
+
# over both attribute and pattern materialization.
|
117
|
+
materialize_attributes = bool(materialize)
|
118
|
+
materialize_patterns = bool(materialize)
|
119
|
+
|
108
120
|
gen = LinkmlGenerator(
|
109
121
|
yamlfile,
|
110
122
|
materialize_attributes=materialize_attributes,
|
linkml/generators/owlgen.py
CHANGED
@@ -170,6 +170,9 @@ class OwlSchemaGenerator(Generator):
|
|
170
170
|
default_factory=lambda: package_schemaview("linkml_runtime.linkml_model.meta")
|
171
171
|
)
|
172
172
|
|
173
|
+
enum_iri_separator: str = "#"
|
174
|
+
"""Separator for enum IRI. Can be overridden for example if your namespace IRI already contains a #"""
|
175
|
+
|
173
176
|
def as_graph(self) -> Graph:
|
174
177
|
"""
|
175
178
|
Generate an rdflib Graph from the LinkML schema.
|
@@ -1254,7 +1257,7 @@ class OwlSchemaGenerator(Generator):
|
|
1254
1257
|
if pv.meaning:
|
1255
1258
|
return URIRef(self.schemaview.expand_curie(pv.meaning))
|
1256
1259
|
else:
|
1257
|
-
return URIRef(enum_uri +
|
1260
|
+
return URIRef(enum_uri + self.enum_iri_separator + pv.text.replace(" ", "+"))
|
1258
1261
|
|
1259
1262
|
def slot_owl_type(self, slot: SlotDefinition) -> URIRef:
|
1260
1263
|
sv = self.schemaview
|
@@ -1353,6 +1356,13 @@ class OwlSchemaGenerator(Generator):
|
|
1353
1356
|
show_default=True,
|
1354
1357
|
help="Default OWL type for permissible values",
|
1355
1358
|
)
|
1359
|
+
@click.option(
|
1360
|
+
"--enum-iri-separator",
|
1361
|
+
default="#",
|
1362
|
+
is_flag=False,
|
1363
|
+
show_default=True,
|
1364
|
+
help="IRI separator for enums.",
|
1365
|
+
)
|
1356
1366
|
@click.version_option(__version__, "-V", "--version")
|
1357
1367
|
def cli(yamlfile, metadata_profile: str, **kwargs):
|
1358
1368
|
"""Generate an OWL representation of a LinkML model
|
linkml/generators/plantumlgen.py
CHANGED
@@ -49,14 +49,13 @@ class PlantumlGenerator(Generator):
|
|
49
49
|
classes: Set[ClassDefinitionName] = None
|
50
50
|
directory: Optional[str] = None
|
51
51
|
kroki_server: Optional[str] = "https://kroki.io"
|
52
|
-
load_image: bool = True
|
53
52
|
tooltips_flag: bool = False
|
53
|
+
dry_run: bool = False
|
54
54
|
|
55
55
|
def visit_schema(
|
56
56
|
self,
|
57
57
|
classes: Set[ClassDefinitionName] = None,
|
58
58
|
directory: Optional[str] = None,
|
59
|
-
load_image: bool = True,
|
60
59
|
**_,
|
61
60
|
) -> Optional[str]:
|
62
61
|
if directory:
|
@@ -96,20 +95,21 @@ class PlantumlGenerator(Generator):
|
|
96
95
|
b64_diagram = base64.urlsafe_b64encode(zlib.compress(plantuml_code.encode(), 9))
|
97
96
|
|
98
97
|
plantuml_url = self.kroki_server + "/plantuml/svg/" + b64_diagram.decode()
|
98
|
+
if self.dry_run:
|
99
|
+
return plantuml_url
|
99
100
|
if directory:
|
100
101
|
file_suffix = ".svg" if self.format == "puml" or self.format == "puml" else "." + self.format
|
101
102
|
self.output_file_name = os.path.join(
|
102
103
|
directory,
|
103
104
|
camelcase(sorted(classes)[0] if classes else self.schema.name) + file_suffix,
|
104
105
|
)
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
self.logger.error(f"{resp.reason} accessing {plantuml_url}")
|
106
|
+
resp = requests.get(plantuml_url, stream=True, timeout=REQUESTS_TIMEOUT)
|
107
|
+
if resp.ok:
|
108
|
+
with open(self.output_file_name, "wb") as f:
|
109
|
+
for chunk in resp.iter_content(chunk_size=2048):
|
110
|
+
f.write(chunk)
|
111
|
+
else:
|
112
|
+
self.logger.error(f"{resp.reason} accessing {plantuml_url}")
|
113
113
|
else:
|
114
114
|
out = (
|
115
115
|
"@startuml\n"
|
@@ -352,6 +352,13 @@ class PlantumlGenerator(Generator):
|
|
352
352
|
help="URL of the Kroki server to use for diagram drawing",
|
353
353
|
default="https://kroki.io",
|
354
354
|
)
|
355
|
+
@click.option(
|
356
|
+
"--dry-run",
|
357
|
+
is_flag=True,
|
358
|
+
default=False,
|
359
|
+
show_default=True,
|
360
|
+
help="Print out Kroki URL calls instead of sending the real requests",
|
361
|
+
)
|
355
362
|
@click.version_option(__version__, "-V", "--version")
|
356
363
|
def cli(yamlfile, **args):
|
357
364
|
"""Generate a UML representation of a LinkML model"""
|
@@ -1,7 +1,15 @@
|
|
1
1
|
import sys
|
2
2
|
from abc import ABC, abstractmethod
|
3
3
|
from enum import Enum
|
4
|
-
from typing import
|
4
|
+
from typing import (
|
5
|
+
ClassVar,
|
6
|
+
Iterable,
|
7
|
+
List,
|
8
|
+
Optional,
|
9
|
+
Type,
|
10
|
+
TypeVar,
|
11
|
+
Union,
|
12
|
+
)
|
5
13
|
|
6
14
|
from linkml_runtime.linkml_model import Element
|
7
15
|
from linkml_runtime.linkml_model.meta import ArrayExpression, DimensionExpression
|
@@ -9,20 +17,15 @@ from pydantic import VERSION as PYDANTIC_VERSION
|
|
9
17
|
|
10
18
|
from linkml.utils.deprecation import deprecation_warning
|
11
19
|
|
12
|
-
if int(PYDANTIC_VERSION[0])
|
13
|
-
from pydantic_core import core_schema
|
14
|
-
else:
|
20
|
+
if int(PYDANTIC_VERSION[0]) < 2:
|
15
21
|
# Support for having pydantic 1 installed in the same environment will be dropped in 1.9.0
|
16
22
|
deprecation_warning("pydantic-v1")
|
17
23
|
|
18
|
-
if
|
19
|
-
from
|
20
|
-
from pydantic_core import CoreSchema
|
21
|
-
|
22
|
-
if sys.version_info.minor <= 8:
|
23
|
-
from typing_extensions import Annotated
|
24
|
+
if sys.version_info.minor < 12:
|
25
|
+
from typing_extensions import TypeAliasType
|
24
26
|
else:
|
25
|
-
from typing import
|
27
|
+
from typing import TypeAliasType
|
28
|
+
|
26
29
|
|
27
30
|
from linkml.generators.pydanticgen.build import RangeResult
|
28
31
|
from linkml.generators.pydanticgen.template import ConditionalImport, Import, Imports, ObjectImport
|
@@ -37,75 +40,32 @@ class ArrayRepresentation(Enum):
|
|
37
40
|
_BOUNDED_ARRAY_FIELDS = ("exact_number_dimensions", "minimum_number_dimensions", "maximum_number_dimensions")
|
38
41
|
|
39
42
|
_T = TypeVar("_T")
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
class AnyShapeArrayType(Generic[_T]):
|
44
|
-
@classmethod
|
45
|
-
def __get_pydantic_core_schema__(cls, source_type: Any, handler: "GetCoreSchemaHandler") -> "CoreSchema":
|
46
|
-
# double-nested parameterized types here
|
47
|
-
# source_type: List[Union[T,List[...]]]
|
48
|
-
item_type = (Any,) if get_args(get_args(source_type)[0])[0] is _T else get_args(get_args(source_type)[0])[:-1]
|
49
|
-
|
50
|
-
if len(item_type) == 1:
|
51
|
-
item_schema = handler.generate_schema(item_type[0])
|
52
|
-
else:
|
53
|
-
item_schema = core_schema.union_schema([handler.generate_schema(i) for i in item_type])
|
54
|
-
|
55
|
-
if all([getattr(i, "__module__", "") == "builtins" and i is not Any for i in item_type]):
|
56
|
-
item_schema["strict"] = True
|
57
|
-
|
58
|
-
# Before python 3.11, `Any` type was a special object without a __name__
|
59
|
-
item_name = "_".join(["Any" if i is Any else i.__name__ for i in item_type])
|
60
|
-
|
61
|
-
array_ref = f"any-shape-array-{item_name}"
|
62
|
-
|
63
|
-
schema = core_schema.definitions_schema(
|
64
|
-
core_schema.list_schema(core_schema.definition_reference_schema(array_ref)),
|
65
|
-
[
|
66
|
-
core_schema.union_schema(
|
67
|
-
[
|
68
|
-
core_schema.list_schema(core_schema.definition_reference_schema(array_ref)),
|
69
|
-
item_schema,
|
70
|
-
],
|
71
|
-
ref=array_ref,
|
72
|
-
)
|
73
|
-
],
|
74
|
-
)
|
75
|
-
|
76
|
-
return schema
|
77
|
-
|
78
|
-
|
79
|
-
AnyShapeArray = Annotated[_RecursiveListType, AnyShapeArrayType]
|
43
|
+
AnyShapeArray = TypeAliasType("AnyShapeArray", Iterable[Union[_T, Iterable["AnyShapeArray[_T]"]]], type_params=(_T,))
|
80
44
|
|
81
45
|
_AnyShapeArrayImports = (
|
82
46
|
Imports()
|
83
47
|
+ Import(
|
84
48
|
module="typing",
|
85
49
|
objects=[
|
86
|
-
ObjectImport(name="Generic"),
|
87
50
|
ObjectImport(name="Iterable"),
|
88
51
|
ObjectImport(name="TypeVar"),
|
89
52
|
ObjectImport(name="Union"),
|
90
|
-
ObjectImport(name="get_args"),
|
91
53
|
],
|
92
54
|
)
|
93
55
|
+ ConditionalImport(
|
94
|
-
condition="sys.version_info.minor
|
56
|
+
condition="sys.version_info.minor >= 12",
|
95
57
|
module="typing",
|
96
|
-
objects=[ObjectImport(name="
|
97
|
-
alternative=Import(module="typing_extensions", objects=[ObjectImport(name="
|
58
|
+
objects=[ObjectImport(name="TypeAliasType")],
|
59
|
+
alternative=Import(module="typing_extensions", objects=[ObjectImport(name="TypeAliasType")]),
|
98
60
|
)
|
99
|
-
+ Import(module="pydantic", objects=[ObjectImport(name="GetCoreSchemaHandler")])
|
100
|
-
+ Import(module="pydantic_core", objects=[ObjectImport(name="CoreSchema"), ObjectImport(name="core_schema")])
|
101
61
|
)
|
102
62
|
|
103
63
|
# annotated types are special and inspect.getsource() can't stringify them
|
104
64
|
_AnyShapeArrayInjects = [
|
105
65
|
'_T = TypeVar("_T")',
|
106
|
-
|
107
|
-
|
108
|
-
|
66
|
+
"""AnyShapeArray = TypeAliasType(
|
67
|
+
"AnyShapeArray", Iterable[Union[_T, Iterable["AnyShapeArray[_T]"]]], type_params=(_T,)
|
68
|
+
)""",
|
109
69
|
]
|
110
70
|
|
111
71
|
_ConListImports = Imports() + Import(module="pydantic", objects=[ObjectImport(name="conlist")])
|
@@ -675,7 +675,18 @@ class PydanticModule(PydanticTemplateModel):
|
|
675
675
|
return [c.name for c in self.classes.values()]
|
676
676
|
|
677
677
|
|
678
|
-
_some_stdlib_module_names = {
|
678
|
+
_some_stdlib_module_names = {
|
679
|
+
"copy",
|
680
|
+
"datetime",
|
681
|
+
"decimal",
|
682
|
+
"enum",
|
683
|
+
"inspect",
|
684
|
+
"os",
|
685
|
+
"re",
|
686
|
+
"sys",
|
687
|
+
"typing",
|
688
|
+
"dataclasses",
|
689
|
+
}
|
679
690
|
"""
|
680
691
|
sys.stdlib_module_names is only present in 3.10 and later
|
681
692
|
so we make a cheap copy of the stdlib modules that we commonly use here,
|
@@ -71,7 +71,7 @@ class PythonIfAbsentProcessor(IfAbsentProcessor):
|
|
71
71
|
def map_enum_default_value(
|
72
72
|
self, enum_name: EnumDefinitionName, permissible_value_name: str, slot: SlotDefinition, cls: ClassDefinition
|
73
73
|
):
|
74
|
-
return f"{
|
74
|
+
return f"'{permissible_value_name}'"
|
75
75
|
|
76
76
|
def map_nc_name_default_value(self, default_value: str, slot: SlotDefinition, cls: ClassDefinition):
|
77
77
|
raise NotImplementedError()
|