linkml 1.8.2__py3-none-any.whl → 1.8.4__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 (50) hide show
  1. linkml/cli/main.py +4 -0
  2. linkml/generators/__init__.py +2 -0
  3. linkml/generators/common/ifabsent_processor.py +286 -0
  4. linkml/generators/docgen/index.md.jinja2 +6 -6
  5. linkml/generators/docgen.py +64 -14
  6. linkml/generators/golanggen.py +3 -1
  7. linkml/generators/jsonldcontextgen.py +0 -1
  8. linkml/generators/jsonschemagen.py +4 -2
  9. linkml/generators/owlgen.py +36 -17
  10. linkml/generators/projectgen.py +13 -11
  11. linkml/generators/pydanticgen/array.py +340 -56
  12. linkml/generators/pydanticgen/build.py +4 -2
  13. linkml/generators/pydanticgen/pydanticgen.py +46 -24
  14. linkml/generators/pydanticgen/template.py +108 -3
  15. linkml/generators/pydanticgen/templates/imports.py.jinja +11 -3
  16. linkml/generators/pydanticgen/templates/module.py.jinja +1 -3
  17. linkml/generators/pydanticgen/templates/validator.py.jinja +2 -2
  18. linkml/generators/python/__init__.py +1 -0
  19. linkml/generators/python/python_ifabsent_processor.py +92 -0
  20. linkml/generators/pythongen.py +19 -31
  21. linkml/generators/shacl/__init__.py +1 -3
  22. linkml/generators/shacl/shacl_data_type.py +1 -1
  23. linkml/generators/shacl/shacl_ifabsent_processor.py +89 -0
  24. linkml/generators/shaclgen.py +39 -13
  25. linkml/generators/sparqlgen.py +3 -1
  26. linkml/generators/sqlalchemygen.py +5 -3
  27. linkml/generators/sqltablegen.py +4 -2
  28. linkml/generators/typescriptgen.py +13 -6
  29. linkml/linter/linter.py +2 -1
  30. linkml/transformers/logical_model_transformer.py +3 -3
  31. linkml/transformers/relmodel_transformer.py +18 -4
  32. linkml/utils/converter.py +3 -1
  33. linkml/utils/exceptions.py +11 -0
  34. linkml/utils/execute_tutorial.py +22 -20
  35. linkml/utils/generator.py +6 -4
  36. linkml/utils/mergeutils.py +4 -2
  37. linkml/utils/schema_fixer.py +5 -5
  38. linkml/utils/schemaloader.py +5 -3
  39. linkml/utils/sqlutils.py +3 -1
  40. linkml/validator/plugins/pydantic_validation_plugin.py +1 -1
  41. linkml/validators/jsonschemavalidator.py +3 -1
  42. linkml/validators/sparqlvalidator.py +5 -3
  43. linkml/workspaces/example_runner.py +3 -1
  44. {linkml-1.8.2.dist-info → linkml-1.8.4.dist-info}/METADATA +3 -1
  45. {linkml-1.8.2.dist-info → linkml-1.8.4.dist-info}/RECORD +48 -45
  46. linkml/generators/shacl/ifabsent_processor.py +0 -59
  47. linkml/utils/ifabsent_functions.py +0 -138
  48. {linkml-1.8.2.dist-info → linkml-1.8.4.dist-info}/LICENSE +0 -0
  49. {linkml-1.8.2.dist-info → linkml-1.8.4.dist-info}/WHEEL +0 -0
  50. {linkml-1.8.2.dist-info → linkml-1.8.4.dist-info}/entry_points.txt +0 -0
@@ -29,8 +29,10 @@ from rdflib import URIRef
29
29
 
30
30
  import linkml
31
31
  from linkml._version import __version__
32
+ from linkml.generators.python.python_ifabsent_processor import PythonIfAbsentProcessor
32
33
  from linkml.utils.generator import Generator, shared_arguments
33
- from linkml.utils.ifabsent_functions import ifabsent_postinit_declaration, ifabsent_value_declaration
34
+
35
+ logger = logging.getLogger(__name__)
34
36
 
35
37
 
36
38
  @dataclass
@@ -57,10 +59,10 @@ class PythonGenerator(Generator):
57
59
  dataclass_repr: bool = False
58
60
  """
59
61
  Whether generated dataclasses should also generate a default __repr__ method.
60
-
62
+
61
63
  Default ``False`` so that the parent :class:`linkml_runtime.utils.yamlutils.YAMLRoot` 's
62
64
  ``__repr__`` method is inherited for model pretty printing.
63
-
65
+
64
66
  References:
65
67
  - https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass
66
68
  """
@@ -70,11 +72,12 @@ class PythonGenerator(Generator):
70
72
  self.schema = str(self.schema)
71
73
  self.sourcefile = self.schema
72
74
  self.schemaview = SchemaView(self.schema, base_dir=self.base_dir)
