linkml 1.8.5__py3-none-any.whl → 1.8.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.
Files changed (39) hide show
  1. linkml/cli/main.py +2 -0
  2. linkml/generators/common/build.py +1 -2
  3. linkml/generators/common/lifecycle.py +18 -2
  4. linkml/generators/dbmlgen.py +173 -0
  5. linkml/generators/erdiagramgen.py +1 -0
  6. linkml/generators/jsonldcontextgen.py +7 -1
  7. linkml/generators/jsonldgen.py +1 -3
  8. linkml/generators/jsonschemagen.py +84 -53
  9. linkml/generators/linkmlgen.py +13 -1
  10. linkml/generators/mermaidclassdiagramgen.py +133 -0
  11. linkml/generators/owlgen.py +16 -14
  12. linkml/generators/plantumlgen.py +17 -10
  13. linkml/generators/pydanticgen/array.py +21 -61
  14. linkml/generators/pydanticgen/templates/attribute.py.jinja +1 -1
  15. linkml/generators/shacl/shacl_ifabsent_processor.py +2 -1
  16. linkml/generators/shaclgen.py +16 -5
  17. linkml/generators/shexgen.py +1 -3
  18. linkml/generators/summarygen.py +1 -3
  19. linkml/generators/typescriptgen.py +3 -1
  20. linkml/generators/yamlgen.py +1 -3
  21. linkml/linter/rules.py +3 -1
  22. linkml/transformers/logical_model_transformer.py +7 -5
  23. linkml/utils/converter.py +17 -0
  24. linkml/utils/deprecation.py +10 -0
  25. linkml/utils/generator.py +11 -2
  26. linkml/utils/helpers.py +65 -0
  27. linkml/utils/validation.py +2 -1
  28. linkml/validator/__init__.py +2 -2
  29. linkml/validator/plugins/jsonschema_validation_plugin.py +1 -0
  30. linkml/validator/report.py +4 -1
  31. linkml/validator/validator.py +4 -4
  32. linkml/validators/jsonschemavalidator.py +10 -0
  33. linkml/validators/sparqlvalidator.py +7 -0
  34. linkml/workspaces/example_runner.py +21 -4
  35. {linkml-1.8.5.dist-info → linkml-1.8.7.dist-info}/METADATA +2 -2
  36. {linkml-1.8.5.dist-info → linkml-1.8.7.dist-info}/RECORD +39 -37
  37. {linkml-1.8.5.dist-info → linkml-1.8.7.dist-info}/entry_points.txt +2 -0
  38. {linkml-1.8.5.dist-info → linkml-1.8.7.dist-info}/LICENSE +0 -0
  39. {linkml-1.8.5.dist-info → linkml-1.8.7.dist-info}/WHEEL +0 -0
