linkml 1.7.7__py3-none-any.whl → 1.7.9__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 +14 -0
- linkml/generators/csvgen.py +15 -5
- linkml/generators/docgen/class_diagram.md.jinja2 +19 -4
- linkml/generators/docgen/subset.md.jinja2 +7 -7
- linkml/generators/docgen.py +59 -14
- linkml/generators/graphqlgen.py +14 -16
- linkml/generators/jsonldcontextgen.py +3 -3
- linkml/generators/jsonldgen.py +4 -3
- linkml/generators/jsonschemagen.py +9 -0
- linkml/generators/markdowngen.py +341 -301
- linkml/generators/owlgen.py +87 -20
- linkml/generators/plantumlgen.py +9 -8
- linkml/generators/prefixmapgen.py +15 -23
- linkml/generators/protogen.py +23 -18
- linkml/generators/pydanticgen/array.py +15 -3
- linkml/generators/pydanticgen/pydanticgen.py +13 -2
- linkml/generators/pydanticgen/template.py +6 -4
- linkml/generators/pythongen.py +5 -5
- linkml/generators/rdfgen.py +14 -5
- linkml/generators/shaclgen.py +18 -6
- linkml/generators/shexgen.py +9 -7
- linkml/generators/sqlalchemygen.py +1 -0
- linkml/generators/sqltablegen.py +16 -1
- linkml/generators/summarygen.py +8 -2
- linkml/generators/terminusdbgen.py +2 -2
- linkml/generators/yumlgen.py +2 -2
- linkml/transformers/relmodel_transformer.py +21 -1
- linkml/utils/__init__.py +3 -0
- linkml/utils/deprecation.py +255 -0
- linkml/utils/generator.py +87 -61
- linkml/utils/rawloader.py +5 -1
- linkml/utils/schemaloader.py +2 -1
- linkml/utils/sqlutils.py +13 -14
- linkml/validator/cli.py +11 -0
- linkml/validator/plugins/jsonschema_validation_plugin.py +2 -0
- linkml/validator/plugins/shacl_validation_plugin.py +10 -4
- linkml/validator/report.py +1 -0
- linkml/workspaces/example_runner.py +2 -0
- {linkml-1.7.7.dist-info → linkml-1.7.9.dist-info}/METADATA +2 -1
- {linkml-1.7.7.dist-info → linkml-1.7.9.dist-info}/RECORD +43 -42
- {linkml-1.7.7.dist-info → linkml-1.7.9.dist-info}/LICENSE +0 -0
- {linkml-1.7.7.dist-info → linkml-1.7.9.dist-info}/WHEEL +0 -0
- {linkml-1.7.7.dist-info → linkml-1.7.9.dist-info}/entry_points.txt +0 -0
linkml/generators/owlgen.py
CHANGED
@@ -30,7 +30,7 @@ from linkml_runtime.linkml_model.meta import (
|
|
30
30
|
)
|
31
31
|
from linkml_runtime.utils.formatutils import camelcase, underscore
|
32
32
|
from linkml_runtime.utils.introspection import package_schemaview
|
33
|
-
from rdflib import OWL, RDF, XSD, BNode, Graph, Literal, URIRef
|
33
|
+
from rdflib import DCTERMS, OWL, RDF, XSD, BNode, Graph, Literal, URIRef
|
34
34
|
from rdflib.collection import Collection
|
35
35
|
from rdflib.namespace import RDFS, SKOS
|
36
36
|
from rdflib.plugin import Parser as rdflib_Parser
|
@@ -147,6 +147,8 @@ class OwlSchemaGenerator(Generator):
|
|
147
147
|
mixins_as_expressions: bool = None
|
148
148
|
"""EXPERIMENTAL: If True, use OWL existential restrictions to represent mixins"""
|
149
149
|
|
150
|
+
default_permissible_value_type: Union[str, URIRef] = field(default_factory=lambda: OWL.Class)
|
151
|
+
|
150
152
|
slot_is_literal_map: Mapping[str, Set[bool]] = field(default_factory=lambda: defaultdict(set))
|
151
153
|
"""DEPRECATED: use node_owltypes"""
|
152
154
|
|
@@ -830,6 +832,28 @@ class OwlSchemaGenerator(Generator):
|
|
830
832
|
if ixn:
|
831
833
|
self.graph.add((type_uri, OWL.equivalentClass, ixn))
|
832
834
|
|
835
|
+
def _get_metatype(
|
836
|
+
self, element: Union[Definition, PermissibleValue], default_value: Optional[Union[str, URIRef]] = None
|
837
|
+
) -> Optional[URIRef]:
|
838
|
+
impls = []
|
839
|
+
if isinstance(element, Definition):
|
840
|
+
impls.extend(element.implements)
|
841
|
+
if isinstance(element, PermissibleValue):
|
842
|
+
if "implements" in element.annotations:
|
843
|
+
ann = element.annotations["implements"]
|
844
|
+
v = ann.value
|
845
|
+
if not isinstance(v, list):
|
846
|
+
v = [v]
|
847
|
+
impls.extend(v)
|
848
|
+
for impl in impls:
|
849
|
+
if impl.startswith("owl:"):
|
850
|
+
return OWL[impl.split(":")[1]]
|
851
|
+
if impl.startswith("rdfs:"):
|
852
|
+
return RDFS[impl.split(":")[1]]
|
853
|
+
if isinstance(default_value, str):
|
854
|
+
return URIRef(default_value)
|
855
|
+
return default_value
|
856
|
+
|
833
857
|
def add_enum(self, e: EnumDefinition) -> None:
|
834
858
|
g = self.graph
|
835
859
|
enum_uri = self._enum_uri(e.name)
|
@@ -856,26 +880,36 @@ class OwlSchemaGenerator(Generator):
|
|
856
880
|
)
|
857
881
|
)
|
858
882
|
pv_uris = []
|
883
|
+
owl_types = []
|
884
|
+
enum_owl_type = self._get_metatype(e, self.default_permissible_value_type)
|
885
|
+
|
859
886
|
for pv in e.permissible_values.values():
|
860
|
-
|
861
|
-
|
862
|
-
if
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
871
|
-
|
887
|
+
pv_owl_type = self._get_metatype(pv, enum_owl_type)
|
888
|
+
owl_types.append(pv_owl_type)
|
889
|
+
if pv_owl_type == RDFS.Literal:
|
890
|
+
pv_node = Literal(pv.text)
|
891
|
+
if pv.meaning:
|
892
|
+
logging.warning(f"Meaning on literal {pv.text} in {e.name} is ignored")
|
893
|
+
else:
|
894
|
+
pv_node = self._permissible_value_uri(pv, enum_uri, e)
|
895
|
+
pv_uris.append(pv_node)
|
896
|
+
g.add(
|
897
|
+
(
|
898
|
+
enum_uri,
|
899
|
+
self.metamodel.namespaces[METAMODEL_NAMESPACE_NAME]["permissible_values"],
|
900
|
+
pv_node,
|
872
901
|
)
|
902
|
+
)
|
903
|
+
if not isinstance(pv_node, Literal):
|
904
|
+
g.add((pv_node, RDF.type, pv_owl_type))
|
905
|
+
g.add((pv_node, RDFS.label, Literal(pv.text)))
|
906
|
+
# TODO: make this configurable
|
873
907
|
# self._add_element_properties(pv_uri, pv)
|
874
908
|
if self.metaclasses:
|
875
|
-
g.add((
|
909
|
+
g.add((pv_node, RDF.type, enum_uri))
|
876
910
|
has_parent = False
|
877
911
|
if pv.is_a:
|
878
|
-
self.graph.add((
|
912
|
+
self.graph.add((pv_node, RDFS.subClassOf, self._permissible_value_uri(pv.is_a, enum_uri, e)))
|
879
913
|
has_parent = True
|
880
914
|
for mixin in sorted(pv.mixins):
|
881
915
|
parent = self._permissible_value_uri(mixin, enum_uri, e)
|
@@ -885,13 +919,33 @@ class OwlSchemaGenerator(Generator):
|
|
885
919
|
has_parent = True
|
886
920
|
self.graph.add((enum_uri, RDFS.subClassOf, parent))
|
887
921
|
if not has_parent and self.add_root_classes:
|
888
|
-
self.graph.add((
|
922
|
+
self.graph.add((pv_node, RDFS.subClassOf, URIRef(PermissibleValue.class_class_uri)))
|
889
923
|
if all([pv is not None for pv in pv_uris]):
|
890
|
-
|
891
|
-
for
|
924
|
+
all_is_class = all([owl_type == OWL.Class for owl_type in owl_types])
|
925
|
+
all_is_individual = all([owl_type == OWL.NamedIndividual for owl_type in owl_types])
|
926
|
+
all_is_literal = all([owl_type == RDFS.Literal for owl_type in owl_types])
|
927
|
+
sub_pred = DCTERMS.isPartOf
|
928
|
+
combo_pred = None
|
929
|
+
if all_is_class or all_is_individual or all_is_literal:
|
930
|
+
if all_is_class:
|
931
|
+
combo_pred = OWL.unionOf
|
932
|
+
# self._union_of(pv_uris, node=enum_uri)
|
933
|
+
sub_pred = RDFS.subClassOf
|
934
|
+
elif all_is_individual:
|
935
|
+
combo_pred = OWL.oneOf
|
936
|
+
# self._object_one_of(pv_uris, node=enum_uri)
|
937
|
+
sub_pred = RDF.type
|
938
|
+
elif all_is_literal:
|
939
|
+
combo_pred = OWL.oneOf
|
940
|
+
# self._object_one_of(pv_uris, node=enum_uri)
|
941
|
+
sub_pred = RDF.type
|
942
|
+
if combo_pred:
|
943
|
+
self._boolean_expression(pv_uris, combo_pred, enum_uri, owl_types=set(owl_types))
|
944
|
+
for pv_node in pv_uris:
|
892
945
|
# this would normally be entailed, but we assert here so it is visible
|
893
946
|
# without reasoning
|
894
|
-
|
947
|
+
if not isinstance(pv_node, Literal):
|
948
|
+
g.add((pv_node, sub_pred, enum_uri))
|
895
949
|
|
896
950
|
def _add_rule(self, subject: Union[URIRef, BNode], rule: ClassRule, cls: ClassDefinition):
|
897
951
|
if not self.use_swrl:
|
@@ -1030,6 +1084,9 @@ class OwlSchemaGenerator(Generator):
|
|
1030
1084
|
def _union_of(self, exprs: List[Union[BNode, URIRef]], **kwargs) -> Optional[Union[BNode, URIRef]]:
|
1031
1085
|
return self._boolean_expression(exprs, OWL.unionOf, **kwargs)
|
1032
1086
|
|
1087
|
+
def _object_one_of(self, exprs: List[Union[BNode, URIRef]], **kwargs) -> Optional[Union[BNode, URIRef]]:
|
1088
|
+
return self._boolean_expression(exprs, OWL.oneOf, **kwargs)
|
1089
|
+
|
1033
1090
|
def _exactly_one_of(self, exprs: List[Union[BNode, URIRef]]) -> Optional[Union[BNode, URIRef]]:
|
1034
1091
|
if not exprs:
|
1035
1092
|
raise ValueError("Must pass at least one")
|
@@ -1165,7 +1222,7 @@ class OwlSchemaGenerator(Generator):
|
|
1165
1222
|
return URIRef(expanded)
|
1166
1223
|
|
1167
1224
|
def _permissible_value_uri(
|
1168
|
-
self, pv: Union[str,
|
1225
|
+
self, pv: Union[str, PermissibleValue], enum_uri: str, enum_def: EnumDefinition = None
|
1169
1226
|
) -> URIRef:
|
1170
1227
|
if isinstance(pv, str):
|
1171
1228
|
pv_name = pv
|
@@ -1182,6 +1239,10 @@ class OwlSchemaGenerator(Generator):
|
|
1182
1239
|
|
1183
1240
|
def slot_owl_type(self, slot: SlotDefinition) -> URIRef:
|
1184
1241
|
sv = self.schemaview
|
1242
|
+
if slot.implements:
|
1243
|
+
for t in ["owl:AnnotationProperty", "owl:ObjectProperty", "owl:DatatypeProperty"]:
|
1244
|
+
if t in slot.implements:
|
1245
|
+
return OWL[t.replace("owl:", "")]
|
1185
1246
|
if slot.range is None:
|
1186
1247
|
range = self.schemaview.schema.default_range
|
1187
1248
|
else:
|
@@ -1267,6 +1328,12 @@ class OwlSchemaGenerator(Generator):
|
|
1267
1328
|
show_default=True,
|
1268
1329
|
help="Use model URIs rather than class/slot URIs",
|
1269
1330
|
)
|
1331
|
+
@click.option(
|
1332
|
+
"--default-permissible-value-type",
|
1333
|
+
default=str(OWL.Class),
|
1334
|
+
show_default=True,
|
1335
|
+
help="Default OWL type for permissible values",
|
1336
|
+
)
|
1270
1337
|
@click.version_option(__version__, "-V", "--version")
|
1271
1338
|
def cli(yamlfile, metadata_profile: str, **kwargs):
|
1272
1339
|
"""Generate an OWL representation of a LinkML model
|
linkml/generators/plantumlgen.py
CHANGED
@@ -57,7 +57,7 @@ class PlantumlGenerator(Generator):
|
|
57
57
|
directory: Optional[str] = None,
|
58
58
|
load_image: bool = True,
|
59
59
|
**_,
|
60
|
-
) ->
|
60
|
+
) -> Optional[str]:
|
61
61
|
if directory:
|
62
62
|
os.makedirs(directory, exist_ok=True)
|
63
63
|
if classes is not None:
|
@@ -110,10 +110,13 @@ class PlantumlGenerator(Generator):
|
|
110
110
|
else:
|
111
111
|
self.logger.error(f"{resp.reason} accessing {plantuml_url}")
|
112
112
|
else:
|
113
|
-
|
114
|
-
"@startuml\
|
115
|
-
|
113
|
+
out = (
|
114
|
+
"@startuml\n"
|
115
|
+
"skinparam nodesep 10\n"
|
116
|
+
"hide circle\n"
|
117
|
+
"hide empty members\n" + "\n".join(dedup_plantumlclassdef) + "\n@enduml\n"
|
116
118
|
)
|
119
|
+
return out
|
117
120
|
|
118
121
|
def add_class(self, cn: ClassDefinitionName) -> str:
|
119
122
|
"""Define the class only if
|
@@ -130,13 +133,11 @@ class PlantumlGenerator(Generator):
|
|
130
133
|
if True or cn in slot.domain_of:
|
131
134
|
mod = self.prop_modifier(cls, slot)
|
132
135
|
slot_defs.append(
|
133
|
-
|
136
|
+
" {field} "
|
134
137
|
+ underscore(self.aliased_slot_name(slot))
|
135
|
-
+ '"'
|
136
138
|
+ mod
|
137
|
-
+
|
139
|
+
+ ": "
|
138
140
|
+ underscore(slot.range)
|
139
|
-
+ '"'
|
140
141
|
+ self.cardinality(slot)
|
141
142
|
)
|
142
143
|
self.class_generated.add(cn)
|
@@ -3,7 +3,6 @@ Generate JSON-LD contexts
|
|
3
3
|
|
4
4
|
"""
|
5
5
|
|
6
|
-
import csv
|
7
6
|
import os
|
8
7
|
from dataclasses import dataclass, field
|
9
8
|
from typing import Dict, Optional, Set, Union
|
@@ -59,7 +58,7 @@ class PrefixGenerator(Generator):
|
|
59
58
|
if self.default_ns:
|
60
59
|
self.emit_prefixes.add(self.default_ns)
|
61
60
|
|
62
|
-
def end_schema(self, base: Optional[Union[str, Namespace]] = None, output: Optional[str] = None, **_) ->
|
61
|
+
def end_schema(self, base: Optional[Union[str, Namespace]] = None, output: Optional[str] = None, **_) -> str:
|
63
62
|
context = JsonObj()
|
64
63
|
if base:
|
65
64
|
base = str(base)
|
@@ -74,31 +73,24 @@ class PrefixGenerator(Generator):
|
|
74
73
|
for k, v in self.slot_class_maps.items():
|
75
74
|
context[k] = v
|
76
75
|
|
77
|
-
if
|
78
|
-
|
76
|
+
if self.format == "tsv":
|
77
|
+
mapping: Dict = {} # prefix to IRI mapping
|
78
|
+
for prefix in sorted(self.emit_prefixes):
|
79
|
+
mapping[prefix] = self.namespaces[prefix]
|
79
80
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
81
|
+
items = []
|
82
|
+
for key, value in mapping.items():
|
83
|
+
items.append("\t".join([key, value]))
|
84
|
+
out = "\n".join(items)
|
84
85
|
|
85
|
-
with open(output, "w", encoding="UTF-8") as outf:
|
86
|
-
writer = csv.writer(outf, delimiter="\t")
|
87
|
-
for key, value in mapping.items():
|
88
|
-
writer.writerow([key, value])
|
89
|
-
else:
|
90
|
-
with open(output, "w", encoding="UTF-8") as outf:
|
91
|
-
outf.write(as_json(context))
|
92
86
|
else:
|
93
|
-
|
94
|
-
mapping: Dict = {} # prefix to IRI mapping
|
95
|
-
for prefix in sorted(self.emit_prefixes):
|
96
|
-
mapping[prefix] = self.namespaces[prefix]
|
87
|
+
out = str(as_json(context))
|
97
88
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
89
|
+
if output:
|
90
|
+
with open(output, "w", encoding="UTF-8") as outf:
|
91
|
+
outf.write(out)
|
92
|
+
|
93
|
+
return out
|
102
94
|
|
103
95
|
def visit_class(self, cls: ClassDefinition) -> bool:
|
104
96
|
class_def = {}
|
linkml/generators/protogen.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
import os
|
2
2
|
from dataclasses import dataclass
|
3
|
+
from typing import Optional
|
3
4
|
|
4
5
|
import click
|
5
6
|
from linkml_runtime.linkml_model.meta import ClassDefinition, SlotDefinition
|
@@ -26,32 +27,36 @@ class ProtoGenerator(Generator):
|
|
26
27
|
# ObjectVars
|
27
28
|
relative_slot_num: int = 0
|
28
29
|
|
29
|
-
def
|
30
|
-
|
31
|
-
self.generate_header()
|
30
|
+
def visit_schema(self, **kwargs) -> Optional[str]:
|
31
|
+
return self.generate_header()
|
32
32
|
|
33
|
-
def generate_header(self):
|
34
|
-
|
35
|
-
|
36
|
-
|
33
|
+
def generate_header(self) -> str:
|
34
|
+
items = []
|
35
|
+
items.append(' syntax="proto3";')
|
36
|
+
items.append(" package")
|
37
|
+
items.append(f"// metamodel_version: {self.schema.metamodel_version}")
|
37
38
|
if self.schema.version:
|
38
|
-
|
39
|
+
items.append(f"// version: {self.schema.version}")
|
40
|
+
out = "\n".join(items) + "\n"
|
41
|
+
return out
|
39
42
|
|
40
|
-
def visit_class(self, cls: ClassDefinition) ->
|
43
|
+
def visit_class(self, cls: ClassDefinition) -> Optional[str]:
|
41
44
|
if cls.mixin or cls.abstract or not cls.slots:
|
42
|
-
return
|
45
|
+
return None
|
46
|
+
items = []
|
43
47
|
if cls.description:
|
44
48
|
for dline in cls.description.split("\n"):
|
45
|
-
|
46
|
-
|
47
|
-
|
49
|
+
items.append(f"// {dline}")
|
50
|
+
items.append(f"message {camelcase(cls.name)}")
|
51
|
+
items.append(" {")
|
52
|
+
out = "\n".join(items)
|
48
53
|
self.relative_slot_num = 0
|
49
|
-
return
|
54
|
+
return out
|
50
55
|
|
51
|
-
def end_class(self, cls: ClassDefinition) ->
|
52
|
-
|
56
|
+
def end_class(self, cls: ClassDefinition) -> str:
|
57
|
+
return "\n }\n"
|
53
58
|
|
54
|
-
def visit_class_slot(self, cls: ClassDefinition, aliased_slot_name: str, slot: SlotDefinition) ->
|
59
|
+
def visit_class_slot(self, cls: ClassDefinition, aliased_slot_name: str, slot: SlotDefinition) -> str:
|
55
60
|
qual = "repeated " if slot.multivalued else ""
|
56
61
|
slotname = lcamelcase(aliased_slot_name)
|
57
62
|
slot_range = camelcase(slot.range)
|
@@ -59,7 +64,7 @@ class ProtoGenerator(Generator):
|
|
59
64
|
# numbering of slots is important in the proto implementation
|
60
65
|
# and should be determined by the rank param.
|
61
66
|
slot.rank = 0
|
62
|
-
|
67
|
+
return f"\n {qual} {lcamelcase(slot_range)} {(slotname)} = {slot.rank}"
|
63
68
|
|
64
69
|
|
65
70
|
@shared_arguments(ProtoGenerator)
|
@@ -30,7 +30,7 @@ else:
|
|
30
30
|
from typing import Annotated
|
31
31
|
|
32
32
|
from linkml.generators.pydanticgen.build import SlotResult
|
33
|
-
from linkml.generators.pydanticgen.template import Import, Imports, ObjectImport
|
33
|
+
from linkml.generators.pydanticgen.template import ConditionalImport, Import, Imports, ObjectImport
|
34
34
|
|
35
35
|
|
36
36
|
class ArrayRepresentation(Enum):
|
@@ -54,7 +54,14 @@ if int(PYDANTIC_VERSION[0]) >= 2:
|
|
54
54
|
item_schema = handler.generate_schema(item_type)
|
55
55
|
if item_schema.get("type", "any") != "any":
|
56
56
|
item_schema["strict"] = True
|
57
|
-
|
57
|
+
|
58
|
+
if item_type is Any:
|
59
|
+
# Before python 3.11, `Any` type was a special object without a __name__
|
60
|
+
item_name = "Any"
|
61
|
+
else:
|
62
|
+
item_name = item_type.__name__
|
63
|
+
|
64
|
+
array_ref = f"any-shape-array-{item_name}"
|
58
65
|
|
59
66
|
schema = core_schema.definitions_schema(
|
60
67
|
core_schema.list_schema(core_schema.definition_reference_schema(array_ref)),
|
@@ -78,7 +85,6 @@ if int(PYDANTIC_VERSION[0]) >= 2:
|
|
78
85
|
+ Import(
|
79
86
|
module="typing",
|
80
87
|
objects=[
|
81
|
-
ObjectImport(name="Annotated"),
|
82
88
|
ObjectImport(name="Generic"),
|
83
89
|
ObjectImport(name="Iterable"),
|
84
90
|
ObjectImport(name="TypeVar"),
|
@@ -86,6 +92,12 @@ if int(PYDANTIC_VERSION[0]) >= 2:
|
|
86
92
|
ObjectImport(name="get_args"),
|
87
93
|
],
|
88
94
|
)
|
95
|
+
+ ConditionalImport(
|
96
|
+
condition="sys.version_info.minor > 8",
|
97
|
+
module="typing",
|
98
|
+
objects=[ObjectImport(name="Annotated")],
|
99
|
+
alternative=Import(module="typing_extensions", objects=[ObjectImport(name="Annotated")]),
|
100
|
+
)
|
89
101
|
+ Import(module="pydantic", objects=[ObjectImport(name="GetCoreSchemaHandler")])
|
90
102
|
+ Import(module="pydantic_core", objects=[ObjectImport(name="CoreSchema"), ObjectImport(name="core_schema")])
|
91
103
|
)
|
@@ -50,9 +50,13 @@ from linkml.generators.pydanticgen.template import (
|
|
50
50
|
PydanticModule,
|
51
51
|
TemplateModel,
|
52
52
|
)
|
53
|
+
from linkml.utils import deprecation_warning
|
53
54
|
from linkml.utils.generator import shared_arguments
|
54
55
|
from linkml.utils.ifabsent_functions import ifabsent_value_declaration
|
55
56
|
|
57
|
+
if int(PYDANTIC_VERSION[0]) == 1:
|
58
|
+
deprecation_warning("pydantic-v1")
|
59
|
+
|
56
60
|
|
57
61
|
def _get_pyrange(t: TypeDefinition, sv: SchemaView) -> str:
|
58
62
|
pyrange = t.repr if t is not None else None
|
@@ -76,6 +80,7 @@ DEFAULT_IMPORTS = (
|
|
76
80
|
+ Import(module="decimal", objects=[ObjectImport(name="Decimal")])
|
77
81
|
+ Import(module="enum", objects=[ObjectImport(name="Enum")])
|
78
82
|
+ Import(module="re")
|
83
|
+
+ Import(module="sys")
|
79
84
|
+ Import(
|
80
85
|
module="typing",
|
81
86
|
objects=[
|
@@ -214,6 +219,11 @@ class PydanticGenerator(OOCodeGenerator):
|
|
214
219
|
genmeta: bool = False
|
215
220
|
emit_metadata: bool = True
|
216
221
|
|
222
|
+
def __post_init__(self):
|
223
|
+
super().__post_init__()
|
224
|
+
if int(self.pydantic_version) == 1:
|
225
|
+
deprecation_warning("pydanticgen-v1")
|
226
|
+
|
217
227
|
def compile_module(self, **kwargs) -> ModuleType:
|
218
228
|
"""
|
219
229
|
Compiles generated python code to a module
|
@@ -644,10 +654,11 @@ class PydanticGenerator(OOCodeGenerator):
|
|
644
654
|
for k, c in pyschema.classes.items():
|
645
655
|
attrs = {}
|
646
656
|
for attr_name, src_attr in c.attributes.items():
|
657
|
+
src_attr = src_attr._as_dict
|
647
658
|
new_fields = {
|
648
|
-
k: src_attr.
|
659
|
+
k: src_attr.get(k, None)
|
649
660
|
for k in PydanticAttribute.model_fields.keys()
|
650
|
-
if src_attr.
|
661
|
+
if src_attr.get(k, None) is not None
|
651
662
|
}
|
652
663
|
predef_slot = predefined.get(k, {}).get(attr_name, None)
|
653
664
|
if predef_slot is not None:
|
@@ -1,3 +1,4 @@
|
|
1
|
+
from copy import copy
|
1
2
|
from importlib.util import find_spec
|
2
3
|
from typing import Any, ClassVar, Dict, Generator, List, Literal, Optional, Union, overload
|
3
4
|
|
@@ -54,6 +55,10 @@ class TemplateModel(BaseModel):
|
|
54
55
|
"""
|
55
56
|
|
56
57
|
template: ClassVar[str]
|
58
|
+
_environment: ClassVar[Environment] = Environment(
|
59
|
+
loader=PackageLoader("linkml.generators.pydanticgen", "templates"), trim_blocks=True, lstrip_blocks=True
|
60
|
+
)
|
61
|
+
|
57
62
|
pydantic_ver: int = int(PYDANTIC_VERSION[0])
|
58
63
|
|
59
64
|
def render(self, environment: Optional[Environment] = None, black: bool = False) -> str:
|
@@ -95,14 +100,11 @@ class TemplateModel(BaseModel):
|
|
95
100
|
def environment(cls) -> Environment:
|
96
101
|
"""
|
97
102
|
Default environment for Template models.
|
98
|
-
|
99
103
|
uses a :class:`jinja2.PackageLoader` for the templates directory within this module
|
100
104
|
with the ``trim_blocks`` and ``lstrip_blocks`` parameters set to ``True`` so that the
|
101
105
|
default templates could be written in a more readable way.
|
102
106
|
"""
|
103
|
-
return
|
104
|
-
loader=PackageLoader("linkml.generators.pydanticgen", "templates"), trim_blocks=True, lstrip_blocks=True
|
105
|
-
)
|
107
|
+
return copy(cls._environment)
|
106
108
|
|
107
109
|
if int(PYDANTIC_VERSION[0]) < 2:
|
108
110
|
# simulate pydantic 2's model_fields behavior
|
linkml/generators/pythongen.py
CHANGED
@@ -4,6 +4,7 @@ import os
|
|
4
4
|
import re
|
5
5
|
from copy import copy
|
6
6
|
from dataclasses import dataclass
|
7
|
+
from pathlib import Path
|
7
8
|
from types import ModuleType
|
8
9
|
from typing import Callable, Dict, Iterator, List, Optional, Set, Tuple, Union
|
9
10
|
|
@@ -62,6 +63,8 @@ class PythonGenerator(Generator):
|
|
62
63
|
emit_metadata: bool = True
|
63
64
|
|
64
65
|
def __post_init__(self) -> None:
|
66
|
+
if isinstance(self.schema, Path):
|
67
|
+
self.schema = str(self.schema)
|
65
68
|
self.sourcefile = self.schema
|
66
69
|
self.schemaview = SchemaView(self.schema, base_dir=self.base_dir)
|
67
70
|
super().__post_init__()
|
@@ -182,11 +185,8 @@ dataclasses._init_fn = dataclasses_init_fn_with_kwargs
|
|
182
185
|
# Slots
|
183
186
|
{self.gen_slotdefs()}"""
|
184
187
|
|
185
|
-
def end_schema(self, **_):
|
186
|
-
|
187
|
-
re.sub(r" +\n", "\n", self.gen_schema().replace("\t", " ")).strip(" "),
|
188
|
-
end="",
|
189
|
-
)
|
188
|
+
def end_schema(self, **_) -> str:
|
189
|
+
return re.sub(r" +\n", "\n", self.gen_schema().replace("\t", " ")).strip(" ")
|
190
190
|
|
191
191
|
def gen_imports(self) -> str:
|
192
192
|
list_ents = [f"from {k} import {', '.join(v)}" for k, v in self.gen_import_list().items()]
|
linkml/generators/rdfgen.py
CHANGED
@@ -7,10 +7,12 @@ Generate a JSON LD representation of the model
|
|
7
7
|
|
8
8
|
import os
|
9
9
|
import urllib.parse as urlparse
|
10
|
+
from copy import deepcopy
|
10
11
|
from dataclasses import dataclass
|
11
12
|
from typing import List, Optional
|
12
13
|
|
13
14
|
import click
|
15
|
+
from linkml_runtime.linkml_model import SchemaDefinition
|
14
16
|
from rdflib import Graph
|
15
17
|
from rdflib.plugin import Parser as rdflib_Parser
|
16
18
|
from rdflib.plugin import plugins as rdflib_plugins
|
@@ -35,13 +37,19 @@ class RDFGenerator(Generator):
|
|
35
37
|
# ObjectVars
|
36
38
|
emit_metadata: bool = False
|
37
39
|
context: List[str] = None
|
40
|
+
original_schema: SchemaDefinition = None
|
41
|
+
"""See https://github.com/linkml/linkml/issues/871"""
|
42
|
+
|
43
|
+
def __post_init__(self):
|
44
|
+
self.original_schema = deepcopy(self.schema)
|
45
|
+
super().__post_init__()
|
38
46
|
|
39
47
|
def _data(self, g: Graph) -> str:
|
40
48
|
return g.serialize(format="turtle" if self.format == "ttl" else self.format)
|
41
49
|
|
42
|
-
def end_schema(self, output: Optional[str] = None, context: str = None, **_) ->
|
50
|
+
def end_schema(self, output: Optional[str] = None, context: str = None, **_) -> str:
|
43
51
|
gen = JSONLDGenerator(
|
44
|
-
self,
|
52
|
+
self.original_schema,
|
45
53
|
format=JSONLDGenerator.valid_formats[0],
|
46
54
|
metadata=self.emit_metadata,
|
47
55
|
importmap=self.importmap,
|
@@ -59,11 +67,12 @@ class RDFGenerator(Generator):
|
|
59
67
|
base=str(self.namespaces._base),
|
60
68
|
prefix=True,
|
61
69
|
)
|
70
|
+
out = self._data(graph)
|
62
71
|
if output:
|
63
72
|
with open(output, "w", encoding="UTF-8") as outf:
|
64
|
-
outf.write(
|
65
|
-
|
66
|
-
|
73
|
+
outf.write(out)
|
74
|
+
|
75
|
+
return out
|
67
76
|
|
68
77
|
|
69
78
|
@shared_arguments(RDFGenerator)
|
linkml/generators/shaclgen.py
CHANGED
@@ -22,7 +22,8 @@ class ShaclGenerator(Generator):
|
|
22
22
|
# ClassVars
|
23
23
|
closed: bool = True
|
24
24
|
"""True means add 'sh:closed=true' to all shapes, except of mixin shapes and shapes, that have parents"""
|
25
|
-
|
25
|
+
suffix: str = None
|
26
|
+
"""parameterized suffix to be appended. No suffix per default."""
|
26
27
|
generatorname = os.path.basename(__file__)
|
27
28
|
generatorversion = "0.0.1"
|
28
29
|
valid_formats = ["ttl"]
|
@@ -35,10 +36,11 @@ class ShaclGenerator(Generator):
|
|
35
36
|
super().__post_init__()
|
36
37
|
self.generate_header()
|
37
38
|
|
38
|
-
def generate_header(self):
|
39
|
-
|
39
|
+
def generate_header(self) -> str:
|
40
|
+
out = f"\n# metamodel_version: {self.schema.metamodel_version}"
|
40
41
|
if self.schema.version:
|
41
|
-
|
42
|
+
out += f"\n# version: {self.schema.version}"
|
43
|
+
return out
|
42
44
|
|
43
45
|
def serialize(self, **args) -> str:
|
44
46
|
g = self.as_graph()
|
@@ -59,9 +61,12 @@ class ShaclGenerator(Generator):
|
|
59
61
|
|
60
62
|
def shape_pv(p, v):
|
61
63
|
if v is not None:
|
62
|
-
g.add((
|
64
|
+
g.add((class_uri_with_suffix, p, v))
|
63
65
|
|
64
66
|
class_uri = URIRef(sv.get_uri(c, expand=True))
|
67
|
+
class_uri_with_suffix = class_uri
|
68
|
+
if self.suffix is not None:
|
69
|
+
class_uri_with_suffix += self.suffix
|
65
70
|
shape_pv(RDF.type, SH.NodeShape)
|
66
71
|
shape_pv(SH.targetClass, class_uri) # TODO
|
67
72
|
if self.closed:
|
@@ -211,14 +216,21 @@ def add_simple_data_type(func: Callable, r: ElementName) -> None:
|
|
211
216
|
|
212
217
|
|
213
218
|
@shared_arguments(ShaclGenerator)
|
219
|
+
@click.command()
|
214
220
|
@click.option(
|
215
221
|
"--closed/--non-closed",
|
216
222
|
default=True,
|
217
223
|
show_default=True,
|
218
224
|
help="Use '--closed' to generate closed SHACL shapes. Use '--non-closed' to generate open SHACL shapes.",
|
219
225
|
)
|
226
|
+
@click.option(
|
227
|
+
"-s",
|
228
|
+
"--suffix",
|
229
|
+
default=None,
|
230
|
+
show_default=True,
|
231
|
+
help="Use --suffix to append given string to SHACL class name (e. g. --suffix Shape: Person becomes PersonShape).",
|
232
|
+
)
|
220
233
|
@click.version_option(__version__, "-V", "--version")
|
221
|
-
@click.command()
|
222
234
|
def cli(yamlfile, **args):
|
223
235
|
"""Generate SHACL turtle from a LinkML model"""
|
224
236
|
gen = ShaclGenerator(yamlfile, **args)
|
linkml/generators/shexgen.py
CHANGED
@@ -55,12 +55,12 @@ class ShExGenerator(Generator):
|
|
55
55
|
self.namespaces.join(self.namespaces[METAMODEL_NAMESPACE_NAME], "")
|
56
56
|
) # URI for the metamodel
|
57
57
|
self.base = Namespace(self.namespaces.join(self.namespaces._base, "")) # Base URI for what is being modeled
|
58
|
-
self.generate_header()
|
59
58
|
|
60
|
-
def generate_header(self):
|
61
|
-
|
59
|
+
def generate_header(self) -> str:
|
60
|
+
out = f"# metamodel_version: {self.schema.metamodel_version}\n"
|
62
61
|
if self.schema.version:
|
63
|
-
|
62
|
+
out += f"# version: {self.schema.version}\n"
|
63
|
+
return out
|
64
64
|
|
65
65
|
def visit_schema(self, **_):
|
66
66
|
# Adjust the schema context to include the base model URI
|
@@ -80,6 +80,8 @@ class ShExGenerator(Generator):
|
|
80
80
|
else:
|
81
81
|
typeof_uri = self._class_or_type_uri(typ.typeof)
|
82
82
|
self.shapes.append(Shape(id=model_uri, expression=typeof_uri))
|
83
|
+
if self.format != "json":
|
84
|
+
return self.generate_header()
|
83
85
|
|
84
86
|
def visit_class(self, cls: ClassDefinition) -> bool:
|
85
87
|
self.shape = Shape()
|
@@ -160,7 +162,7 @@ class ShExGenerator(Generator):
|
|
160
162
|
else:
|
161
163
|
constraint.valueExpr = self._class_or_type_uri(slot.range)
|
162
164
|
|
163
|
-
def end_schema(self, output: Optional[str] = None, **_) ->
|
165
|
+
def end_schema(self, output: Optional[str] = None, **_) -> str:
|
164
166
|
self.shex.shapes = self.shapes if self.shapes else [Shape()]
|
165
167
|
shex = as_json_1(self.shex)
|
166
168
|
if self.format == "rdf":
|
@@ -172,11 +174,11 @@ class ShExGenerator(Generator):
|
|
172
174
|
g = Graph()
|
173
175
|
self.namespaces.load_graph(g)
|
174
176
|
shex = str(ShExC(self.shex, base=sfx(self.namespaces._base), namespaces=g))
|
177
|
+
|
175
178
|
if output:
|
176
179
|
with open(output, "w", encoding="UTF-8") as outf:
|
177
180
|
outf.write(shex)
|
178
|
-
|
179
|
-
print(shex)
|
181
|
+
return shex
|
180
182
|
|
181
183
|
def _class_or_type_uri(
|
182
184
|
self,
|