linkml 1.8.3__py3-none-any.whl → 1.8.5__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 (44) hide show
  1. linkml/generators/__init__.py +2 -0
  2. linkml/generators/common/ifabsent_processor.py +98 -21
  3. linkml/generators/common/naming.py +106 -0
  4. linkml/generators/docgen/index.md.jinja2 +6 -6
  5. linkml/generators/docgen.py +80 -21
  6. linkml/generators/golanggen.py +3 -1
  7. linkml/generators/graphqlgen.py +34 -2
  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 +35 -16
  14. linkml/generators/pydanticgen/template.py +119 -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/python_ifabsent_processor.py +1 -1
  19. linkml/generators/pythongen.py +135 -31
  20. linkml/generators/shaclgen.py +34 -10
  21. linkml/generators/sparqlgen.py +3 -1
  22. linkml/generators/sqlalchemygen.py +5 -3
  23. linkml/generators/sqltablegen.py +4 -2
  24. linkml/generators/typescriptgen.py +13 -6
  25. linkml/linter/linter.py +2 -1
  26. linkml/transformers/logical_model_transformer.py +3 -3
  27. linkml/transformers/relmodel_transformer.py +18 -4
  28. linkml/utils/converter.py +3 -1
  29. linkml/utils/exceptions.py +11 -0
  30. linkml/utils/execute_tutorial.py +22 -20
  31. linkml/utils/generator.py +6 -4
  32. linkml/utils/mergeutils.py +4 -2
  33. linkml/utils/schema_fixer.py +5 -5
  34. linkml/utils/schemaloader.py +5 -3
  35. linkml/utils/sqlutils.py +3 -1
  36. linkml/validator/plugins/pydantic_validation_plugin.py +1 -1
  37. linkml/validators/jsonschemavalidator.py +3 -1
  38. linkml/validators/sparqlvalidator.py +5 -3
  39. linkml/workspaces/example_runner.py +3 -1
  40. {linkml-1.8.3.dist-info → linkml-1.8.5.dist-info}/METADATA +3 -1
  41. {linkml-1.8.3.dist-info → linkml-1.8.5.dist-info}/RECORD +44 -42
  42. {linkml-1.8.3.dist-info → linkml-1.8.5.dist-info}/LICENSE +0 -0
  43. {linkml-1.8.3.dist-info → linkml-1.8.5.dist-info}/WHEEL +0 -0
  44. {linkml-1.8.3.dist-info → linkml-1.8.5.dist-info}/entry_points.txt +0 -0
@@ -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
- print(GraphqlGenerator(yamlfile, **args).serialize(**args))
90
+ generator = GraphqlGenerator(yamlfile, **args)
91
+ print(generator.serialize(**args))
60
92
 
61
93
 
62
94
  if __name__ == "__main__":
@@ -24,6 +24,8 @@ from linkml._version import __version__
24
24
  from linkml.generators.common.type_designators import get_type_designator_value
25
25
  from linkml.utils.generator import Generator, shared_arguments
26
26
 
27
+ logger = logging.getLogger(__name__)
28
+
27
29
  # Map from underlying python data type to json equivalent
28
30
  # Note: The underlying types are a union of any built-in python datatype + any type defined in
29
31
  # linkml-runtime/utils/metamodelcore.py
@@ -222,14 +224,14 @@ class JsonSchemaGenerator(Generator):
222
224
 
223
225
  def __post_init__(self):
224
226
  if self.topClass:
225
- logging.warning("topClass is deprecated - use top_class")
227
+ logger.warning("topClass is deprecated - use top_class")
226
228
  self.top_class = self.topClass
227
229
 
228
230
  super().__post_init__()
229
231
 
230
232
  if self.top_class:
231
233
  if self.schemaview.get_class(self.top_class) is None:
232
- logging.warning(f"No class in schema named {self.top_class}")
234
+ logger.warning(f"No class in schema named {self.top_class}")
233
235
 
234
236
  def start_schema(self, inline: bool = False) -> JsonSchema:
235
237
  self.inline = inline
@@ -40,6 +40,8 @@ from linkml import METAMODEL_NAMESPACE_NAME
40
40
  from linkml._version import __version__
41
41
  from linkml.utils.generator import Generator, shared_arguments
42
42
 
43
+ logger = logging.getLogger(__name__)
44
+
43
45
  OWL_TYPE = URIRef ## RDFS.Literal or OWL.Thing
44
46
 