@@ -0,0 +1,133 @@
1
+ import importlib
2
+ import logging
3
+ import os
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from typing import List, Optional
7
+
8
+ import click
9
+ from jinja2 import Environment, FileSystemLoader
10
+ from linkml_runtime.linkml_model.meta import Element, SlotDefinition
11
+ from linkml_runtime.utils.schemaview import SchemaView
12
+
13
+ from linkml.generators.docgen import DocGenerator, customize_environment
14
+ from linkml.utils.generator import Generator, shared_arguments
15
+
16
+
17
+ @dataclass
18
+ class MermaidClassDiagramGenerator(Generator):
19
+ """This generator creates Mermaid Class diagrams (https://mermaid.js.org/syntax/classDiagram.html)
20
+ for individual classes in a given LinkML schema and outputs them as Markdown files. It uses
21
+ a jinja template to render these class diagrams, which can be customized by the user and pointed
22
+ to using the `--template-file` option. If no template is provided, then the default template
23
+ file provided by LinkML and present at linkml/generators/docgen/class_diagram.md.jinja2 will be used.
24
+ The generator also has the option of specifying a certain set of classes for which you want the
25
+ class diagrams to be generated by using the `--classes` option.
26
+ """
27
+
28
+ generatorname = os.path.basename(__file__)
29
+ generatorversion = "0.0.1"
30
+ valid_formats = ["markdown"]
31
+ uses_schemaloader = False
32
+ requires_metamodel = False
33
+
34
+ directory: Optional[str] = None # output directory with generated markdown files
35
+ template_file: Optional[str] = None # custom/default jinja template for class diagrams
36
+ classes: List[str] = field(default_factory=list) # optional subset of classes
37
+
38
+ def __post_init__(self):
39
+ super().__post_init__()
40
+ self.logger = logging.getLogger(__name__)
41
+ self.schemaview = SchemaView(self.schema, merge_imports=self.mergeimports)
42
+
43
+ # set the output directory
44
+ self.output_directory = Path(self.directory)
45
+
46
+ # set the template file
47
+ if not self.template_file:
48
+ package_dir = os.path.dirname(importlib.util.find_spec("linkml").origin)
49
+ # Default location of the template file
50
+ self.template_file = os.path.join(package_dir, "generators", "docgen", "class_diagram.md.jinja2")
51
+
52
+ def generate_class_diagrams(self):
53
+ """Generate Mermaid class diagrams for the specified subset of classes
54
+ or all classes if none are specified.
55
+ """
56
+ self.output_directory.mkdir(parents=True, exist_ok=True)
57
+
58
+ template_folder = os.path.dirname(self.template_file)
59
+ template_name = os.path.basename(self.template_file)
60
+ loader = FileSystemLoader(template_folder)
61
+ env = Environment(loader=loader)
62
+ customize_environment(env)
63
+
64
+ template = env.get_template(template_name)
65
+
66
+ all_classes = self.schemaview.all_classes()
67
+
68
+ if self.classes:
69
+ class_items = [(cn, all_classes[cn]) for cn in self.classes if cn in all_classes]
70
+ else:
71
+ class_items = list(all_classes.items())
72
+
73
+ for cn, class_def in class_items:
74
+ self.logger.info(f"Generating Mermaid diagram for class: {cn}")
75
+ rendered = template.render(gen=self, element=class_def, schemaview=self.schemaview)
76
+ outfile = self.output_directory / f"{cn}.md"
77
+ with open(outfile, "w", encoding="utf-8") as f:
78
+ f.write(rendered)
79
+
80
+ def cardinality(self, slot: SlotDefinition) -> str:
81
+ """Reuses the cardinality logic from DocGenerator."""
82
+ return DocGenerator.cardinality(slot)
83
+
84
+ def mermaid_directive(self) -> str:
85
+ """Provides the code fence directive (e.g., `mermaid` or `{mermaid}`)."""
86
+ return "mermaid"
87
+
88
+ def name(self, element: Element) -> str:
89
+ """Returns the canonical name for an element."""
90
+ return element.name
91
+
92
+ def all_type_object_names(self):
93
+ return list(self.schemaview.all_types().keys())
94
+
95
+
96
+ @shared_arguments(MermaidClassDiagramGenerator)
97
+ @click.command()
98
+ @click.option(
99
+ "--template-file",
100
+ "-t",
101
+ type=click.Path(exists=True),
102
+ default=None,
103
+ help="Path to Jinja template for class diagrams.",
104
+ )
105
+ @click.option(
106
+ "--directory",
107
+ "-d",
108
+ type=click.Path(),
109
+ required=True,
110
+ help="Folder in which to write the Markdown files.",
111
+ )
112
+ @click.option(
113
+ "--classes",
114
+ "-c",
115
+ multiple=True,
116
+ help="One or more classes in the schema for which to generate diagrams. "
117
+ "If omitted, diagrams for all classes are generated.",
118
+ )
119
+ @click.version_option(click.__version__, "-V", "--version")
120
+ def cli(yamlfile, template_file, directory, classes, **args):
121
+ logging.basicConfig(level=logging.INFO)
122
+ gen = MermaidClassDiagramGenerator(
123
+ schema=yamlfile,
124
+ template_file=template_file,
125
+ directory=directory,
126
+ classes=list(classes), # convert tuple to a list
127
+ **args,
128
+ )
129
+ gen.generate_class_diagrams()
130
+
131
+
132
+ if __name__ == "__main__":
133
+ cli()
@@ -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.
@@ -705,18 +708,10 @@ class OwlSchemaGenerator(Generator):
705
708
  owl_exprs.append(eq_uri)
