linkml 1.8.0rc2__py3-none-any.whl → 1.8.2__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 (70) hide show
  1. linkml/cli/__init__.py +0 -0
  2. linkml/cli/__main__.py +4 -0
  3. linkml/cli/main.py +126 -0
  4. linkml/generators/common/build.py +105 -0
  5. linkml/generators/common/lifecycle.py +124 -0
  6. linkml/generators/common/template.py +89 -0
  7. linkml/generators/csvgen.py +1 -1
  8. linkml/generators/docgen/slot.md.jinja2 +4 -0
  9. linkml/generators/docgen.py +1 -1
  10. linkml/generators/dotgen.py +1 -1
  11. linkml/generators/erdiagramgen.py +1 -1
  12. linkml/generators/excelgen.py +1 -1
  13. linkml/generators/golanggen.py +1 -1
  14. linkml/generators/golrgen.py +1 -1
  15. linkml/generators/graphqlgen.py +1 -1
  16. linkml/generators/javagen.py +1 -1
  17. linkml/generators/jsonldcontextgen.py +4 -4
  18. linkml/generators/jsonldgen.py +1 -1
  19. linkml/generators/jsonschemagen.py +80 -21
  20. linkml/generators/linkmlgen.py +1 -1
  21. linkml/generators/markdowngen.py +1 -1
  22. linkml/generators/namespacegen.py +1 -1
  23. linkml/generators/oocodegen.py +2 -1
  24. linkml/generators/owlgen.py +1 -1
  25. linkml/generators/plantumlgen.py +24 -7
  26. linkml/generators/prefixmapgen.py +1 -1
  27. linkml/generators/projectgen.py +1 -1
  28. linkml/generators/protogen.py +1 -1
  29. linkml/generators/pydanticgen/__init__.py +9 -3
  30. linkml/generators/pydanticgen/array.py +114 -194
  31. linkml/generators/pydanticgen/build.py +64 -25
  32. linkml/generators/pydanticgen/includes.py +4 -28
  33. linkml/generators/pydanticgen/pydanticgen.py +635 -283
  34. linkml/generators/pydanticgen/template.py +153 -180
  35. linkml/generators/pydanticgen/templates/attribute.py.jinja +9 -7
  36. linkml/generators/pydanticgen/templates/base_model.py.jinja +0 -13
  37. linkml/generators/pydanticgen/templates/class.py.jinja +2 -2
  38. linkml/generators/pydanticgen/templates/footer.py.jinja +2 -10
  39. linkml/generators/pydanticgen/templates/module.py.jinja +3 -3
  40. linkml/generators/pydanticgen/templates/validator.py.jinja +0 -4
  41. linkml/generators/pythongen.py +12 -2
  42. linkml/generators/rdfgen.py +1 -1
  43. linkml/generators/shaclgen.py +6 -2
  44. linkml/generators/shexgen.py +1 -1
  45. linkml/generators/sparqlgen.py +1 -1
  46. linkml/generators/sqlalchemygen.py +1 -1
  47. linkml/generators/sqltablegen.py +1 -1
  48. linkml/generators/sssomgen.py +1 -1
  49. linkml/generators/summarygen.py +1 -1
  50. linkml/generators/terminusdbgen.py +7 -4
  51. linkml/generators/typescriptgen.py +1 -1
  52. linkml/generators/yamlgen.py +1 -1
  53. linkml/generators/yumlgen.py +1 -1
  54. linkml/linter/cli.py +1 -1
  55. linkml/transformers/logical_model_transformer.py +117 -18
  56. linkml/utils/converter.py +1 -1
  57. linkml/utils/execute_tutorial.py +2 -0
  58. linkml/utils/logictools.py +142 -29
  59. linkml/utils/schema_builder.py +7 -6
  60. linkml/utils/schema_fixer.py +1 -1
  61. linkml/utils/sqlutils.py +1 -1
  62. linkml/validator/cli.py +4 -1
  63. linkml/validators/jsonschemavalidator.py +1 -1
  64. linkml/validators/sparqlvalidator.py +1 -1
  65. linkml/workspaces/example_runner.py +1 -1
  66. {linkml-1.8.0rc2.dist-info → linkml-1.8.2.dist-info}/METADATA +2 -2
  67. {linkml-1.8.0rc2.dist-info → linkml-1.8.2.dist-info}/RECORD +70 -64
  68. {linkml-1.8.0rc2.dist-info → linkml-1.8.2.dist-info}/entry_points.txt +1 -1
  69. {linkml-1.8.0rc2.dist-info → linkml-1.8.2.dist-info}/LICENSE +0 -0
  70. {linkml-1.8.0rc2.dist-info → linkml-1.8.2.dist-info}/WHEEL +0 -0