75
+ self.ifabsent_processor = PythonIfAbsentProcessor(self.schemaview)
73
76
  super().__post_init__()
74
77
  if self.format is None:
75
78
  self.format = self.valid_formats[0]
76
79
  if self.schema.default_prefix == "linkml" and not self.genmeta:
77
- logging.error("Generating metamodel without --genmeta is highly inadvisable!")
80
+ logger.error("Generating metamodel without --genmeta is highly inadvisable!")
78
81
  if not self.schema.source_file and isinstance(self.sourcefile, str) and "\n" not in self.sourcefile:
79
82
  self.schema.source_file = os.path.basename(self.sourcefile)
80
83
 
@@ -87,8 +90,8 @@ class PythonGenerator(Generator):
87
90
  try:
88
91
  return compile_python(pycode)
89
92
  except NameError as e:
90
- logging.error(f"Code:\n{pycode}")
91
- logging.error(f"Error compiling generated python code: {e}")
93
+ logger.error(f"Code:\n{pycode}")
94
+ logger.error(f"Error compiling generated python code: {e}")
92
95
  raise e
93
96
 
94
97
  def visit_schema(self, **kwargs) -> None:
@@ -153,7 +156,7 @@ import re
153
156
  from jsonasobj2 import JsonObj, as_dict
154
157
  from typing import Optional, List, Union, Dict, ClassVar, Any
155
158
  from dataclasses import dataclass
156
- from datetime import date, datetime
159
+ from datetime import date, datetime, time
157
160
  {enumimports}
158
161
  from linkml_runtime.utils.slot import Slot
159
162
  from linkml_runtime.utils.metamodelcore import empty_list, empty_dict, bnode
@@ -515,7 +518,7 @@ dataclasses._init_fn = dataclasses_init_fn_with_kwargs
515
518
 
516
519
  return "\n\t".join(initializers)
517
520
 
518
- def gen_class_variable(self, cls: ClassDefinition, slot: SlotDefinition, can_be_positional: bool) -> str:
521
+ def gen_class_variable(self, cls: ClassDefinition, slot: SlotDefinition, can_be_positional: bool = False) -> str:
519
522
  """
520
523
  Generate a class variable declaration for the supplied slot. Note: the can_be_positional attribute works,
521
524
  but it makes tag/value lists unduly complex, as you can't load them with tag=..., value=... -- you HAVE
@@ -527,12 +530,9 @@ dataclasses._init_fn = dataclasses_init_fn_with_kwargs
527
530
  :param can_be_positional: True means that positional parameters are allowed.
528
531
  :return: Initializer string
529
532
  """
530
- can_be_positional = False # Force everything to be tag values
531
533
  slotname = self.slot_name(slot.name)
532
534
  slot_range, default_val = self.range_cardinality(slot, cls, can_be_positional)
533
- ifabsent_text = (
534
- ifabsent_value_declaration(slot.ifabsent, self, cls, slot) if slot.ifabsent is not None else None
535
- )
535
+ ifabsent_text = self.ifabsent_processor.process_slot(slot, cls) if slot.ifabsent is not None else None
536
536
  if ifabsent_text is not None:
537
537
  default = f"= {ifabsent_text}"
538
538
  else:
@@ -637,15 +637,6 @@ dataclasses._init_fn = dataclasses_init_fn_with_kwargs
637
637
 
638
638
  def gen_postinits(self, cls: ClassDefinition) -> str:
639
639
  """Generate all the typing and existence checks post initialize"""
640
- post_inits_pre_super = []
641
- for slot in self.domain_slots(cls):
642
- if slot.ifabsent:
643
- dflt = ifabsent_postinit_declaration(slot.ifabsent, self, cls, slot)
644
-
645
- if dflt and dflt != "None":
646
- post_inits_pre_super.append(f"if self.{self.slot_name(slot.name)} is None:")
647
- post_inits_pre_super.append(f"\tself.{self.slot_name(slot.name)} = {dflt}")
648
-
649
640
  post_inits = []
650
641
  if not (cls.mixin or cls.abstract):
651
642
  pkeys = self.primary_keys_for(cls)
@@ -676,20 +667,17 @@ dataclasses._init_fn = dataclasses_init_fn_with_kwargs
676
667
  if slot.name not in domain_slot_names and slot.designates_type:
677
668
  post_inits_designators.append(self.gen_postinit(cls, slot))
678
669
 
679
- post_inits_pre_super_line = "\n\t\t".join([p for p in post_inits_pre_super if p]) + (
680
- "\n\t\t" if post_inits_pre_super else ""
681
- )
682
670
  post_inits_post_super_line = "\n\t\t".join(post_inits_designators)
683
671
  post_inits_line = "\n\t\t".join([p for p in post_inits if p])
684
672
  return (
685
673
  (
686
674
  f"""
687
675
  def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
688
- {post_inits_pre_super_line}{post_inits_line}
676
+ {post_inits_line}
689
677
  super().__post_init__(**kwargs)
690
678
  {post_inits_post_super_line}"""
691
679
  )
692
- if post_inits_line or post_inits_pre_super_line or post_inits_post_super_line
680
+ if post_inits_line or post_inits_post_super_line
693
681
  else ""
694
682
  )
