linkml 1.7.8__py3-none-any.whl → 1.7.10__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.
@@ -3,9 +3,9 @@ Generate CSVs
3
3
  """
4
4
 
5
5
  import os
6
- import sys
7
6
  from csv import DictWriter
8
7
  from dataclasses import dataclass
8
+ from io import StringIO
9
9
  from typing import List, Optional, Set
10
10
 
11
11
  import click
@@ -42,14 +42,20 @@ class CsvGenerator(Generator):
42
42
  writer: Optional[DictWriter] = None
43
43
  """Python dictwriter"""
44
44
 
45
+ _str_io: Optional[StringIO] = None
46
+ """String that the writer outputs to"""
47
+
45
48
  def __post_init__(self):
46
49
  super().__post_init__()
50
+ self._str_io = None
51
+
47
52
  self.generate_header() # TODO: don't do this in initialization
48
53
 
49
- def generate_header(self):
50
- print(f"# metamodel_version: {self.schema.metamodel_version}")
54
+ def generate_header(self) -> str:
55
+ out = f"# metamodel_version: {self.schema.metamodel_version}"
51
56
  if self.schema.version:
52
- print(f"# version: {self.schema.version}")
57
+ out = "\n".join([out, f"# version: {self.schema.version}"])
58
+ return out
53
59
 
54
60
  def visit_schema(self, classes: List[ClassDefinitionName] = None, **_) -> None:
55
61
  # Note: classes comes from the "root" argument
@@ -65,8 +71,9 @@ class CsvGenerator(Generator):
65
71
  else:
66
72
  self.closure.update(self.ancestors(self.schema.classes[clsname]))
67
73
 
74
+ self._str_io = StringIO()
68
75
  dialect: str = "excel" if self.format == "csv" else "excel-tab"
69
- self.writer = DictWriter(sys.stdout, ["id", "mappings", "description"], dialect=dialect)
76
+ self.writer = DictWriter(self._str_io, ["id", "mappings", "description"], dialect=dialect)
70
77
  self.writer.writeheader()
71
78
 
72
79
  def visit_class(self, cls: ClassDefinition) -> bool:
@@ -83,6 +90,9 @@ class CsvGenerator(Generator):
83
90
  return True
84
91
  return False
85
92
 
93
+ def end_schema(self, **kwargs) -> str:
94
+ return self._str_io.getvalue()
95
+
86
96
 
87
97
  @shared_arguments(CsvGenerator)
88
98
  @click.command()
@@ -1,19 +1,29 @@
1
+ {% macro slot_relationship(element, slot) %}
2
+ {% set range_element = gen.name(schemaview.get_element(slot.range)) %}
3
+ {% set relation_label = gen.name(slot) %}
4
+ {{ gen.name(element) }} --> "{{ gen.cardinality(slot) }}" {{ range_element }} : {{ relation_label }}
5
+ click {{ range_element }} href "../{{ range_element }}"
6
+ {% endmacro %}
7
+
1
8
  {% if schemaview.class_parents(element.name) and schemaview.class_children(element.name) %}
2
9
  ```{{ gen.mermaid_directive() }}
3
10
  classDiagram
4
11
  class {{ gen.name(element) }}
12
+ click {{ gen.name(element) }} href "../{{gen.name(element)}}"
5
13
  {% for s in schemaview.class_parents(element.name)|sort(attribute='name') -%}
6
14
  {{ gen.name(schemaview.get_element(s)) }} <|-- {{ gen.name(element) }}
15
+ click {{ gen.name(schemaview.get_element(s)) }} href "../{{gen.name(schemaview.get_element(s))}}"
7
16
  {% endfor %}
8
17
 
9
18
  {% for s in schemaview.class_children(element.name)|sort(attribute='name') -%}
10
19
  {{ gen.name(element) }} <|-- {{ gen.name(schemaview.get_element(s)) }}
20
+ click {{ gen.name(schemaview.get_element(s)) }} href "../{{gen.name(schemaview.get_element(s))}}"
11
21
  {% endfor %}
12
22
 
13
23
  {% for s in schemaview.class_induced_slots(element.name)|sort(attribute='name') -%}
14
24
  {{ gen.name(element) }} : {{gen.name(s)}}
15
25
  {% if s.range not in gen.all_type_object_names() %}
16
- {{ gen.name(element) }} --> {{ s.range }} : {{ gen.name(s) }}
26
+ {{ slot_relationship(element, s) }}
17
27
  {% endif %}
18
28
  {% endfor %}
19
29
  ```
@@ -21,13 +31,15 @@
21
31
  ```{{ gen.mermaid_directive() }}
22
32
  classDiagram
23
33
  class {{ gen.name(element) }}
34
+ click {{ gen.name(element) }} href "../{{gen.name(element)}}"
24
35
  {% for s in schemaview.class_parents(element.name)|sort(attribute='name') -%}
25
36
  {{ gen.name(schemaview.get_element(s)) }} <|-- {{ gen.name(element) }}
37
+ click {{ gen.name(schemaview.get_element(s)) }} href "../{{gen.name(schemaview.get_element(s))}}"
26
38
  {% endfor %}
27
39
  {% for s in schemaview.class_induced_slots(element.name)|sort(attribute='name') -%}
28
40
  {{ gen.name(element) }} : {{gen.name(s)}}
29
41
  {% if s.range not in gen.all_type_object_names() %}
30
- {{ gen.name(element) }} --> {{ s.range }} : {{ gen.name(s) }}
42
+ {{ slot_relationship(element, s) }}
31
43
  {% endif %}
32
44
  {% endfor %}
33
45
  ```
@@ -35,13 +47,15 @@
35
47
  ```{{ gen.mermaid_directive() }}
36
48
  classDiagram
37
49
  class {{ gen.name(element) }}
50
+ click {{ gen.name(element) }} href "../{{gen.name(element)}}"
38
51
  {% for s in schemaview.class_children(element.name)|sort(attribute='name') -%}
39
52
  {{ gen.name(element) }} <|-- {{ gen.name(schemaview.get_element(s)) }}
53
+ click {{ gen.name(schemaview.get_element(s)) }} href "../{{gen.name(schemaview.get_element(s))}}"
40
54
  {% endfor %}
41
55
  {% for s in schemaview.class_induced_slots(element.name)|sort(attribute='name') -%}
42
56
  {{ gen.name(element) }} : {{gen.name(s)}}
43
57
  {% if s.range not in gen.all_type_object_names() %}
44
- {{ gen.name(element) }} --> {{ s.range }} : {{ gen.name(s) }}
58
+ {{ slot_relationship(element, s) }}
45
59
  {% endif %}
46
60
  {% endfor %}
47
61
  ```
@@ -49,10 +63,11 @@
49
63
  ```{{ gen.mermaid_directive() }}
50
64
  classDiagram
51
65
  class {{ gen.name(element) }}
66
+ click {{ gen.name(element) }} href "../{{gen.name(element)}}"
52
67
  {% for s in schemaview.class_induced_slots(element.name)|sort(attribute='name') -%}
53
68
  {{ gen.name(element) }} : {{gen.name(s)}}
54
69
  {% if s.range not in gen.all_type_object_names() %}
55
- {{ gen.name(element) }} --> {{ s.range }} : {{ gen.name(s) }}
70
+ {{ slot_relationship(element, s) }}
56
71
  {% endif %}
57
72
  {% endfor %}
58
73
  ```
@@ -567,23 +567,60 @@ class DocGenerator(Generator):
567
567
  @staticmethod
568
568
  def cardinality(slot: SlotDefinition) -> str:
569
569
  """