@@ -126,7 +126,13 @@ class JsonSchema(dict):
126
126
 
127
127
  @property
128
128
  def is_array(self):
129
- return self.get("type") == "array"
129
+ typ = self.get("type", False)
130
+ if isinstance(typ, str):
131
+ return typ == "array"
132
+ elif isinstance(typ, list):
133
+ return "array" in typ
134
+ else:
135
+ return False
130
136
 
131
137
  @property
132
138
  def is_object(self):
@@ -136,7 +142,7 @@ class JsonSchema(dict):
136
142
  return json.dumps(self, **kwargs)
137
143
 
138
144
  @classmethod
139
- def ref_for(cls, class_name: Union[str, List[str]], identifier_optional: bool = False):
145
+ def ref_for(cls, class_name: Union[str, List[str]], identifier_optional: bool = False, required: bool = True):
140
146
  def _ref(class_name):
141
147
  def_name = camelcase(class_name)
142
148
  def_suffix = cls.OPTIONAL_IDENTIFIER_SUFFIX if identifier_optional else ""
@@ -144,15 +150,27 @@ class JsonSchema(dict):
144
150
 
145
151
  if isinstance(class_name, list):
146
152
  if len(class_name) == 1:
147
- return _ref(class_name[0])
153
+ ref = _ref(class_name[0])
148
154
  else:
149
- return JsonSchema({"anyOf": [_ref(name) for name in class_name]})
155
+ ref = JsonSchema({"anyOf": [_ref(name) for name in class_name]})
150
156
  else:
151
- return _ref(class_name)
157
+ ref = _ref(class_name)
158
+
159
+ if not required:
160
+ if "anyOf" in ref:
161
+ ref["anyOf"].append({"type": "null"})
162
+ else:
163
+ ref = JsonSchema({"anyOf": [ref, {"type": "null"}]})
164
+ return ref
152
165
 
153
166
  @classmethod
154
- def array_of(cls, subschema: "JsonSchema") -> "JsonSchema":
155
- schema = {"type": "array", "items": subschema}
167
+ def array_of(cls, subschema: "JsonSchema", required: bool = True) -> "JsonSchema":
168
+ if required:
169
+ typ = "array"
170
+ else:
171
+ typ = ["array", "null"]
172
+
173
+ schema = {"type": typ, "items": subschema}
156
174
 
157
175
  return JsonSchema(schema)
158
176
 
@@ -199,6 +217,9 @@ class JsonSchemaGenerator(Generator):
199
217
 
200
218
  top_level_schema: JsonSchema = None
201
219
 
220
+ include_null: bool = True
221
+ """Whether to include a "null" type in optional slots"""
222
+
202
223
  def __post_init__(self):
203
224
  if self.topClass:
204
225
  logging.warning("topClass is deprecated - use top_class")
@@ -315,7 +336,7 @@ class JsonSchemaGenerator(Generator):
315
336
 
316
337
  subschema = JsonSchema()
317
338
  for slot in cls.slot_conditions.values():
318
- prop = self.get_subschema_for_slot(slot, omit_type=True)
339
+ prop = self.get_subschema_for_slot(slot, omit_type=True, include_null=False)
319
340
  value_required = False
320
341
  value_disallowed = False
321
342
  if slot.value_presence:
@@ -433,12 +454,31 @@ class JsonSchemaGenerator(Generator):
433
454
  constraints.add_keyword("maximum", slot.maximum_value)
434
455
  constraints.add_keyword("const", slot.equals_string)
435
456
  constraints.add_keyword("const", slot.equals_number)
457
+ if slot.equals_string_in:
458
+ constraints.add_keyword("enum", slot.equals_string_in)
436
459
  return constraints