706
709
  if element.equals_string_in:
707
710
  equals_string_in = element.equals_string_in
708
- if is_literal is None:
709
- logger.warning(f"ignoring equals_string={equals_string_in} as unable to tell if literal")
710
- elif is_literal:
711
- dt_exprs = [
712
- self._datatype_restriction(XSD.string, [self._facet(XSD.pattern, s)]) for s in equals_string_in
713
- ]
714
- union_expr = self._union_of(dt_exprs, owl_types={RDFS.Literal})
715
- owl_exprs.append(union_expr)
716
- owl_types.add(RDFS.Literal)
717
- else:
718
- eq_uris = [URIRef(self.schemaview.expand_curie(s)) for s in equals_string_in]
719
- owl_exprs.append(self._union_of(eq_uris))
711
+ literals = [Literal(s) for s in equals_string_in]
712
+ one_of_expr = self._boolean_expression(literals, OWL.oneOf, owl_types={RDFS.Literal})
713
+ owl_exprs.append(one_of_expr)
714
+ owl_types.add(RDFS.Literal)
720
715
  for constraint_prop, constraint_val in constraints.items():
721
716
  if is_literal is not None and not is_literal:
722
717
  # In LinkML, it is permissible to have a literal constraints on slots that refer to
@@ -1137,7 +1132,7 @@ class OwlSchemaGenerator(Generator):
1137
1132
 
1138
1133
  def _boolean_expression(
1139
1134
  self,
1140
- exprs: List[Union[BNode, URIRef]],
1135
+ exprs: List[Union[BNode, URIRef, Literal]],
1141
1136
  predicate: URIRef,
1142
1137
  node: Optional[URIRef] = None,
1143
1138
  owl_types: Set[OWL_TYPE] = None,
@@ -1254,7 +1249,7 @@ class OwlSchemaGenerator(Generator):
1254
1249
  if pv.meaning:
1255
1250
  return URIRef(self.schemaview.expand_curie(pv.meaning))
1256
1251
  else:
1257
- return URIRef(enum_uri + "#" + pv.text.replace(" ", "+"))
1252
+ return URIRef(enum_uri + self.enum_iri_separator + pv.text.replace(" ", "+"))
1258
1253
 
1259
1254
  def slot_owl_type(self, slot: SlotDefinition) -> URIRef:
1260
1255
  sv = self.schemaview
@@ -1353,6 +1348,13 @@ class OwlSchemaGenerator(Generator):
1353
1348
  show_default=True,
1354
1349
  help="Default OWL type for permissible values",
1355
1350
  )
1351
+ @click.option(
1352
+ "--enum-iri-separator",
1353
+ default="#",
1354
+ is_flag=False,
1355
+ show_default=True,
1356
+ help="IRI separator for enums.",
1357
+ )
1356
1358
  @click.version_option(__version__, "-V", "--version")
1357
1359
  def cli(yamlfile, metadata_profile: str, **kwargs):