570
- Render combination of required, multivalued, and recommended as a range, e.g. 0..*
571
- :param slot:
572
- :return:
570
+ Render combination of required, multivalued, recommended, and exact_cardinality as a range,
571
+ according to Mermaid conventions. Considers 'required' and 'multivalued' to set defaults
572
+ for 'minimum_cardinality' and 'maximum_cardinality'.
573
+
574
+ Reference: https://mermaid.js.org/syntax/classDiagram.html#cardinality-multiplicity-on-relations
575
+
576
+ The different cardinality options are:
577
+ - 1 Only 1
578
+ - 0..1 Zero or One
579
+ - 1..* One or more
580
+ - * Many
581
+ - n n (where n>1)
582
+ - 0..n zero to n (where n>1)
583
+ - 1..n one to n (where n>1)
584
+ :param slot: SlotDefinition
585
+ :return: cardinality string as used in Mermaid diagrams
573
586
  """
574
- if slot.required or slot.identifier:
575
- min = "1"
576
- else:
577
- min = "0"
578
- if slot.multivalued:
579
- max = "*"
587
+ if slot.exact_cardinality is not None:
588
+ cardinality = str(slot.exact_cardinality) # handles 'n' case
580
589
  else:
581
- max = "1"
590
+ if slot.required or slot.identifier:
591
+ min_card = "1"
592
+ else:
593
+ min_card = str(slot.minimum_cardinality) if slot.minimum_cardinality is not None else "0"
594
+
595
+ if slot.multivalued:
596
+ max_card = "*"
597
+ else:
598
+ max_card = str(slot.maximum_cardinality) if slot.maximum_cardinality is not None else "1"
599
+
600
+ if min_card == "0":
601
+ if max_card == "1":
602
+ cardinality = "0..1" # handles '0..1' case
603
+ elif max_card == "*":
604
+ cardinality = "*" # handles '*' case
605
+ else:
606
+ cardinality = f"0..{max_card}" # handles '0..n' case
607
+ elif min_card == "1":
608
+ if max_card == "1":
609
+ cardinality = "1" # handles '1' case
610
+ elif max_card == "*":
611
+ cardinality = "1..*" # handles '1..*' case
612
+ else:
613
+ cardinality = f"1..{max_card}" # handles '1..n' case
614
+ else:
615
+ if max_card == "*":
616
+ cardinality = f"{min_card}..*" # handles 'n..*' case
617
+ else:
618
+ cardinality = f"{min_card}..{max_card}" # handles 'n..m' case
619
+
582
620
  if slot.recommended:
583
- info = " _recommended_"
584
- else:
585
- info = ""
586
- return f"{min}..{max}{info}"
621
+ cardinality += " _recommended_"
622
+
623
+ return cardinality
587
624
 
588
625
  def mermaid_directive(self) -> str:
589
626
  """