437
460
 
438
- def get_subschema_for_slot(self, slot: SlotDefinition, omit_type: bool = False) -> JsonSchema:
461
+ def get_subschema_for_slot(
462
+ self, slot: Union[SlotDefinition, AnonymousSlotExpression], omit_type: bool = False, include_null: bool = True
463
+ ) -> JsonSchema:
464
+ """
465
+ Args:
466
+ include_null: Include ``type: null`` when generating ranges that are not required
467
+ """
439
468
  prop = JsonSchema()
469
+ if isinstance(slot, SlotDefinition) and slot.array:
470
+ # TODO: this is currently too lax, in that it will validate ANY array.
471
+ # see https://github.com/linkml/linkml/issues/2188
472
+ prop = JsonSchema(
473
+ {
474
+ "type": ["null", "boolean", "object", "number", "string", "array"],
475
+ "additionalProperties": True,
476
+ }
477
+ )
478
+ return JsonSchema.array_of(prop, required=slot.required)
440
479
  slot_is_multivalued = "multivalued" in slot and slot.multivalued
441
480
  slot_is_inlined = self.schemaview.is_inlined(slot)
481
+ slot_is_boolean = any([slot.any_of, slot.all_of, slot.exactly_one_of, slot.none_of])
442
482
  if not omit_type:
443
483
  typ, fmt, reference = self.get_type_info_for_slot_subschema(slot)
444
484
  if slot_is_inlined:
@@ -459,7 +499,9 @@ class JsonSchemaGenerator(Generator):
459
499
  # If the range can be collected as a simple dict, then we can also accept the value
460
500
  # of that simple dict directly.
461
501
  if range_simple_dict_value_slot is not None:
462
- additionalProps.append(self.get_subschema_for_slot(range_simple_dict_value_slot))
502
+ additionalProps.append(
503
+ self.get_subschema_for_slot(range_simple_dict_value_slot, include_null=False)
504
+ )
463
505
 
464
506
  # If the range has no required slots, then null is acceptable
465
507
  if len(range_required_slots) == 0:
@@ -471,12 +513,17 @@ class JsonSchemaGenerator(Generator):
471
513
  additionalProps = additionalProps[0]
472
514
  else:
473
515
  additionalProps = JsonSchema({"anyOf": additionalProps})
474
- prop = JsonSchema({"type": "object", "additionalProperties": additionalProps})
516
+ if slot.required or not include_null:
517
+ typ = "object"
518
+ else:
519
+ typ = ["object", "null"]
520
+ prop = JsonSchema({"type": typ, "additionalProperties": additionalProps})
475
521
  self.top_level_schema.add_lax_def(reference, self.aliased_slot_name(range_id_slot))
476
522
  else:
477
- prop = JsonSchema.array_of(JsonSchema.ref_for(reference))
523
+ prop = JsonSchema.array_of(JsonSchema.ref_for(reference), required=slot.required)
478
524
  else:
479
- prop = JsonSchema.ref_for(reference)
525
+ prop = JsonSchema.ref_for(reference, required=slot.required or not include_null)
526
+
480
527
  else:
481
528
  if reference is not None:
482
529
  prop = JsonSchema.ref_for(reference)
@@ -486,7 +533,12 @@ class JsonSchemaGenerator(Generator):
486
533
  prop = JsonSchema({"type": typ, "format": fmt})
487
534
 
488
535
  if slot_is_multivalued:
489
- prop = JsonSchema.array_of(prop)
536
+ prop = JsonSchema.array_of(prop, required=slot.required)
537
+ else:
538
+ # handle optionals - bools like any_of, etc. below as they call this method recursively
539
+ if not slot.required and not slot_is_boolean and include_null:
540
+ if "type" in prop:
541
+ prop["type"] = [prop["type"], "null"]
490
542
 
491
543
  prop.add_keyword("description", slot.description)
492
544
  if self.title_from == "title" and slot.title:
@@ -512,22 +564,29 @@ class JsonSchemaGenerator(Generator):
512
564
 
513
565
  bool_subschema = JsonSchema()
514
566
  if slot.any_of is not None and len(slot.any_of) > 0:
515
- bool_subschema["anyOf"] = [self.get_subschema_for_slot(s) for s in slot.any_of]
567
+ bool_subschema["anyOf"] = [self.get_subschema_for_slot(s, include_null=False) for s in slot.any_of]
568
+ if not slot.required and not prop.is_array and include_null:
569
+ bool_subschema["anyOf"].append({"type": "null"})
516
570
 
517
571
  if slot.all_of is not None and len(slot.all_of) > 0:
518
- bool_subschema["allOf"] = [self.get_subschema_for_slot(s) for s in slot.all_of]
572
+ bool_subschema["allOf"] = [self.get_subschema_for_slot(s, include_null=False) for s in slot.all_of]
519
573
 
520
574
  if slot.exactly_one_of is not None and len(slot.exactly_one_of) > 0:
521
- bool_subschema["oneOf"] = [self.get_subschema_for_slot(s) for s in slot.exactly_one_of]
575
+ bool_subschema["oneOf"] = [self.get_subschema_for_slot(s, include_null=False) for s in slot.exactly_one_of]
522
576
 
523
577
  if slot.none_of is not None and len(slot.none_of) > 0:
524
- bool_subschema["not"] = {"anyOf": [self.get_subschema_for_slot(s) for s in slot.none_of]}
578
+ bool_subschema["not"] = {
579
+ "anyOf": [self.get_subschema_for_slot(s, include_null=False) for s in slot.none_of]
580
+ }
525
581
 
526
582
  if bool_subschema:
527
583
  if prop.is_array:
528
584
  if "items" not in prop:
529
585
  prop["items"] = {}
530
- prop["type"] = "array"
586
+ if slot.required or not include_null:
587
+ prop["type"] = "array"
588
+ else:
589
+ prop["type"] = ["array", "null"]
531
590
  prop["items"].update(bool_subschema)
532
591
  else:
533
592
  prop.update(bool_subschema)
@@ -542,7 +601,7 @@ class JsonSchemaGenerator(Generator):
542
601
  value_disallowed = slot.value_presence == PresenceEnum(PresenceEnum.ABSENT)
543
602
 
544
603
  aliased_slot_name = self.aliased_slot_name(slot)
545
- prop = self.get_subschema_for_slot(slot)
604
+ prop = self.get_subschema_for_slot(slot, include_null=self.include_null)
546
605
  subschema.add_property(
547
606
  aliased_slot_name, prop, value_required=value_required, value_disallowed=value_disallowed
548
607
  )
@@ -613,7 +672,7 @@ class JsonSchemaGenerator(Generator):
613
672
 
614
673
 
615
674
  @shared_arguments(JsonSchemaGenerator)
616
- @click.command()
675
+ @click.command(name="json-schema")
617
676
  @click.option(
618
677
  "-i",
619
678
  "--inline",
@@ -97,7 +97,7 @@ class LinkmlGenerator(Generator):
97
97
  help="Name of JSON or YAML file to be created",
98
98
  )
99
99
  @click.version_option(__version__, "-V", "--version")