695
683
 
@@ -958,11 +946,11 @@ dataclasses._init_fn = dataclasses_init_fn_with_kwargs
958
946
 
959
947
  def forward_reference(self, slot_range: str, owning_class: str) -> bool:
960
948
  """Determine whether slot_range is a forward reference"""
961
- # logging.info(f"CHECKING: {slot_range} {owning_class}")
949
+ # logger.info(f"CHECKING: {slot_range} {owning_class}")
962
950
  if (slot_range in self.schema.classes and self.schema.classes[slot_range].imported_from) or (
963
951
  slot_range in self.schema.enums and self.schema.enums[slot_range].imported_from
964
952
  ):
965
- logging.info(
953
+ logger.info(
966
954
  f"FALSE: FORWARD: {slot_range} {owning_class} // IMP={self.schema.classes[slot_range].imported_from}"
967
955
  )
968
956
  return False
@@ -971,10 +959,10 @@ dataclasses._init_fn = dataclasses_init_fn_with_kwargs
971
959
  clist = [x.name for x in self._sort_classes(self.schema.classes.values())]
972
960
  for cname in clist:
973
961
  if cname == owning_class:
974
- logging.info(f"TRUE: OCCURS SAME: {cname} == {slot_range} owning: {owning_class}")
962
+ logger.info(f"TRUE: OCCURS SAME: {cname} == {slot_range} owning: {owning_class}")
975
963
  return True # Occurs on or after
976
964
  elif cname == slot_range:
977
- logging.info(f"FALSE: OCCURS BEFORE: {cname} == {slot_range} owning: {owning_class}")
965
+ logger.info(f"FALSE: OCCURS BEFORE: {cname} == {slot_range} owning: {owning_class}")
978
966
  return False # Occurs before
979
967
  return True
980
968
 
@@ -1228,7 +1216,7 @@ def cli(
1228
1216
  )
1229
1217
  if validate:
1230
1218
  mod = gen.compile_module()
1231
- logging.info(f"Module {mod} compiled successfully")
1219
+ logger.info(f"Module {mod} compiled successfully")
1232
1220
  print(gen.serialize(emit_metadata=head, **args))
1233
1221
 
1234
1222
 
@@ -1,3 +1 @@
1
- from linkml.generators.shacl import ifabsent_processor
2
-
3
- __all__ = ["shacl_data_type", "ifabsent_processor"]
1
+ __all__ = ["shacl_data_type", "shacl_ifabsent_processor"]
@@ -20,7 +20,7 @@ class ShaclDataType(DataType, Enum):
20
20
  FLOAT = ("float", URIRef("http://www.w3.org/2001/XMLSchema#float"))
21
21
  DOUBLE = ("double", URIRef("http://www.w3.org/2001/XMLSchema#double"))
22
22
  URI = ("uri", URIRef("http://www.w3.org/2001/XMLSchema#anyURI"))
23
- CURI = ("curi", URIRef("http://www.w3.org/2001/XMLSchema#string"))
23
+ CURIE = ("curi", URIRef("http://www.w3.org/2001/XMLSchema#string"))
24
24
  NCNAME = ("ncname", URIRef("http://www.w3.org/2001/XMLSchema#string"))
25
25
  OBJECT_IDENTIFIER = ("objectidentifier", URIRef("http://www.w3.org/ns/shex#iri"))
26
26
  NODE_IDENTIFIER = ("nodeidentifier", URIRef("http://www.w3.org/ns/shex#nonLiteral"))
@@ -0,0 +1,89 @@
1
+ from linkml_runtime.linkml_model import ClassDefinition, EnumDefinitionName, SlotDefinition
2
+ from rdflib import Literal, URIRef
3
+
4
+ from linkml.generators.common.ifabsent_processor import IfAbsentProcessor
5
+ from linkml.generators.shacl.shacl_data_type import ShaclDataType
6
+
7
+
8
+ class ShaclIfAbsentProcessor(IfAbsentProcessor):
9
+
10
+ def map_custom_default_values(self, default_value: str, slot: SlotDefinition, cls: ClassDefinition) -> (bool, str):
11
+ if default_value == "class_curie":
12
+ class_uri = self.schema_view.get_uri(cls, expand=True)
13
+ if class_uri:
14
+ return True, URIRef(class_uri)
15
+ return True, ""
16
+
17
+ return False, None
18
+
19
+ def map_enum_default_value(
20
+ self, enum_name: EnumDefinitionName, permissible_value_name: str, slot: SlotDefinition, cls: ClassDefinition
21
+ ):
22
+ return Literal(permissible_value_name)
23
+
24
+ def map_string_default_value(self, default_value: str, slot: SlotDefinition, cls: ClassDefinition):
25
+ return Literal(default_value, datatype=ShaclDataType.STRING.uri_ref)
26
+
27
+ def map_integer_default_value(self, default_value: str, slot: SlotDefinition, cls: ClassDefinition):
28
+ return Literal(default_value, datatype=ShaclDataType.INTEGER.uri_ref)
29
+
30
+ def map_boolean_true_default_value(self, slot: SlotDefinition, cls: ClassDefinition):
31
+ return Literal(True, datatype=ShaclDataType.BOOLEAN.uri_ref)
32
+
33
+ def map_boolean_false_default_value(self, slot: SlotDefinition, cls: ClassDefinition):
34
+ return Literal(False, datatype=ShaclDataType.BOOLEAN.uri_ref)
35
+
36
+ def map_float_default_value(self, default_value: str, slot: SlotDefinition, cls: ClassDefinition):
37
+ return Literal(default_value, datatype=ShaclDataType.FLOAT.uri_ref)
38
+
39
+ def map_double_default_value(self, default_value: str, slot: SlotDefinition, cls: ClassDefinition):
40
+ return Literal(default_value, datatype=ShaclDataType.DOUBLE.uri_ref)
41
+
42
+ def map_decimal_default_value(self, default_value: str, slot: SlotDefinition, cls: ClassDefinition):
43
+ return Literal(default_value, datatype=ShaclDataType.DECIMAL.uri_ref)
44
+
45
+ def map_time_default_value(self, hour: str, minutes: str, seconds: str, slot: SlotDefinition, cls: ClassDefinition):
46
+ return Literal(f"{hour}:{minutes}:{seconds}", datatype=ShaclDataType.TIME.uri_ref)
47
+
48
+ def map_date_default_value(self, year: str, month: str, day: str, slot: SlotDefinition, cls: ClassDefinition):
49
+ return Literal(f"{year}-{month}-{day}", datatype=ShaclDataType.DATE.uri_ref)
50
+
51
+ def map_datetime_default_value(
52
+ self,
53
+ year: str,
54
+ month: str,
55
+ day: str,
56
+ hour: str,
57
+ minutes: str,
58
+ seconds: str,
59
+ slot: SlotDefinition,
60
+ cls: ClassDefinition,
61
+ ):
62
+ return Literal(f"{year}-{month}-{day}T{hour}:{minutes}:{seconds}", datatype=ShaclDataType.DATETIME.uri_ref)
63
+
64
+ def map_uri_or_curie_default_value(self, default_value: str, slot: SlotDefinition, cls: ClassDefinition):
65
+ raise NotImplementedError()
66
+
67
+ def map_curie_default_value(self, default_value: str, slot: SlotDefinition, cls: ClassDefinition):
68
+ return Literal(default_value, datatype=ShaclDataType.CURIE.uri_ref)
69
+
70
+ def map_uri_default_value(self, default_value: str, slot: SlotDefinition, cls: ClassDefinition):
71
+ return Literal(default_value, datatype=ShaclDataType.URI.uri_ref)
72
+
73
+ def map_nc_name_default_value(self, default_value: str, slot: SlotDefinition, cls: ClassDefinition):
74
+ raise NotImplementedError()
75
+
76
+ def map_object_identifier_default_value(self, default_value: str, slot: SlotDefinition, cls: ClassDefinition):
77
+ raise NotImplementedError()
78
+
79
+ def map_node_identifier_default_value(self, default_value: str, slot: SlotDefinition, cls: ClassDefinition):
80
+ raise NotImplementedError()
81
+
82
+ def map_json_pointer_default_value(self, default_value: str, slot: SlotDefinition, cls: ClassDefinition):
83
+ raise NotImplementedError()
84
+
85
+ def map_json_path_default_value(self, default_value: str, slot: SlotDefinition, cls: ClassDefinition):
86
+ raise NotImplementedError()
87
+
88
+ def map_sparql_path_default_value(self, default_value: str, slot: SlotDefinition, cls: ClassDefinition):
89
+ raise NotImplementedError()
@@ -5,19 +5,21 @@ from typing import Callable, List
5
5
 
6
6
  import click
7
7
  from jsonasobj2 import JsonObj, as_dict
8
- from linkml_runtime.linkml_model.meta import ElementName
8
+ from linkml_runtime.linkml_model.meta import ClassDefinition, ElementName
9
9
  from linkml_runtime.utils.formatutils import underscore
10
10
  from linkml_runtime.utils.schemaview import SchemaView
11
11
  from linkml_runtime.utils.yamlutils import TypedNode, extended_float, extended_int, extended_str
12
12
  from rdflib import BNode, Graph, Literal, URIRef
13
13
  from rdflib.collection import Collection
14
- from rdflib.namespace import RDF, RDFS, SH, XSD
14
+ from rdflib.namespace import RDF, SH, XSD
15
15
 
16
16
  from linkml._version import __version__
17
- from linkml.generators.shacl.ifabsent_processor import IfAbsentProcessor
18
17
  from linkml.generators.shacl.shacl_data_type import ShaclDataType
18
+ from linkml.generators.shacl.shacl_ifabsent_processor import ShaclIfAbsentProcessor
19
19
  from linkml.utils.generator import Generator, shared_arguments
20
20
 
21
+ logger = logging.getLogger(__name__)
22
+
21
23
 
22
24
  @dataclass
23
25
  class ShaclGenerator(Generator):
@@ -56,7 +58,7 @@ class ShaclGenerator(Generator):
56
58
  g = Graph()
57
59
  g.bind("sh", SH)
58
60
 
59
- ifabsent_processor = IfAbsentProcessor(sv)
61
+ ifabsent_processor = ShaclIfAbsentProcessor(sv)
60
62
 
61
63
  for pfx in self.schema.prefixes.values():
62
64
  g.bind(str(pfx.prefix_prefix), pfx.prefix_reference)
@@ -74,9 +76,6 @@ class ShaclGenerator(Generator):
74
76
  shape_pv(RDF.type, SH.NodeShape)
75
77
  shape_pv(SH.targetClass, class_uri) # TODO
76
78
 
77
- if c.is_a:
78
- shape_pv(RDFS.subClassOf, URIRef(sv.get_uri(c.is_a, expand=True)))
79
-
80
79
  if self.closed:
81
80
  if c.mixin or c.abstract:
82
81
  shape_pv(SH.closed, Literal(False))
@@ -88,9 +87,9 @@ class ShaclGenerator(Generator):
88
87
  shape_pv(SH.name, Literal(c.title))
89
88
  if c.description is not None:
90
89
  shape_pv(SH.description, Literal(c.description))
91
- list_node = BNode()
92
- Collection(g, list_node, [RDF.type])
93
- shape_pv(SH.ignoredProperties, list_node)
90
+
91
+ shape_pv(SH.ignoredProperties, self._build_ignored_properties(g, c))
92
+
94
93
  if c.annotations and self.include_annotations:
95
94
  self._add_annotations(shape_pv, c)
96
95
  order = 0
@@ -130,7 +129,7 @@ class ShaclGenerator(Generator):
130
129
  # slot definition, as both are mapped to sh:in in SHACL
131
130
  if s.equals_string or s.equals_string_in:
132
131
  error = "'equals_string'/'equals_string_in' and 'any_of' are mutually exclusive"
133
- raise ValueError(f'{TypedNode.yaml_loc(s, suffix="")} {error}')
132
+ raise ValueError(f'{TypedNode.yaml_loc(str(s), suffix="")} {error}')
134
133
 
135
134
  or_node = BNode()
136
135
  prop_pv(SH["or"], or_node)
@@ -208,7 +207,9 @@ class ShaclGenerator(Generator):
208
207
  # Map equal_string and equal_string_in to sh:in
209
208
  self._and_equals_string(g, prop_pv, s.equals_string_in)
210
209
 
211
- ifabsent_processor.process_slot(prop_pv, s, class_uri)
210
+ default_value = ifabsent_processor.process_slot(s, c)
211
+ if default_value:
212
+ prop_pv(SH.defaultValue, default_value)
212
213
 
213
214
  return g
214
215
 
@@ -242,7 +243,7 @@ class ShaclGenerator(Generator):
242
243
  if rt.annotations and self.include_annotations:
243
244
  self._add_annotations(func, rt)
244
245
  else:
245
- logging.error(f"No URI for type {rt.name}")
246
+ logger.error(f"No URI for type {rt.name}")
246
247
 
247
248
  def _and_equals_string(self, g: Graph, func: Callable, values: List) -> None:
248
249
  pv_node = BNode()
@@ -299,6 +300,31 @@ class ShaclGenerator(Generator):
299
300
  )