1358
1360
  """Generate an OWL representation of a LinkML model
@@ -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
- if load_image:
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}")
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 TYPE_CHECKING, Any, ClassVar, Generic, Iterable, List, Optional, Type, TypeVar, Union, get_args
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]) >= 2:
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 TYPE_CHECKING:
19
- from pydantic import GetCoreSchemaHandler
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 Annotated
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
- _RecursiveListType = Iterable[Union[_T, Iterable["_RecursiveListType"]]]
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 > 8",
56
+ condition="sys.version_info.minor >= 12",
95
57
  module="typing",
96
- objects=[ObjectImport(name="Annotated")],
97
- alternative=Import(module="typing_extensions", objects=[ObjectImport(name="Annotated")]),
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
- '_RecursiveListType = Iterable[Union[_T, Iterable["_RecursiveListType"]]]',
107
- AnyShapeArrayType,
108
- "AnyShapeArray = Annotated[_RecursiveListType, AnyShapeArrayType]",
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")])
@@ -1,4 +1,4 @@
1
- {{name}}: {{ range }} = Field({{ field }}
1
+ {{name}}: {{ range }} = Field(default={{ field }}
2
2
  {%- if title != None %}, title="{{title}}"{% endif -%}
3
3
  {%- if description %}, description="""{{description}}"""{% endif -%}
4
4
  {%- if equals_number != None %}
@@ -62,7 +62,8 @@ class ShaclIfAbsentProcessor(IfAbsentProcessor):
62
62
  return Literal(f"{year}-{month}-{day}T{hour}:{minutes}:{seconds}", datatype=ShaclDataType.DATETIME.uri_ref)
63
63
 
64
64
  def map_uri_or_curie_default_value(self, default_value: str, slot: SlotDefinition, cls: ClassDefinition):
65
- raise NotImplementedError()
65
+ uri = URIRef(self.schema_view.expand_curie(default_value))
66
+ return Literal(uri, datatype=ShaclDataType.URI.uri_ref)
66
67
 
67
68
  def map_curie_default_value(self, default_value: str, slot: SlotDefinition, cls: ClassDefinition):
68
69
  return Literal(default_value, datatype=ShaclDataType.CURIE.uri_ref)
@@ -116,10 +116,20 @@ class ShaclGenerator(Generator):
116
116
  order += 1
117
117
  prop_pv_literal(SH.name, s.title)
118
118
  prop_pv_literal(SH.description, s.description)
119
- if not s.multivalued:
120
- prop_pv_literal(SH.maxCount, 1)
121
- if s.required:
119
+ # minCount
120
+ if s.minimum_cardinality:
121
+ prop_pv_literal(SH.minCount, s.minimum_cardinality)
122
+ elif s.exact_cardinality:
123
+ prop_pv_literal(SH.minCount, s.exact_cardinality)
124
+ elif s.required:
122
125
  prop_pv_literal(SH.minCount, 1)
126
+ # maxCount
127
+ if s.maximum_cardinality:
128
+ prop_pv_literal(SH.maxCount, s.maximum_cardinality)
129
+ elif s.exact_cardinality:
130
+ prop_pv_literal(SH.maxCount, s.exact_cardinality)
131
+ elif not s.multivalued:
132
+ prop_pv_literal(SH.maxCount, 1)
123
133
  prop_pv_literal(SH.minInclusive, s.minimum_value)
124
134
  prop_pv_literal(SH.maxInclusive, s.maximum_value)
125
135
 
@@ -198,8 +208,6 @@ class ShaclGenerator(Generator):
198
208
  add_simple_data_type(prop_pv, r)
199
209
  if s.pattern:
200
210
  prop_pv(SH.pattern, Literal(s.pattern))
201
- if s.annotations and self.include_annotations:
202
- self._add_annotations(prop_pv, s)
203
211
  if s.equals_string:
204
212
  # Map equal_string and equal_string_in to sh:in
205
213
  self._and_equals_string(g, prop_pv, [s.equals_string])
@@ -207,6 +215,9 @@ class ShaclGenerator(Generator):
207
215
  # Map equal_string and equal_string_in to sh:in
208
216
  self._and_equals_string(g, prop_pv, s.equals_string_in)
209
217
 
218
+ if s.annotations and self.include_annotations:
219
+ self._add_annotations(prop_pv, s)
220
+
210
221
  default_value = ifabsent_processor.process_slot(s, c)
211
222
  if default_value:
212
223
  prop_pv(SH.defaultValue, default_value)
@@ -1,6 +1,4 @@
1
- """Generate ShEx definition of a model
2
-
3
- """
1
+ """Generate ShEx definition of a model"""
4
2
 
5
3
  import os
6
4
  import urllib.parse as urlparse
@@ -1,6 +1,4 @@
1
- """Generate Summary Spreadsheets
2
-
3
- """
1
+ """Generate Summary Spreadsheets"""
4
2
 
5
3
  import os
6
4
  from csv import DictWriter
@@ -280,7 +280,9 @@ def cli(yamlfile, gen_type_utils=False, include_induced_slots=False, output=None
280
280
  gen = TypescriptGenerator(
281
281
  yamlfile, gen_type_utils=gen_type_utils, include_induced_slots=include_induced_slots, **args
282
282
  )
283
- gen.serialize(output=output)
283
+ serialized = gen.serialize(output=output)
284
+ if output is None:
285
+ print(serialized)
284
286
 
285
287
 
286
288
  if __name__ == "__main__":
@@ -1,6 +1,4 @@
1
- """Validate linkml input and optionally emit completely resolved biolink yaml output
2
-
3
- """
1
+ """Validate linkml input and optionally emit completely resolved biolink yaml output"""
4
2
 
5
3
  import os
6
4
  from dataclasses import dataclass
linkml/linter/rules.py CHANGED
@@ -134,7 +134,9 @@ class TreeRootClassRule(LinterRule):
134
134
  if self.config.validate_existing_class_name:
135
135
  for tree_root in tree_roots:
136
136
  if str(tree_root.name) != self.config.root_class_name:
137
- yield LinterProblem(message=f"Tree root class has name '{tree_root.name}'")
137
+ yield LinterProblem(message=f"Tree root class has an invalid name '{tree_root.name}'")
138
+ if len(tree_roots) > 1:
139
+ yield LinterProblem("Schema has more than one class with `tree_root: true`")
138
140
  else:
139
141
  if fix:
140
142
  container = ClassDefinition(self.config.root_class_name, tree_root=True)
@@ -409,11 +409,13 @@ class LogicalModelTransformer(ModelTransformer):
409
409
  target_class_name: ClassDefinitionName,
410
410
  ancestors: List[ClassDefinitionName],
411
411
  ):
412
- anc_classes = [self.schemaview.get_class(anc) for anc in ancestors]
412
+ anc_classes = [self.schemaview.get_class(ancestor) for ancestor in ancestors]
413
413
  attributes: Dict[SlotDefinitionName, SlotDefinition] = {}
414
- for anc in anc_classes:
415
- top_level_slots = [(s, target_schema.slots[s]) for s in anc.slots]
416
- for slot_name, slot_expr in list(anc.attributes.items()) + list(anc.slot_usage.items()) + top_level_slots:
414
+ for ancestor_class in anc_classes:
415
+ top_level_slots = [(s, target_schema.slots[s]) for s in ancestor_class.slots]
416
+ for slot_name, slot_expr in (
417
+ list(ancestor_class.attributes.items()) + list(ancestor_class.slot_usage.items()) + top_level_slots
418
+ ):
417
419
  if slot_name not in attributes:
418
420
  attributes[slot_name] = SlotDefinition(slot_name)
419
421
  sx = attributes[slot_name]
@@ -644,7 +646,7 @@ class LogicalModelTransformer(ModelTransformer):
644
646
  if slot_expression.all_of:
645
647
  exprs.append(logictools.And(*[self._as_logical_expression(subx) for subx in slot_expression.all_of]))
646
648
  if slot_expression.exactly_one_of:
647
- # TODO: disjointness
649
+ # TODO: disjointedness
648
650
  exprs.append(logictools.Or(*[self._as_logical_expression(subx) for subx in slot_expression.exactly_one_of]))
649
651
  if slot_expression.none_of:
650
652
  exprs.append(
linkml/utils/converter.py CHANGED
@@ -1,8 +1,10 @@
1
1
  import logging
2
2
  import os
3
+ import pathlib
3
4
  import sys
4
5
 
5
6
  import click
7
+ import yaml
6
8
  from linkml_runtime.linkml_model import Prefix
7
9
  from linkml_runtime.utils import inference_utils
8
10
  from linkml_runtime.utils.compile_python import compile_python
@@ -55,6 +57,7 @@ logger = logging.getLogger(__name__)
55
57
  @click.option("--index-slot", "-S", help="top level slot. Required for CSV dumping/loading")
56
58
  @click.option("--schema", "-s", help="Path to schema specified as LinkML yaml")
57
59
  @click.option("--prefix", "-P", multiple=True, help="Prefixmap base=URI pairs")
60
+ @click.option("--prefix-file", help="Path to yaml file containing base=URI pairs")
58
61
  @click.option(
59
62
  "--validate/--no-validate",
60
63
  default=True,
@@ -79,6 +82,7 @@ def cli(
79
82
  input_format=None,
80
83
  output_format=None,
81
84
  prefix=None,
85
+ prefix_file=None,
82
86
  target_class_from_path=None,
83
87
  schema=None,
84
88
  validate=None,
@@ -99,6 +103,8 @@ def cli(
99
103
 
100
104
  For more information, see https://linkml.io/linkml/data/index.html
101
105
  """