45
47
  SWRL = rdflib.Namespace("http://www.w3.org/2003/11/swrl#")
@@ -270,7 +272,7 @@ class OwlSchemaGenerator(Generator):
270
272
  # if isinstance(v, str):
271
273
  # obj = URIRef(msv.expand_curie(v))
272
274
  # else:
273
- # logging.debug(f"Skipping {uri} {metaslot_uri} => {v}")
275
+ # logger.debug(f"Skipping {uri} {metaslot_uri} => {v}")
274
276
  else:
275
277
  obj = Literal(v)
276
278
  self.graph.add((uri, metaslot_uri, obj))
@@ -438,7 +440,7 @@ class OwlSchemaGenerator(Generator):
438
440
  if slot:
439
441
  own_slots.append(slot)
440
442
  else:
441
- logging.warning(f"Unknown top-level slot {slot_name}")
443
+ logger.warning(f"Unknown top-level slot {slot_name}")
442
444
  else:
443
445
  own_slots = []
444
446
  own_slots.extend(cls.slot_conditions.values())
@@ -503,9 +505,9 @@ class OwlSchemaGenerator(Generator):
503
505
  owl_exprs.append(self._complement_of_union_of([self.transform_class_expression(x) for x in cls.none_of]))
504
506
  for slot in own_slots:
505
507
  if slot.name:
506
- owltypes = self.slot_node_owltypes(sv.get_slot(slot.name))
508
+ owltypes = self.slot_node_owltypes(sv.get_slot(slot.name), owning_class=cls)
507
509
  else:
508
- owltypes = self.slot_node_owltypes(slot)
510
+ owltypes = self.slot_node_owltypes(slot, owning_class=cls)
509
511
  x = self.transform_class_slot_expression(cls, slot, slot, owltypes)
510
512
  if not x:
511
513
  range = sv.schema.default_range
@@ -552,12 +554,28 @@ class OwlSchemaGenerator(Generator):
552
554
  owl_exprs.append(self._some_values_from(slot_uri, has_member_expr))
553
555
  return self._intersection_of(owl_exprs)
554
556
 
555
- def slot_node_owltypes(self, slot: Union[SlotDefinition, AnonymousSlotExpression]) -> Set[URIRef]:
557
+ def slot_node_owltypes(
558
+ self,
559
+ slot: Union[SlotDefinition, AnonymousSlotExpression],
560
+ owning_class: Optional[Union[ClassDefinition, AnonymousClassExpression]] = None,
561
+ ) -> Set[URIRef]:
562
+ """
563
+ Determine the OWL types of a named slot or slot expression
564
+
565
+ The OWL type is either OWL.Thing or RDFS.Datatype
566
+
567
+ :param slot:
568
+ :param owning_class:
569
+ :return:
570
+ """
556
571
  sv = self.schemaview
557
572
  node_types = set()
558
573
  if isinstance(slot, SlotDefinition):
559
- if slot.range in sv.all_classes():
560
- range_class = sv.get_class(slot.range)
574
+ slot_range = slot.range
575
+ if isinstance(owning_class, ClassDefinition):
576
+ slot_range = sv.induced_slot(slot.name, owning_class.name).range
577
+ if slot_range in sv.all_classes():
578
+ range_class = sv.get_class(slot_range)
561
579
  if not (range_class and range_class.class_uri == "linkml:Any"):
562
580
  node_types.add(OWL.Thing)
563
581
  if slot.range in sv.all_types():
@@ -565,7 +583,7 @@ class OwlSchemaGenerator(Generator):
565
583
  for k in ["any_of", "all_of", "exactly_one_of", "none_of"]:
566
584
  subslot = getattr(slot, k, None)
567
585
  if subslot:
568
- node_types.update(self.slot_node_owltypes(subslot))
586
+ node_types.update(self.slot_node_owltypes(subslot, owning_class=owning_class))
569
587
  return node_types
570
588
 