300
301
  func(SH["in"], pv_node)
301
302
 
303
+ def _build_ignored_properties(self, g: Graph, c: ClassDefinition) -> BNode:
304
+ def collect_child_properties(class_name: str, output: set) -> None:
305
+ for childName in self.schemaview.class_children(class_name, imports=True, mixins=False, is_a=True):
306
+ output.update(
307
+ {
308
+ URIRef(self.schemaview.get_uri(prop, expand=True))
309
+ for prop in self.schemaview.class_slots(childName)
310
+ }
311
+ )
312
+ collect_child_properties(childName, output)
313
+
314
+ child_properties = set()
315
+ collect_child_properties(c.name, child_properties)
316
+
317
+ class_slot_uris = {
318
+ URIRef(self.schemaview.get_uri(prop, expand=True)) for prop in self.schemaview.class_slots(c.name)
319
+ }
320
+ ignored_properties = child_properties.difference(class_slot_uris)
321
+
322
+ list_node = BNode()
323
+ ignored_properties.add(RDF.type)
324
+ Collection(g, list_node, list(ignored_properties))
325
+
326
+ return list_node
327
+
302
328
 
303
329
  def add_simple_data_type(func: Callable, r: ElementName) -> None:
304
330
  for datatype in list(ShaclDataType):