106
+ if prefix and prefix_file is not None:
107
+ raise Exception("Either set prefix OR prefix_file, not both.")
102
108
  if prefix is None:
103
109
  prefix = []
104
110
  if module is None:
@@ -113,6 +119,17 @@ def cli(
113
119
  for p in prefix:
114
120
  base, uri = p.split("=")
115
121
  prefix_map[base] = uri
122
+ if prefix_file is not None:
123
+ prefix_path = pathlib.Path(prefix_file).resolve()
124
+ if not prefix_path.exists():
125
+ raise Exception(f"Path {prefix_file} to prefix map does not exists.")
126
+ with open(prefix_path, "r") as prefix_stream:
127
+ raw_prefix_map = yaml.safe_load(prefix_stream)
128
+ prefix_file_map = raw_prefix_map.get("prefixes", None)
129
+ if prefix_file_map is None:
130
+ raise Exception("Provided prefix file does not contain the prefixes key.")
131
+ prefix_map = prefix_file_map
132
+
116
133
  if schema is not None:
117
134
  sv = SchemaView(schema)
118
135
  if prefix_map:
@@ -222,6 +222,16 @@ DEPRECATIONS = (
222
222
  recommendation="Update dependent packages to use pydantic>=2",
223
223
  issue=1925,
224
224
  ),
225
+ Deprecation(
226
+ name="validators",
227
+ deprecated_in=SemVer.from_str("1.8.6"),
228
+ removed_in=SemVer.from_str("1.9.0"),
229
+ message=(
230
+ "linkml.validators and linkml.utils.validation are the older versions "
231
+ "of linkml.validator and have unmaintained, duplicated functionality"
232
+ ),
233
+ recommendation="Update to use linkml.validator",
234
+ ),
225
235
  ) # type: tuple[Deprecation, ...]