571
589
  def transform_class_slot_expression(
@@ -679,7 +697,7 @@ class OwlSchemaGenerator(Generator):
679
697
  if element.equals_string is not None:
680
698
  equals_string = element.equals_string
681
699
  if is_literal is None:
682
- logging.warning(f"ignoring equals_string={equals_string} as unable to tell if literal")
700
+ logger.warning(f"ignoring equals_string={equals_string} as unable to tell if literal")
683
701
  elif is_literal:
684
702
  constraints[XSD.pattern] = equals_string
685
703
  else:
@@ -688,7 +706,7 @@ class OwlSchemaGenerator(Generator):
688
706
  if element.equals_string_in:
689
707
  equals_string_in = element.equals_string_in
690
708
  if is_literal is None:
691
- logging.warning(f"ignoring equals_string={equals_string_in} as unable to tell if literal")
709
+ logger.warning(f"ignoring equals_string={equals_string_in} as unable to tell if literal")
692
710
  elif is_literal:
693
711
  dt_exprs = [
694
712
  self._datatype_restriction(XSD.string, [self._facet(XSD.pattern, s)]) for s in equals_string_in
@@ -754,7 +772,7 @@ class OwlSchemaGenerator(Generator):
754
772
  if slot_uri == URIRef(att_uri):
755
773
  n += 1
756
774
  if n > 1:
757
- logging.warning(f"Ambiguous attribute: {slot.name} {slot_uri}")
775
+ logger.warning(f"Ambiguous attribute: {slot.name} {slot_uri}")
758
776
  return
759
777
 
760
778
  self.add_metadata(slot, slot_uri)
@@ -845,6 +863,7 @@ class OwlSchemaGenerator(Generator):
845
863
  if not isinstance(v, list):
846
864
  v = [v]
847
865
  impls.extend(v)
866
+ impls.extend(element.implements)
848
867
  for impl in impls:
849
868
  if impl.startswith("owl:"):
850
869
  return OWL[impl.split(":")[1]]
@@ -889,7 +908,7 @@ class OwlSchemaGenerator(Generator):
889
908
  if pv_owl_type == RDFS.Literal:
890
909
  pv_node = Literal(pv.text)
891
910
  if pv.meaning:
892
- logging.warning(f"Meaning on literal {pv.text} in {e.name} is ignored")
911
+ logger.warning(f"Meaning on literal {pv.text} in {e.name} is ignored")
893
912
  else:
894
913
  pv_node = self._permissible_value_uri(pv, enum_uri, e)
895
914
  pv_uris.append(pv_node)
@@ -950,7 +969,7 @@ class OwlSchemaGenerator(Generator):
950
969
  def _add_rule(self, subject: Union[URIRef, BNode], rule: ClassRule, cls: ClassDefinition):
951
970
  if not self.use_swrl:
952
971
  return
953
- logging.warning("SWRL support is experimental and incomplete")
972
+ logger.warning("SWRL support is experimental and incomplete")
954
973
  head = []
955
974
  body = []
956
975
  for pre in rule.preconditions:
@@ -997,7 +1016,7 @@ class OwlSchemaGenerator(Generator):
997
1016
  owltypes.update(x_owltypes)
998
1017
  owltypes.update(current)
999
1018
  if len(owltypes) > 1:
1000
- logging.warning(f"Multiple owl types {owltypes}")
1019
+ logger.warning(f"Multiple owl types {owltypes}")
1001
1020
  # if self.target_profile == OWLProfile.dl:
1002
1021
  return owltypes
1003
1022
 
@@ -1125,7 +1144,7 @@ class OwlSchemaGenerator(Generator):
1125
1144
  ) -> Optional[Union[BNode, URIRef]]:
1126
1145
  graph = self.graph
1127
1146
  if [x for x in exprs if x is None]:
1128
- logging.warning(f"Null expr in: {exprs} for {predicate} {node}")
1147
+ logger.warning(f"Null expr in: {exprs} for {predicate} {node}")
1129
1148
  exprs = [x for x in exprs if x is not None]
1130
1149
  if len(exprs) == 0:
1131
1150
  return None
@@ -1251,10 +1270,10 @@ class OwlSchemaGenerator(Generator):
1251
1270
  return OWL.ObjectProperty
1252
1271
  is_literal_vals = self.slot_is_literal_map[slot.name]
1253
1272
  if len(is_literal_vals) > 1:
1254
- logging.warning(f"Ambiguous type for: {slot.name}")
1273
+ logger.warning(f"Ambiguous type for: {slot.name}")
1255
1274
  if range is None:
1256
1275
  if not is_literal_vals:
1257
- logging.warning(f"Guessing type for {slot.name}")
1276
+ logger.warning(f"Guessing type for {slot.name}")
1258
1277
  return OWL.ObjectProperty
1259
1278
  if (list(is_literal_vals))[0]:
1260
1279
  return OWL.DatatypeProperty
@@ -26,6 +26,8 @@ from linkml.generators.sqltablegen import SQLTableGenerator
26
26
  from linkml.utils.cli_utils import log_level_option
27
27
  from linkml.utils.generator import Generator
28
28
 
29
+ logger = logging.getLogger(__name__)
30
+
29
31
  PATH_FSTRING = str
30
32
  GENERATOR_NAME = str
31
33
  ARG_DICT = Dict[str, Any]
@@ -63,14 +65,14 @@ GEN_MAP = {
63
65
 
64
66
  @lru_cache()
65
67
  def get_local_imports(schema_path: str, dir: str):
66
- logging.info(f"GETTING IMPORTS = {schema_path}")
68
+ logger.info(f"GETTING IMPORTS = {schema_path}")
67
69
  all_imports = [schema_path]
68
70
  with open(schema_path) as stream:
69
71
  with open(schema_path) as stream:
70
72
  schema = yaml.safe_load(stream)
71
73
  for imp in schema.get("imports", []):
72
74
  imp_path = os.path.join(dir, imp) + ".yaml"
73
- logging.info(f" IMP={imp} // path={imp_path}")
75
+ logger.info(f" IMP={imp} // path={imp_path}")
74
76
  if os.path.isfile(imp_path):
75
77
  all_imports += get_local_imports(imp_path, dir)
76
78
  return all_imports
@@ -105,23 +107,23 @@ class ProjectGenerator:
105
107
  all_schemas = [schema_path]
106
108
  else:
107
109
  all_schemas = get_local_imports(schema_path, os.path.dirname(schema_path))
108
- logging.debug(f"ALL_SCHEMAS = {all_schemas}")
110
+ logger.debug(f"ALL_SCHEMAS = {all_schemas}")
109
111
  for gen_name, (gen_cls, gen_path_fmt, default_gen_args) in GEN_MAP.items():
110
112
  if config.includes is not None and config.includes != [] and gen_name not in config.includes:
111
- logging.info(f"Skipping {gen_name} as not in inclusion list: {config.includes}")
113
+ logger.info(f"Skipping {gen_name} as not in inclusion list: {config.includes}")
112
114
  continue
113
115
  if config.excludes is not None and gen_name in config.excludes:
114
- logging.info(f"Skipping {gen_name} as it is in exclusion list")
116
+ logger.info(f"Skipping {gen_name} as it is in exclusion list")
115
117
  continue
116
- logging.info(f"Generating: {gen_name}")
118
+ logger.info(f"Generating: {gen_name}")
117
119
  for local_path in all_schemas:
118
- logging.info(f" SCHEMA: {local_path}")
120
+ logger.info(f" SCHEMA: {local_path}")
119
121
  name = os.path.basename(local_path).replace(".yaml", "")
120
122
  gen_path = gen_path_fmt.format(name=name)
121
123
  gen_path_full = f"{config.directory}/{gen_path}"
122
124
  parts = gen_path_full.split("/")
123
125
  parent_dir = "/".join(parts[0:-1])
124
- logging.info(f" PARENT={parent_dir}")
126
+ logger.info(f" PARENT={parent_dir}")
125
127
  Path(parent_dir).mkdir(parents=True, exist_ok=True)
126
128
  gen_path_full = "/".join(parts)
127
129
  all_gen_args = {
@@ -143,13 +145,13 @@ class ProjectGenerator:
143
145
  if isinstance(v, str):
144
146
  v = v.format(name=name, parent=parent_dir)
145
147
  serialize_args[k] = v
146
- logging.info(f" {gen_name} ARGS: {serialize_args}")
148
+ logger.info(f" {gen_name} ARGS: {serialize_args}")
147
149
  gen_dump = gen.serialize(**serialize_args)
148
150
 
149
151
  if gen_name != "excel":
150
152
  if parts[-1] != "":
151
153
  # markdowngen does not write to a file
152
- logging.info(f" WRITING TO: {gen_path_full}")
154
+ logger.info(f" WRITING TO: {gen_path_full}")
153
155
  with open(gen_path_full, "w", encoding="UTF-8") as stream:
154
156
  stream.write(gen_dump)
155
157
  else:
@@ -249,7 +251,7 @@ def cli(
249
251
  project_config.generator_args = yaml.safe_load(generator_arguments)
250
252
  except Exception:
251
253
  raise Exception("Argument must be a valid YAML blob")
252
- logging.info(f"generator args: {project_config.generator_args}")
254
+ logger.info(f"generator args: {project_config.generator_args}")
253
255
  if dir is not None:
254
256
  project_config.directory = dir
255
257
  project_config.mergeimports = mergeimports