@@ -14,6 +14,8 @@ from linkml_runtime.utils.schemaview import SchemaView
14
14
  from linkml._version import __version__
15
15
  from linkml.utils.generator import Generator, shared_arguments
16
16
 
17
+ logger = logging.getLogger(__name__)
18
+
17
19
  template = """
18
20
  {% for pfxn, pfx in schema.prefixes.items() -%}
19
21
  PREFIX {{pfxn}}: <{{pfx.prefix_reference}}>
@@ -150,7 +152,7 @@ class SparqlGenerator(Generator):
150
152
  extra = ""
151
153
  if named_graphs is not None:
152
154
  extra += f'FILTER( ?graph in ( {",".join(named_graphs)} ))'
153
- logging.info(f"Named Graphs = {named_graphs} // extra={extra}")
155
+ logger.info(f"Named Graphs = {named_graphs} // extra={extra}")
154
156
  if limit is not None and isinstance(limit, int):
155
157
  limit = f"LIMIT {limit}"
156
158
  else:
@@ -21,6 +21,8 @@ from linkml.generators.sqltablegen import SQLTableGenerator
21
21
  from linkml.transformers.relmodel_transformer import ForeignKeyPolicy, RelationalModelTransformer
22
22
  from linkml.utils.generator import Generator, shared_arguments
23
23
 
24
+ logger = logging.getLogger(__name__)
25
+
24
26
 
25
27
  class TemplateEnum(Enum):
26
28
  DECLARATIVE = "declarative"
@@ -84,7 +86,7 @@ class SQLAlchemyGenerator(Generator):
84
86
  template_obj = Template(template_str)
85
87
  if model_path is None:
86
88
  model_path = self.schema.name
87
- logging.info(f"Package for dataclasses == {model_path}")
89
+ logger.info(f"Package for dataclasses == {model_path}")
88
90
  backrefs = defaultdict(list)
89
91
  for m in tr_result.mappings:
90
92
  backrefs[m.source_class].append(m)
@@ -113,7 +115,7 @@ class SQLAlchemyGenerator(Generator):
113
115
  is_join_table=lambda c: any(tag for tag in c.annotations.keys() if tag == "linkml:derived_from"),
114
116
  classes=rel_schema_classes_ordered,
115
117
  )
116
- logging.debug(f"# Generated code:\n{code}")
118
+ logger.debug(f"# Generated code:\n{code}")
117
119
  return code
118
120
 
119
121
  def serialize(self, **kwargs) -> str:
@@ -173,7 +175,7 @@ class SQLAlchemyGenerator(Generator):
173
175
  def skip(cls: ClassDefinition) -> bool:
174
176
  is_skip = len(cls.attributes) == 0
175
177
  if is_skip:
176
- logging.error(f"SKIPPING: {cls.name}")
178
+ logger.error(f"SKIPPING: {cls.name}")
177
179
  return is_skip
178
180
 
179
181
  # TODO: move this
@@ -16,6 +16,8 @@ from linkml.transformers.relmodel_transformer import ForeignKeyPolicy, Relationa
16
16
  from linkml.utils.generator import Generator, shared_arguments
17
17
  from linkml.utils.schemaloader import SchemaLoader
18
18
 
19
+ logger = logging.getLogger(__name__)
20
+
19
21
 
20
22
  class SqlNamingPolicy(Enum):
21
23
  preserve = "preserve"
@@ -262,12 +264,12 @@ class SQLTableGenerator(Generator):
262
264
  elif range is None:
263
265
  return Text()
264
266
  else:
265
- logging.error(f"Unknown range: {range} for {slot.name} = {slot.range}")
267
+ logger.error(f"Unknown range: {range} for {slot.name} = {slot.range}")
266
268
  return Text()
267
269
  if range_base in RANGEMAP:
268
270
  return RANGEMAP[range_base]
269
271
  else:
270
- logging.error(f"UNKNOWN range base: {range_base} for {slot.name} = {slot.range}")
272
+ logger.error(f"UNKNOWN range base: {range_base} for {slot.name} = {slot.range}")
271
273
  return Text()
272
274
 
273
275
  @staticmethod
@@ -19,6 +19,9 @@ from linkml._version import __version__
19
19
  from linkml.generators.oocodegen import OOCodeGenerator
20
20
  from linkml.utils.generator import shared_arguments
21
21
 
22
+ logger = logging.getLogger(__name__)
23
+
24
+
22
25
  type_map = {
23
26
  "str": "string",
24
27
  "int": "number",
@@ -120,7 +123,7 @@ class TypescriptGenerator(OOCodeGenerator):
120
123
  gen_type_utils: bool = False
121
124
  include_induced_slots: bool = False
122
125
 
123
- def serialize(self) -> str:
126
+ def serialize(self, output=None) -> str:
124
127
  """Serialize a schema to typescript string"""
125
128
 
126
129
  sv: SchemaView = self.schemaview
@@ -132,6 +135,9 @@ class TypescriptGenerator(OOCodeGenerator):
132
135
  view=self.schemaview,
133
136
  enums=enums,
134
137
  )
138
+ if output is not None:
139
+ with open(output, "w") as out:
140
+ out.write(out_str)
135
141
  return out_str
136
142
 
137
143
  @staticmethod
@@ -207,7 +213,7 @@ class TypescriptGenerator(OOCodeGenerator):
207
213
  elif t.typeof and t.typeof in type_map:
208
214
  tsrange = type_map[t.typeof]
209
215
  else:
210
- logging.warning(f"Unknown type.base: {t.name}")
216
+ logger.warning(f"Unknown type.base: {t.name}")
211
217
  if slot.multivalued:
212
218
  tsrange = f"{tsrange}[]"
213
219
  return tsrange
@@ -241,7 +247,7 @@ class TypescriptGenerator(OOCodeGenerator):
241
247
  elif t.typeof and t.typeof in type_map:
242
248
  return type_init_map[t.typeof]
243
249
  else:
244
- logging.warning(f"Unknown type.base: {t.name}")
250
+ logger.warning(f"Unknown type.base: {t.name}")
245
251
  return "null"
246
252
  return "null"
247
253
 
@@ -264,8 +270,9 @@ class TypescriptGenerator(OOCodeGenerator):
264
270
  @click.version_option(__version__, "-V", "--version")
265
271
  @click.option("--gen-type-utils/", "-u", help="Generate Type checking utils", is_flag=True)
266
272
  @click.option("--include-induced-slots/", help="Generate slots induced through inheritance", is_flag=True)
267
- @click.command(name="typescript")
268
- def cli(yamlfile, gen_type_utils=False, include_induced_slots=False, **args):
273
+ @click.option("--output", type=click.Path(dir_okay=False))
274
+ @click.command()
275
+ def cli(yamlfile, gen_type_utils=False, include_induced_slots=False, output=None, **args):
269
276
  """Generate typescript interfaces and types