226
236
 
227
237
  EMITTED = set() # type: set[str]
linkml/utils/generator.py CHANGED
@@ -27,6 +27,7 @@ from typing import Callable, ClassVar, Dict, List, Mapping, Optional, Set, TextI
27
27
 
28
28
  import click
29
29
  from click import Argument, Command, Option
30
+ from jsonasobj2 import JsonObj
30
31
  from linkml_runtime import SchemaView
31
32
  from linkml_runtime.linkml_model.meta import (
32
33
  ClassDefinition,
@@ -265,8 +266,16 @@ class Generator(metaclass=abc.ABCMeta):
265
266
  def _init_namespaces(self):
266
267
  if self.namespaces is None:
267
268
  self.namespaces = Namespaces()
268
- for prefix in self.schema.prefixes.values():
269
- self.namespaces[prefix.prefix_prefix] = prefix.prefix_reference
269
+ if isinstance(self.schema.prefixes, dict):
270
+ for key, value in self.schema.prefixes.items():
271
+ self.namespaces[key] = value
272
+ elif isinstance(self.schema.prefixes, JsonObj):
273
+ prefixes = vars(self.schema.prefixes)
274
+ for key, value in prefixes.items():
275
+ self.namespaces[key] = value
276
+ else:
277
+ for prefix in self.schema.prefixes.values():
278
+ self.namespaces[prefix.prefix_prefix] = prefix.prefix_reference
270
279
 
271
280
  def serialize(self, **kwargs) -> str:
272
281
  """