100
- @click.command()
100
+ @click.command(name="linkml")
101
101
  def cli(
102
102
  yamlfile,
103
103
  materialize_attributes: bool,
@@ -789,7 +789,7 @@ def pad_heading(text: str) -> str:
789
789
 
790
790
 
791
791
  @shared_arguments(MarkdownGenerator)
792
- @click.command()
792
+ @click.command(name="markdown")
793
793
  @click.option("--dir", "-d", required=True, help="Output directory")
794
794
  @click.option("--classes", "-c", multiple=True, help="Class(es) to emit")
795
795
  @click.option("--map-fields", "-M", multiple=True, help="Map metamodel fields, e.g. slot=field")
@@ -195,7 +195,7 @@ def curie(identifier) -> str:
195
195
 
196
196
  @shared_arguments(NamespaceGenerator)
197
197
  @click.version_option(__version__, "-V", "--version")
198
- @click.command()
198
+ @click.command(name="namespaces")
199
199
  def cli(yamlfile, **args):
200
200
  """Generate a namespace manager for all of the prefixes represented in a LinkML model"""
201
201
  print(NamespaceGenerator(yamlfile, **args).serialize(**args))
@@ -76,6 +76,7 @@ class OOCodeGenerator(Generator):
76
76
  visit_all_class_slots = False
77
77
  uses_schemaloader = False
78
78
  requires_metamodel = False
79
+ schemaview: SchemaView = None
79
80
 
80
81
  template_file: str = None
81
82
  """Path to template"""
@@ -84,7 +85,7 @@ class OOCodeGenerator(Generator):
84
85
 
85
86
  def __post_init__(self):
86
87
  # TODO: consider moving up a level
87
- self.schemaview = SchemaView(self.schema)
88
+ self.schemaview: SchemaView = SchemaView(self.schema)
88
89
  super().__post_init__()
89
90
 
90
91
  @abc.abstractmethod
@@ -1271,7 +1271,7 @@ class OwlSchemaGenerator(Generator):
1271
1271
 
1272
1272
 
1273
1273
  @shared_arguments(OwlSchemaGenerator)
1274
- @click.command()
1274
+ @click.command(name="owl")
1275
1275
  @click.option("-o", "--output", help="Output file name")
1276
1276
  @click.option(
1277
1277
  "--metadata-profile",
@@ -12,7 +12,11 @@ from typing import Callable, List, Optional, Set, cast
12
12
 
13
13
  import click
14
14
  import requests
15
- from linkml_runtime.linkml_model.meta import ClassDefinition, ClassDefinitionName, SlotDefinition
15
+ from linkml_runtime.linkml_model.meta import (
16
+ ClassDefinition,
17
+ ClassDefinitionName,
18
+ SlotDefinition,
19
+ )
16
20
  from linkml_runtime.utils.formatutils import camelcase, underscore
17
21
 
18
22
  from linkml import REQUESTS_TIMEOUT
@@ -46,6 +50,7 @@ class PlantumlGenerator(Generator):
46
50
  directory: Optional[str] = None
47
51
  kroki_server: Optional[str] = "https://kroki.io"
48
52
  load_image: bool = True
53
+ tooltips_flag: bool = False
49
54
 
50
55
  def visit_schema(
51
56
  self,
@@ -132,18 +137,28 @@ class PlantumlGenerator(Generator):
132
137
  " {field} "
133
138
  + underscore(self.aliased_slot_name(slot))
134
139
  + mod
135
- + ": "
140
+ + " : "
136
141
  + underscore(slot.range)
142
+ + " "
137
143
  + self.cardinality(slot)
138
144
  )
139
145
  self.class_generated.add(cn)
140
146
  self.referenced.add(cn)
141
147
  cls = self.schema.classes[cn]
148
+
149
+ tooltip_contents = str(cls.description)
150
+ first_newline_index = tooltip_contents.find("\n")
151
+ tooltip_contents = tooltip_contents if first_newline_index < 0 else tooltip_contents[0:first_newline_index]
152
+
153
+ if self.format == "svg" and len(tooltip_contents) > 200:
154
+ tooltip_contents = tooltip_contents[0:197] + " ... "
155
+
156
+ tooltip = " [[{" + tooltip_contents + "}]] "
142
157
  if cls.abstract:
143
158
  class_type = "abstract"
144
159
  else:
145
160
  class_type = "class"
146
- return class_type + ' "' + cn + ('" {\n' + "\n".join(slot_defs) + "\n}" if slot_defs else '"')
161
+ return class_type + ' "' + cn + '"' + tooltip + ("{\n" + "\n".join(slot_defs) + "\n}")
147
162
 
148
163
  def class_associations(self, cn: ClassDefinitionName, must_render: bool = False) -> str:
149
164
  """Emit all associations for a focus class. If none are specified, all classes are generated
@@ -217,7 +232,7 @@ class PlantumlGenerator(Generator):
217
232
  classes.append(self.add_class(cn))
218
233
  if mixin not in self.class_generated:
219
234
  classes.append(self.add_class(mixin))
220
- assocs.append(cn + plantuml_uses[0] + mixin + plantuml_uses[1])
235
+ assocs.append('"' + cn + '" ' + plantuml_uses[0] + ' "' + mixin + '" ' + plantuml_uses[1])
221
236
 
222
237
  # Classes that use the class as a mixin
223
238
  if cls.name in self.synopsis.mixinrefs:
@@ -226,7 +241,9 @@ class PlantumlGenerator(Generator):
226
241
  classes.append(self.add_class(ClassDefinitionName(mixin)))
227
242
  if cn not in self.class_generated:
228
243
  classes.append(self.add_class(cn))
229
- assocs.append(ClassDefinitionName(mixin) + plantuml_uses[0] + cn + plantuml_uses[1])
244
+ assocs.append(
245
+ '"' + ClassDefinitionName(mixin) + '" ' + plantuml_uses[0] + ' "' + cn + '" ' + plantuml_uses[1]
246
+ )
230
247
 
231
248
  # Classes that inject information
232
249
  if cn in self.synopsis.applytos.classrefs:
@@ -265,7 +282,7 @@ class PlantumlGenerator(Generator):
265
282
  if slot.multivalued:
266
283
  return " [1..*]" if slot.required else " [0..*]"
267
284
  else:
268
- return " [req]" if slot.required else " [opt]"
285
+ return " "
269
286
  else:
270
287
  if slot.multivalued:
271
288
  return '"1..*" ' if slot.required else '"0..*" '
@@ -322,7 +339,7 @@ class PlantumlGenerator(Generator):
322
339
 
323
340
 
324
341
  @shared_arguments(PlantumlGenerator)
325
- @click.command()
342
+ @click.command(name="plantuml")
326
343
  @click.option("--classes", "-c", multiple=True, help="Class(es) to emit")
327
344
  @click.option(
328
345
  "--directory",
@@ -112,7 +112,7 @@ class PrefixGenerator(Generator):
112
112
 
113
113
 
114
114
  @shared_arguments(PrefixGenerator)
115
- @click.command()
115
+ @click.command(name="prefix-map")
116
116
  @click.option("--base", help="Base URI for model")
117
117
  @click.option("--output", "-o", help="Output file path")
118
118
  @click.version_option(__version__, "-V", "--version")
@@ -159,7 +159,7 @@ class ProjectGenerator:
159
159
  gen.serialize(**serialize_args)
160
160
 
161
161
 
162
- @click.command()
162
+ @click.command(name="project")
163
163
  @click.option(
164
164
  "--dir",
165
165
  "-d",
@@ -69,7 +69,7 @@ class ProtoGenerator(Generator):
69
69
 
70
70
  @shared_arguments(ProtoGenerator)
71
71
  @click.version_option(__version__, "-V", "--version")
72
- @click.command()
72
+ @click.command(name="proto")
73
73
  def cli(yamlfile, **args):
74
74
  """Generate proto representation of LinkML model"""
75
75
  print(ProtoGenerator(yamlfile, **args).serialize(**args))
@@ -1,4 +1,9 @@
1
- from linkml.generators.pydanticgen.pydanticgen import DEFAULT_IMPORTS, PydanticGenerator, cli
1
+ from linkml.generators.pydanticgen.pydanticgen import (
2
+ DEFAULT_IMPORTS,
3
+ MetadataMode,
4
+ PydanticGenerator,
5
+ cli,
6
+ )
2
7
  from linkml.generators.pydanticgen.template import (
3
8
  ConditionalImport,
4
9
  Import,
@@ -8,8 +13,8 @@ from linkml.generators.pydanticgen.template import (
8
13
  PydanticClass,
9
14
  PydanticEnum,
10
15
  PydanticModule,
16
+ PydanticTemplateModel,
11
17
  PydanticValidator,
12
- TemplateModel,
13
18
  )
14
19
 
15
20
  __all__ = [
@@ -18,6 +23,7 @@ __all__ = [
18
23
  "DEFAULT_IMPORTS",
19
24
  "Import",
20
25
  "Imports",
26
+ "MetadataMode",
21
27
  "PydanticAttribute",
22
28
  "PydanticBaseModel",
23
29
  "PydanticClass",
@@ -25,5 +31,5 @@ __all__ = [
25
31
  "PydanticGenerator",
26
32
  "PydanticModule",
27
33
  "PydanticValidator",
28
- "TemplateModel",
34
+ "PydanticTemplateModel",
29
35
  ]