270
277
 
271
278
  See https://github.com/linkml/linkml-runtime.js
@@ -273,7 +280,7 @@ def cli(yamlfile, gen_type_utils=False, include_induced_slots=False, **args):
273
280
  gen = TypescriptGenerator(
274
281
  yamlfile, gen_type_utils=gen_type_utils, include_induced_slots=include_induced_slots, **args
275
282
  )
276
- print(gen.serialize())
283
+ gen.serialize(output=output)
277
284
 
278
285
 
279
286
  if __name__ == "__main__":
linkml/linter/linter.py CHANGED
@@ -8,6 +8,7 @@ from typing import Any, Dict, Iterable, Union
8
8
  import jsonschema
9
9
  import yaml
10
10
  from jsonschema.exceptions import best_match
11
+ from jsonschema.protocols import Validator
11
12
  from linkml_runtime import SchemaView
12
13
  from linkml_runtime.dumpers import yaml_dumper
13
14
  from linkml_runtime.linkml_model import SchemaDefinition
@@ -35,7 +36,7 @@ def get_named_config(name: str) -> Dict[str, Any]:
35
36
 
36
37
 
37
38
  @lru_cache
38
- def get_metamodel_validator() -> jsonschema.Validator:
39
+ def get_metamodel_validator() -> Validator:
39
40
  meta_json_gen = JsonSchemaGenerator(LOCAL_METAMODEL_YAML_FILE, not_closed=False)