@@ -932,6 +969,14 @@ class DocGenerator(Generator):
932
969
  "--example-directory",
933
970
  help="Folder in which example files are found. These are used to make inline examples",
934
971
  )
972
+ @click.option(
973
+ "-d",
974
+ "--include",
975
+ help="""
976
+ Include LinkML Schema outside of imports mechanism. Helpful in including deprecated classes and slots in a separate
977
+ YAML, and including it when necessary but not by default (e.g. in documentation or for backwards compatibility)
978
+ """,
979
+ )
935
980
  @click.version_option(__version__, "-V", "--version")
936
981
  @click.command()
937
982
  def cli(
@@ -19,28 +19,26 @@ class GraphqlGenerator(Generator):
19
19
  uses_schemaloader = True
20
20
  requires_metamodel = False
21
21
 
22
- def __post_init__(self):
23
- super().__post_init__()
24
- # TODO: move this
25
- self.generate_header()
22
+ def visit_schema(self, **kwargs) -> str:
23
+ return self.generate_header()
26
24
 
27
- def generate_header(self):
28
- print(f"# metamodel_version: {self.schema.metamodel_version}")
25
+ def generate_header(self) -> str:
26
+ out = f"# metamodel_version: {self.schema.metamodel_version}\n"
29
27
  if self.schema.version:
30
- print(f"# version: {self.schema.version}")
28
+ out += f"# version: {self.schema.version}\n"
29
+ return out
31
30
 
32
- def visit_class(self, cls: ClassDefinition) -> bool:
31
+ def visit_class(self, cls: ClassDefinition) -> str:
33
32
  etype = "interface" if (cls.abstract or cls.mixin) and not cls.mixins else "type"
34
33
  mixins = ", ".join([camelcase(mixin) for mixin in cls.mixins])
35
- print(f"{etype} {camelcase(cls.name)}" + (f" implements {mixins}" if mixins else ""))
36
- print(" {")
37
- return True
34
+ out = f"{etype} {camelcase(cls.name)}" + (f" implements {mixins}" if mixins else "")
35
+ out = "\n".join([out, " {"])
36
+ return out
38
37
 
39
- def end_class(self, cls: ClassDefinition) -> None:
40
- print(" }")
41
- print()
38
+ def end_class(self, cls: ClassDefinition) -> str:
39
+ return "\n }\n\n"
42
40
 
43
- def visit_class_slot(self, cls: ClassDefinition, aliased_slot_name: str, slot: SlotDefinition) -> None:
41
+ def visit_class_slot(self, cls: ClassDefinition, aliased_slot_name: str, slot: SlotDefinition) -> str:
44
42
  slotrange = (
45
43
  camelcase(slot.range)
46
44
  if slot.range in self.schema.classes or slot.range in self.schema.types or slot.range in self.schema.enums
@@ -50,7 +48,7 @@ class GraphqlGenerator(Generator):
50
48
  slotrange = f"[{slotrange}]"
51
49
  if slot.required:
52
50
  slotrange = slotrange + "!"
53
- print(f" {lcamelcase(aliased_slot_name)}: {slotrange}")
51
+ return f"\n {lcamelcase(aliased_slot_name)}: {slotrange}"
54
52
 
55
53
 
56
54
  @shared_arguments(GraphqlGenerator)
@@ -89,7 +89,7 @@ class ContextGenerator(Generator):
89
89
  flatprefixes: Optional[bool] = False,
90
90
  model: Optional[bool] = True,
91
91
  **_,
92
- ) -> None:
92
+ ) -> str:
93
93
  if model is None:
94
94
  model = self.model
95
95
  context = JsonObj()
@@ -126,8 +126,8 @@ class ContextGenerator(Generator):
126
126
  if output:
127
127
  with open(output, "w", encoding="UTF-8") as outf:
128
128
  outf.write(as_json(context))
129
- else:
130
- print(as_json(context))
129
+
130
+ return str(as_json(context)) + "\n"
131
131
 
132
132
  def visit_class(self, cls: ClassDefinition) -> bool:
133
133
  class_def = {}
@@ -151,7 +151,7 @@ class JSONLDGenerator(Generator):
151
151
  def visit_subset(self, ss: SubsetDefinition) -> None:
152
152
  self._visit(ss)
153
153
 
154
- def end_schema(self, context: str = None, **_) -> None:
154
+ def end_schema(self, context: str = None, **_) -> str:
155
155
  self._add_type(self.schema)
156
156
  base_prefix = self.default_prefix()
157
157
 
@@ -186,8 +186,9 @@ class JSONLDGenerator(Generator):
186
186
  if base_prefix:
187
187
  self.schema["@context"].append({"@base": base_prefix})
188
188
  # json_obj["@id"] = self.schema.id
189
- print(as_json(self.schema, indent=" "))
189
+ out = str(as_json(self.schema, indent=" ")) + "\n"
190
190
  self.schema = self.original_schema
191
+ return out
191
192
 
192
193
 
193
194
  @shared_arguments(JSONLDGenerator)
@@ -185,6 +185,7 @@ class JsonSchemaGenerator(Generator):
185
185
  indent: int = 4
186
186
 
187
187
  inline: bool = False
188
+
188
189
  top_class: Optional[Union[ClassDefinitionName, str]] = None # JSON object is one instance of this
189
190
  """Class instantiated by the root node of the document tree"""
190
191
 
@@ -662,6 +663,14 @@ disable pretty-printing and return the most compact JSON representation
662
663
  Specify from which slot are JSON Schema 'title' annotations generated.
663
664
  """,
664
665
  )
666
+ @click.option(
667
+ "-d",
668
+ "--include",
669
+ help="""
670
+ Include LinkML Schema outside of imports mechanism. Helpful in including deprecated classes and slots in a separate
671
+ YAML, and including it when necessary but not by default (e.g. in documentation or for backwards compatibility)
672
+ """,
673
+ )
665
674
  @click.version_option(__version__, "-V", "--version")
666
675
  def cli(yamlfile, **kwargs):
667
676
  """Generate JSON Schema representation of a LinkML model"""