40
41
  meta_json_schema = meta_json_gen.generate()
41
42
  validator_cls = jsonschema.validators.validator_for(meta_json_schema, default=jsonschema.Draft7Validator)
@@ -360,13 +360,13 @@ class LogicalModelTransformer(ModelTransformer):
360
360
  target_schema = target_schemaview.schema
361
361
  for tn, typ in target_schema.types.items():
362
362
  ancs = sv.type_ancestors(tn, reflexive=False)
363
- logging.debug(f"Unrolling type {tn}, merging {len(ancs)}")
363
+ logger.debug(f"Unrolling type {tn}, merging {len(ancs)}")
364
364
  if ancs:
365
365
  for type_anc in ancs:
366
366
  self._merge_type_ancestors(target=typ, source=sv.get_type(type_anc))
367
367
  for sn, slot in target_schema.slots.items():
368
368
  ancs = sv.slot_ancestors(sn, reflexive=False)
369
- logging.debug(f"Unrolling slot {sn}, merging {len(ancs)}")
369
+ logger.debug(f"Unrolling slot {sn}, merging {len(ancs)}")
370
370
  if ancs:
371
371
  for slot_anc in ancs:
372
372
  self._merge_slot_ancestors(target=slot, source=target_schema.slots[slot_anc])
@@ -379,7 +379,7 @@ class LogicalModelTransformer(ModelTransformer):
379
379
  depth_first=False,
380
380
  )
381
381
  ancs = list(reversed(ancs))
382
- logging.debug(f"Unrolling class {cn}, merging {len(ancs)}")
382
+ logger.debug(f"Unrolling class {cn}, merging {len(ancs)}")
383
383
  self._roll_down(target_schema, cn, ancs)
384
384
  self.apply_defaults(target_schema)
385
385
  if simplify:
@@ -12,9 +12,12 @@ from linkml_runtime.linkml_model import (
12
12
  SchemaDefinition,
13
13
  SlotDefinition,
14
14
  )
15
+ from linkml_runtime.linkml_model.meta import UniqueKey
15
16
  from linkml_runtime.utils.schemaview import SchemaView, SlotDefinitionName
16
17
  from sqlalchemy import Enum
17
18
 
19
+ logger = logging.getLogger(__name__)
20
+
18
21
 
19
22
  class RelationalAnnotations(Enum):
20
23
  PRIMARY_KEY = "primary_key"
@@ -215,11 +218,11 @@ class RelationalModelTransformer:
215
218
  for cn in target_sv.all_classes():
216
219
  pk = self.get_direct_identifier_attribute(target_sv, cn)
217
220
  if self.foreign_key_policy == ForeignKeyPolicy.NO_FOREIGN_KEYS:
218
- logging.info(f"Will not inject any PKs, and policy == {self.foreign_key_policy}")
221
+ logger.info(f"Will not inject any PKs, and policy == {self.foreign_key_policy}")
219
222
  else:
220
223
  if pk is None:
221
224
  pk = self.add_primary_key(cn, target_sv)
222
- logging.info(f"Added primary key {cn}.{pk.name}")
225
+ logger.info(f"Added primary key {cn}.{pk.name}")
223
226
  for link in links:
224
227
  if link.target_class == cn:
225
228
  link.target_slot = pk.name
@@ -233,7 +236,7 @@ class RelationalModelTransformer:
233
236
  continue
234
237
  pk_slot = self.get_direct_identifier_attribute(target_sv, cn)
235
238
  # if self.is_skip(c) and len(incoming_links) == 0:
236
- # logging.info(f'Skipping class: {c.name}')
239
+ # logger.info(f'Skipping class: {c.name}')
237
240
  # del target.classes[cn]
238
241
  # continue
239
242
  for src_slot in list(c.attributes.values()):
@@ -278,6 +281,17 @@ class RelationalModelTransformer:
278
281
  target_slot=backref_slot.name,
279
282
  )
280
283
  )
284
+ backref_key_slots = [s for s in backref_class.attributes.values() if s.key]
285
+ if backref_key_slots:
286
+ if len(backref_key_slots) > 1:
287
+ raise ValueError(f"Multiple keys for {c.name}: {backref_key_slots}")
288
+ backref_key_slot = backref_key_slots[0]
289
+ unique_key_name = f"{c.name}_{backref_key_slot.name}"
290
+ backref_class.unique_keys[unique_key_name] = UniqueKey(
291
+ unique_key_name=unique_key_name,
292
+ unique_key_slots=[backref_slot.name, backref_key_slot.name],
293
+ )
294
+
281
295
  else:
282
296
  # MANY-TO-MANY
283
297
  # create new linking table
@@ -375,7 +389,7 @@ class RelationalModelTransformer:
375
389
  removed_ucs = []
376
390
  for uc_name, uc in c.unique_keys.items():
377
391
  if any(sn in multivalued_slots_original for sn in uc.unique_key_slots):
378
- logging.warning(
392
+ logger.warning(
379
393
  f"Cannot represent uniqueness constraint {uc_name}. "
380
394
  f"one of the slots {uc.unique_key_slots} is multivalued"
381
395
  )
linkml/utils/converter.py CHANGED
@@ -23,6 +23,8 @@ from linkml.utils.datautils import (
23
23
  infer_root_class,
24
24
  )
25
25
 
26
+ logger = logging.getLogger(__name__)
27
+
26
28
 
27
29
  @click.command(name="convert")
28
30
  @click.option("--module", "-m", help="Path to python datamodel module")
@@ -119,7 +121,7 @@ def cli(
119
121
  sv.set_modified()
120
122
  if target_class is None and target_class_from_path:
121
123
  target_class = os.path.basename(input).split("-")[0]
122
- logging.info(f"inferred target class = {target_class} from {input}")
124
+ logger.info(f"inferred target class = {target_class} from {input}")
123
125
  if target_class is None:
124
126
  target_class = infer_root_class(sv)
125
127
  if target_class is None:
@@ -0,0 +1,11 @@
1
+ """
2
+ Custom exceptions
3
+ """
4
+
5
+
6
+ class SchemaError(ValueError):
7
+ """Base class for errors relating to schema specification, parsing, structure, etc."""
8
+
9
+
10
+ class ValidationError(SchemaError):
11
+ """Schema is invalid!"""