linkml 1.9.1rc2__py3-none-any.whl → 1.9.2rc1__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.
- linkml/cli/main.py +2 -0
- linkml/generators/__init__.py +2 -0
- linkml/generators/docgen/class_diagram.md.jinja2 +10 -8
- linkml/generators/docgen/common_metadata.md.jinja2 +1 -1
- linkml/generators/docgen/index.md.jinja2 +1 -1
- linkml/generators/docgen.py +33 -7
- linkml/generators/jsonldgen.py +1 -1
- linkml/generators/markdowngen.py +24 -1
- linkml/generators/mermaidclassdiagramgen.py +3 -2
- linkml/generators/panderagen/__init__.py +9 -0
- linkml/generators/panderagen/class_generator_mixin.py +20 -0
- linkml/generators/panderagen/enum_generator_mixin.py +17 -0
- linkml/generators/panderagen/panderagen.py +216 -0
- linkml/generators/panderagen/panderagen_class_based/class.jinja2 +26 -0
- linkml/generators/panderagen/panderagen_class_based/enums.jinja2 +9 -0
- linkml/generators/panderagen/panderagen_class_based/header.jinja2 +21 -0
- linkml/generators/panderagen/panderagen_class_based/mixins.jinja2 +26 -0
- linkml/generators/panderagen/panderagen_class_based/pandera.jinja2 +16 -0
- linkml/generators/panderagen/panderagen_class_based/slots.jinja2 +36 -0
- linkml/generators/panderagen/slot_generator_mixin.py +77 -0
- linkml/generators/pydanticgen/templates/enum.py.jinja +6 -4
- linkml/generators/pydanticgen/templates/validator.py.jinja +7 -6
- linkml/generators/sqltablegen.py +143 -86
- linkml/generators/yumlgen.py +33 -1
- linkml/transformers/rollup_transformer.py +115 -0
- linkml/utils/deprecation.py +26 -0
- linkml/utils/execute_tutorial.py +71 -9
- linkml/validator/plugins/recommended_slots_plugin.py +4 -4
- {linkml-1.9.1rc2.dist-info → linkml-1.9.2rc1.dist-info}/METADATA +19 -16
- {linkml-1.9.1rc2.dist-info → linkml-1.9.2rc1.dist-info}/RECORD +33 -21
- {linkml-1.9.1rc2.dist-info → linkml-1.9.2rc1.dist-info}/WHEEL +1 -1
- {linkml-1.9.1rc2.dist-info → linkml-1.9.2rc1.dist-info}/entry_points.txt +1 -0
- {linkml-1.9.1rc2.dist-info → linkml-1.9.2rc1.dist-info}/LICENSE +0 -0
linkml/cli/main.py
CHANGED
@@ -24,6 +24,7 @@ from linkml.generators.linkmlgen import cli as gen_linkml
|
|
24
24
|
from linkml.generators.markdowngen import cli as gen_markdown
|
25
25
|
from linkml.generators.namespacegen import cli as gen_namespaces
|
26
26
|
from linkml.generators.owlgen import cli as gen_owl
|
27
|
+
from linkml.generators.panderagen import cli as gen_pandera
|
27
28
|
from linkml.generators.plantumlgen import cli as gen_plantuml
|
28
29
|
from linkml.generators.prefixmapgen import cli as gen_prefix_map
|
29
30
|
from linkml.generators.projectgen import cli as gen_project
|
@@ -110,6 +111,7 @@ generate.add_command(gen_plantuml, name="plantuml")
|
|
110
111
|
generate.add_command(gen_proto, name="proto")
|
111
112
|
generate.add_command(gen_python, name="python")
|
112
113
|
generate.add_command(gen_pydantic, name="pydantic")
|
114
|
+
generate.add_command(gen_pandera, name="pandera")
|
113
115
|
generate.add_command(gen_rdf, name="rdf")
|
114
116
|
generate.add_command(gen_shex, name="shex")
|
115
117
|
generate.add_command(gen_shacl, name="shacl")
|
linkml/generators/__init__.py
CHANGED
@@ -8,6 +8,7 @@ from linkml.generators.jsonldcontextgen import ContextGenerator
|
|
8
8
|
from linkml.generators.jsonldgen import JSONLDGenerator
|
9
9
|
from linkml.generators.jsonschemagen import JsonSchemaGenerator
|
10
10
|
from linkml.generators.owlgen import OwlSchemaGenerator
|
11
|
+
from linkml.generators.panderagen import PanderaGenerator
|
11
12
|
from linkml.generators.pydanticgen import PydanticGenerator
|
12
13
|
from linkml.generators.pythongen import PythonGenerator
|
13
14
|
from linkml.generators.rdfgen import RDFGenerator
|
@@ -42,6 +43,7 @@ __all__ = [
|
|
42
43
|
"yumlgen",
|
43
44
|
"OwlSchemaGenerator",
|
44
45
|
"PydanticGenerator",
|
46
|
+
"PanderaGenerator",
|
45
47
|
"PythonGenerator",
|
46
48
|
"JavaGenerator",
|
47
49
|
"ContextGenerator",
|
@@ -1,8 +1,10 @@
|
|
1
1
|
{% macro slot_relationship(element, slot) %}
|
2
|
-
{%
|
3
|
-
|
4
|
-
|
5
|
-
|
2
|
+
{% if slot.range is not none %}
|
3
|
+
{% set range_element = gen.name(schemaview.get_element(slot.range)) %}
|
4
|
+
{% set relation_label = gen.name(slot) %}
|
5
|
+
{{ gen.name(element) }} --> "{{ gen.cardinality(slot) }}" {{ range_element }} : {{ relation_label }}
|
6
|
+
click {{ range_element }} href "../{{ range_element }}"
|
7
|
+
{% endif %}
|
6
8
|
{% endmacro %}
|
7
9
|
|
8
10
|
{% if schemaview.class_parents(element.name) and schemaview.class_children(element.name) %}
|
@@ -22,7 +24,7 @@
|
|
22
24
|
|
23
25
|
{% for s in schemaview.class_induced_slots(element.name)|sort(attribute='name') -%}
|
24
26
|
{{ gen.name(element) }} : {{gen.name(s)}}
|
25
|
-
{% if s.range not in gen.all_type_object_names() %}
|
27
|
+
{% if s.range is not none and s.range not in gen.all_type_object_names() %}
|
26
28
|
{{ slot_relationship(element, s) }}
|
27
29
|
{% endif %}
|
28
30
|
{% endfor %}
|
@@ -38,7 +40,7 @@
|
|
38
40
|
{% endfor %}
|
39
41
|
{% for s in schemaview.class_induced_slots(element.name)|sort(attribute='name') -%}
|
40
42
|
{{ gen.name(element) }} : {{gen.name(s)}}
|
41
|
-
{% if s.range not in gen.all_type_object_names() %}
|
43
|
+
{% if s.range is not none and s.range not in gen.all_type_object_names() %}
|
42
44
|
{{ slot_relationship(element, s) }}
|
43
45
|
{% endif %}
|
44
46
|
{% endfor %}
|
@@ -54,7 +56,7 @@
|
|
54
56
|
{% endfor %}
|
55
57
|
{% for s in schemaview.class_induced_slots(element.name)|sort(attribute='name') -%}
|
56
58
|
{{ gen.name(element) }} : {{gen.name(s)}}
|
57
|
-
{% if s.range not in gen.all_type_object_names() %}
|
59
|
+
{% if s.range is not none and s.range not in gen.all_type_object_names() %}
|
58
60
|
{{ slot_relationship(element, s) }}
|
59
61
|
{% endif %}
|
60
62
|
{% endfor %}
|
@@ -66,7 +68,7 @@
|
|
66
68
|
click {{ gen.name(element) }} href "../{{gen.name(element)}}"
|
67
69
|
{% for s in schemaview.class_induced_slots(element.name)|sort(attribute='name') -%}
|
68
70
|
{{ gen.name(element) }} : {{gen.name(s)}}
|
69
|
-
{% if s.range not in gen.all_type_object_names() %}
|
71
|
+
{% if s.range is not none and s.range not in gen.all_type_object_names() %}
|
70
72
|
{{ slot_relationship(element, s) }}
|
71
73
|
{% endif %}
|
72
74
|
{% endfor %}
|
@@ -62,7 +62,7 @@ Instances of this class *should* have identifiers with one of the following pref
|
|
62
62
|
{% for a in element.annotations -%}
|
63
63
|
{%- if a|string|first != '_' -%}
|
64
64
|
| {{ a }} | {{ element.annotations[a].value }} |
|
65
|
-
{
|
65
|
+
{% endif -%}
|
66
66
|
{% endfor %}
|
67
67
|
{% endif %}
|
68
68
|
|
@@ -21,7 +21,7 @@ Name: {{ schema.name }}
|
|
21
21
|
| --- | --- |
|
22
22
|
{% if gen.hierarchical_class_view -%}
|
23
23
|
{% for u, v in gen.class_hierarchy_as_tuples() -%}
|
24
|
-
| {{ " "|safe*u*8 }}{{ gen.link(schemaview.get_class(v), True) }} | {{ schemaview.get_class(v).description }} |
|
24
|
+
| {{ " "|safe*u*8 }}{{ gen.link(schemaview.get_class(v), True) }} | {{ schemaview.get_class(v).description|enshorten }} |
|
25
25
|
{% endfor %}
|
26
26
|
{% else -%}
|
27
27
|
{% for c in gen.all_class_objects()|sort(attribute=sort_by) -%}
|
linkml/generators/docgen.py
CHANGED
@@ -61,7 +61,7 @@ SUBSET_SUBFOLDER = "subsets"
|
|
61
61
|
|
62
62
|
def enshorten(input):
|
63
63
|
"""
|
64
|
-
Custom
|
64
|
+
Custom filters to truncate any long text intended to go in a table
|
65
65
|
and to remove anything after a newline"""
|
66
66
|
if input is None:
|
67
67
|
return ""
|
@@ -76,8 +76,11 @@ def enshorten(input):
|
|
76
76
|
return input
|
77
77
|
|
78
78
|
|
79
|
-
def
|
80
|
-
|
79
|
+
def text_to_web(input) -> str:
|
80
|
+
"""Custom filter to convert multi-line text strings into a suitable format for web/markdown output."""
|
81
|
+
if input is None:
|
82
|
+
return ""
|
83
|
+
return "<br>".join(input.strip().split("\n"))
|
81
84
|
|
82
85
|
|
83
86
|
def _ensure_ranked(elements: Iterable[Element]):
|
@@ -155,6 +158,9 @@ class DocGenerator(Generator):
|
|
155
158
|
subfolder_type_separation: bool = False
|
156
159
|
"""Whether each type (class, slot, etc.) should be put in separate subfolder for navigation purposes"""
|
157
160
|
|
161
|
+
truncate_descriptions: bool = True
|
162
|
+
"""Whether to truncate long (multi-line) descriptions down to a single line."""
|
163
|
+
|
158
164
|
example_directory: Optional[str] = None
|
159
165
|
example_runner: ExampleRunner = field(default_factory=lambda: ExampleRunner())
|
160
166
|
|
@@ -314,7 +320,7 @@ class DocGenerator(Generator):
|
|
314
320
|
# TODO: relative paths
|
315
321
|
# loader = FileSystemLoader()
|
316
322
|
env = Environment()
|
317
|
-
customize_environment(env)
|
323
|
+
self.customize_environment(env)
|
318
324
|
return env.get_template(path)
|
319
325
|
else:
|
320
326
|
base_file_name = f"{element_type}.{self._file_suffix()}.jinja2"
|
@@ -332,7 +338,7 @@ class DocGenerator(Generator):
|
|
332
338
|
folder = os.path.join(package_dir, "docgen", "")
|
333
339
|
loader = FileSystemLoader(folder)
|
334
340
|
env = Environment(loader=loader)
|
335
|
-
customize_environment(env)
|
341
|
+
self.customize_environment(env)
|
336
342
|
return env.get_template(base_file_name)
|
337
343
|
|
338
344
|
def schema_title(self) -> str:
|
@@ -384,6 +390,10 @@ class DocGenerator(Generator):
|
|
384
390
|
if isinstance(element, (EnumDefinition, SubsetDefinition)):
|
385
391
|
# TODO: fix schema view to handle URIs for enums and subsets
|
386
392
|
return self.name(element)
|
393
|
+
|
394
|
+
if self.subfolder_type_separation:
|
395
|
+
return self.schemaview.get_uri(element, expand=expand, use_element_type=True)
|
396
|
+
|
387
397
|
return self.schemaview.get_uri(element, expand=expand)
|
388
398
|
|
389
399
|
def uri_link(self, element: Union[Element, str]) -> str:
|
@@ -955,13 +965,19 @@ class DocGenerator(Generator):
|
|
955
965
|
objs.append((stem, f.read()))
|
956
966
|
return objs
|
957
967
|
|
968
|
+
def customize_environment(self, env: Environment):
|
969
|
+
if self.truncate_descriptions:
|
970
|
+
env.filters["enshorten"] = enshorten
|
971
|
+
else:
|
972
|
+
env.filters["enshorten"] = text_to_web
|
973
|
+
|
958
974
|
|
959
975
|
@shared_arguments(DocGenerator)
|
960
976
|
@click.option(
|
961
977
|
"--directory",
|
962
978
|
"-d",
|
963
979
|
required=True,
|
964
|
-
help="
|
980
|
+
help="Path to the directory where you want the Markdown files to be written to",
|
965
981
|
)
|
966
982
|
@click.option("--index-name", default="index", show_default=True, help="Name of the index document.")
|
967
983
|
@click.option("--dialect", help="Dialect or 'flavor' of Markdown used.")
|
@@ -988,7 +1004,7 @@ class DocGenerator(Generator):
|
|
988
1004
|
show_default=True,
|
989
1005
|
help="Generating metamodel. Only use this for generating meta.py",
|
990
1006
|
)
|
991
|
-
@click.option("--template-directory", help="
|
1007
|
+
@click.option("--template-directory", help="Path to the directory with custom jinja2 templates")
|
992
1008
|
@click.option(
|
993
1009
|
"--use-slot-uris/--no-use-slot-uris",
|
994
1010
|
default=False,
|
@@ -1027,6 +1043,14 @@ YAML, and including it when necessary but not by default (e.g. in documentation
|
|
1027
1043
|
default=False,
|
1028
1044
|
help="Separate type (class, slot, etc.) outputs in different subfolders for navigation purposes",
|
1029
1045
|
)
|
1046
|
+
@click.option(
|
1047
|
+
"--truncate-descriptions",
|
1048
|
+
default=True,
|
1049
|
+
show_default=True,
|
1050
|
+
help="""
|
1051
|
+
Whether to truncate long (potentially spanning multiple lines) descriptions of classes, slots, etc., in the docs.
|
1052
|
+
Set to true for truncated descriptions, and false to display full descriptions.""",
|
1053
|
+
)
|
1030
1054
|
@click.version_option(__version__, "-V", "--version")
|
1031
1055
|
@click.command(name="doc")
|
1032
1056
|
def cli(
|
@@ -1040,6 +1064,7 @@ def cli(
|
|
1040
1064
|
hierarchical_class_view,
|
1041
1065
|
subfolder_type_separation,
|
1042
1066
|
render_imports,
|
1067
|
+
truncate_descriptions,
|
1043
1068
|
**args,
|
1044
1069
|
):
|
1045
1070
|
"""Generate documentation folder from a LinkML YAML schema
|
@@ -1072,6 +1097,7 @@ def cli(
|
|
1072
1097
|
index_name=index_name,
|
1073
1098
|
subfolder_type_separation=subfolder_type_separation,
|
1074
1099
|
render_imports=render_imports,
|
1100
|
+
truncate_descriptions=truncate_descriptions,
|
1075
1101
|
**args,
|
1076
1102
|
)
|
1077
1103
|
print(gen.serialize())
|
linkml/generators/jsonldgen.py
CHANGED
@@ -162,7 +162,7 @@ class JSONLDGenerator(Generator):
|
|
162
162
|
# model_context = self.schema.source_file.replace('.yaml', '.prefixes.context.jsonld')
|
163
163
|
# context = [METAMODEL_CONTEXT_URI, f'file://./{model_context}']
|
164
164
|
# TODO: The _visit function above alters the schema in situ
|
165
|
-
add_prefixes = ContextGenerator(self.original_schema, model=False,
|
165
|
+
add_prefixes = ContextGenerator(self.original_schema, model=False, emit_metadata=False).serialize()
|
166
166
|
add_prefixes_json = loads(add_prefixes)
|
167
167
|
context = [METAMODEL_CONTEXT_URI, add_prefixes_json["@context"]]
|
168
168
|
elif isinstance(context, str): # Some of the older code doesn't do multiple contexts
|
linkml/generators/markdowngen.py
CHANGED
@@ -18,6 +18,7 @@ from linkml_runtime.utils.formatutils import be, camelcase, underscore
|
|
18
18
|
|
19
19
|
from linkml._version import __version__
|
20
20
|
from linkml.generators.yumlgen import YumlGenerator
|
21
|
+
from linkml.utils.deprecation import deprecation_warning
|
21
22
|
from linkml.utils.generator import Generator, shared_arguments
|
22
23
|
from linkml.utils.typereferences import References
|
23
24
|
|
@@ -31,8 +32,24 @@ class MarkdownGenerator(Generator):
|
|
31
32
|
additionally, an index.md is generated that links everything together.
|
32
33
|
|
33
34
|
The markdown is suitable for deployment as a MkDocs or Sphinx site
|
35
|
+
|
36
|
+
.. admonition:: Deprecated
|
37
|
+
:class: warning
|
38
|
+
|
39
|
+
The MarkdownGenerator class is being deprecated in favor of DocGenerator which can
|
40
|
+
be found at the following path – `linkml/generators/docgen.py`. Going forward, DocGenerator
|
41
|
+
which can be invoked using the `gen-doc` command will be the preferred way to generate
|
42
|
+
Markdown documentation files for LinkML schemas.
|
43
|
+
|
44
|
+
.. deprecated:: v1.9.1
|
45
|
+
|
46
|
+
Recommendation: Update to use `gen-doc`
|
34
47
|
"""
|
35
48
|
|
49
|
+
def __post_init__(self) -> None:
|
50
|
+
deprecation_warning("gen-markdown")
|
51
|
+
super().__post_init__()
|
52
|
+
|
36
53
|
# ClassVars
|
37
54
|
generatorname = os.path.basename(__file__)
|
38
55
|
generatorversion = "0.2.1"
|
@@ -799,7 +816,13 @@ def pad_heading(text: str) -> str:
|
|
799
816
|
@click.option("--warnonexist", is_flag=True, help="Warn if output file already exists")
|
800
817
|
@click.version_option(__version__, "-V", "--version")
|
801
818
|
def cli(yamlfile, map_fields, dir, img, index_file, notypesdir, warnonexist, **kwargs):
|
802
|
-
"""Generate markdown documentation of a LinkML model
|
819
|
+
"""Generate markdown documentation of a LinkML model.
|
820
|
+
|
821
|
+
.. warning::
|
822
|
+
`gen-markdown` is deprecated. Please use `gen-doc` instead.
|
823
|
+
"""
|
824
|
+
deprecation_warning("gen-markdown")
|
825
|
+
|
803
826
|
gen = MarkdownGenerator(yamlfile, no_types_dir=notypesdir, warn_on_exist=warnonexist, **kwargs)
|
804
827
|
if map_fields is not None:
|
805
828
|
gen.metamodel_name_map = {}
|
@@ -10,7 +10,7 @@ from jinja2 import Environment, FileSystemLoader
|
|
10
10
|
from linkml_runtime.linkml_model.meta import Element, SlotDefinition
|
11
11
|
from linkml_runtime.utils.schemaview import SchemaView
|
12
12
|
|
13
|
-
from linkml.generators.docgen import DocGenerator
|
13
|
+
from linkml.generators.docgen import DocGenerator
|
14
14
|
from linkml.utils.generator import Generator, shared_arguments
|
15
15
|
|
16
16
|
|
@@ -59,7 +59,8 @@ class MermaidClassDiagramGenerator(Generator):
|
|
59
59
|
template_name = os.path.basename(self.template_file)
|
60
60
|
loader = FileSystemLoader(template_folder)
|
61
61
|
env = Environment(loader=loader)
|
62
|
-
|
62
|
+
temp_doc_gen = DocGenerator(self.schema, mergeimports=self.mergeimports)
|
63
|
+
temp_doc_gen.customize_environment(env)
|
63
64
|
|
64
65
|
template = env.get_template(template_name)
|
65
66
|
|
@@ -0,0 +1,20 @@
|
|
1
|
+
from linkml_runtime.linkml_model import ClassDefinitionName
|
2
|
+
|
3
|
+
|
4
|
+
class ClassGeneratorMixin:
|
5
|
+
def ordered_classes(self):
|
6
|
+
return [self.schemaview.get_class(cn, strict=True) for cn in self.order_classes_by_hierarchy()]
|
7
|
+
|
8
|
+
def order_classes_by_hierarchy(self) -> list[ClassDefinitionName]:
|
9
|
+
sv = self.schemaview
|
10
|
+
olist = sv.class_roots()
|
11
|
+
unprocessed = [cn for cn in sv.all_classes() if cn not in olist]
|
12
|
+
|
13
|
+
while len(unprocessed) > 0:
|
14
|
+
ext_list = [cn for cn in unprocessed if not any(p for p in sv.class_parents(cn) if p not in olist)]
|
15
|
+
if len(ext_list) == 0:
|
16
|
+
raise ValueError(f"Cycle in hierarchy, cannot process: {unprocessed}")
|
17
|
+
olist += ext_list
|
18
|
+
unprocessed = [cn for cn in unprocessed if cn not in olist]
|
19
|
+
|
20
|
+
return olist
|
@@ -0,0 +1,17 @@
|
|
1
|
+
from linkml_runtime.linkml_model.meta import PermissibleValue, PermissibleValueText
|
2
|
+
|
3
|
+
|
4
|
+
class EnumGeneratorMixin:
|
5
|
+
def escape_permissible_value_text(self, pv_text):
|
6
|
+
return pv_text.replace("'", "\\'").replace('"', '\\"')
|
7
|
+
|
8
|
+
def extract_permissible_text(self, pv):
|
9
|
+
if isinstance(pv, str) or isinstance(pv, PermissibleValueText):
|
10
|
+
return self.escape_permissible_value_text(pv)
|
11
|
+
elif isinstance(pv, PermissibleValue):
|
12
|
+
return pv.text.code
|
13
|
+
else:
|
14
|
+
raise ValueError(f"Invalid permissible value in enum : {pv}")
|
15
|
+
|
16
|
+
def get_enum_permissible_values(self, enum):
|
17
|
+
return list(map(self.extract_permissible_text, enum.permissible_values or []))
|
@@ -0,0 +1,216 @@
|
|
1
|
+
import logging
|
2
|
+
import os
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from enum import Enum
|
5
|
+
from pathlib import PurePosixPath
|
6
|
+
from types import ModuleType
|
7
|
+
from typing import Optional
|
8
|
+
|
9
|
+
import click
|
10
|
+
from jinja2 import Environment, PackageLoader
|
11
|
+
from linkml_runtime.linkml_model.meta import TypeDefinition
|
12
|
+
from linkml_runtime.utils.compile_python import compile_python
|
13
|
+
from linkml_runtime.utils.formatutils import camelcase
|
14
|
+
from linkml_runtime.utils.schemaview import SchemaView
|
15
|
+
|
16
|
+
from linkml._version import __version__
|
17
|
+
from linkml.generators.oocodegen import OOClass, OOCodeGenerator, OODocument
|
18
|
+
|
19
|
+
from .class_generator_mixin import ClassGeneratorMixin
|
20
|
+
from .enum_generator_mixin import EnumGeneratorMixin
|
21
|
+
from .slot_generator_mixin import SlotGeneratorMixin
|
22
|
+
|
23
|
+
logger = logging.getLogger(__name__)
|
24
|
+
|
25
|
+
|
26
|
+
# keys are template_path
|
27
|
+
TYPEMAP = {
|
28
|
+
"panderagen_class_based": {
|
29
|
+
"xsd:string": "str",
|
30
|
+
"xsd:integer": "int",
|
31
|
+
"xsd:float": "float",
|
32
|
+
"xsd:double": "float",
|
33
|
+
"xsd:boolean": "bool",
|
34
|
+
"xsd:dateTime": "DateTime",
|
35
|
+
"xsd:date": "Date",
|
36
|
+
"xsd:time": "Time",
|
37
|
+
"xsd:anyURI": "str",
|
38
|
+
"xsd:decimal": "float",
|
39
|
+
},
|
40
|
+
}
|
41
|
+
|
42
|
+
|
43
|
+
class TemplateEnum(Enum):
|
44
|
+
CLASS_BASED = "panderagen_class_based"
|
45
|
+
|
46
|
+
|
47
|
+
@dataclass
|
48
|
+
class PanderaGenerator(OOCodeGenerator, EnumGeneratorMixin, ClassGeneratorMixin, SlotGeneratorMixin):
|
49
|
+
"""
|
50
|
+
Generates Pandera python classes from a LinkML schema.
|
51
|
+
|
52
|
+
Status: incompletely implemented
|
53
|
+
|
54
|
+
One styles is supported:
|
55
|
+
|
56
|
+
- panderagen_class_based
|
57
|
+
"""
|
58
|
+
|
59
|
+
DEFAULT_TEMPLATE_PATH = "panderagen_class_based"
|
60
|
+
DEFAULT_TEMPLATE_FILE = "pandera.jinja2"
|
61
|
+
|
62
|
+
# ClassVars
|
63
|
+
generatorname = os.path.basename(__file__)
|
64
|
+
generatorstem = PurePosixPath(generatorname).stem
|
65
|
+
generatorversion = "0.0.1"
|
66
|
+
valid_formats = ["python"]
|
67
|
+
file_extension = "py"
|
68
|
+
java_style = False
|
69
|
+
|
70
|
+
# ObjectVars
|
71
|
+
template_file: Optional[str] = None
|
72
|
+
template_path: str = DEFAULT_TEMPLATE_PATH
|
73
|
+
|
74
|
+
gen_classvars: bool = True
|
75
|
+
gen_slots: bool = True
|
76
|
+
genmeta: bool = False
|
77
|
+
emit_metadata: bool = True
|
78
|
+
coerce: bool = False
|
79
|
+
|
80
|
+
def default_value_for_type(self, typ: str) -> str:
|
81
|
+
"""Allow underlying framework to handle default if not specified."""
|
82
|
+
return None
|
83
|
+
|
84
|
+
@staticmethod
|
85
|
+
def make_multivalued(range: str) -> str:
|
86
|
+
return f"List[{range}]"
|
87
|
+
|
88
|
+
def uri_type_map(self, xsd_uri: str, template: str = None):
|
89
|
+
if template is None:
|
90
|
+
template = self.template_path
|
91
|
+
|
92
|
+
return TYPEMAP[template].get(xsd_uri)
|
93
|
+
|
94
|
+
def map_type(self, t: TypeDefinition) -> str:
|
95
|
+
if t.uri:
|
96
|
+
typ = self.uri_type_map(t.uri)
|
97
|
+
return typ
|
98
|
+
elif t.typeof:
|
99
|
+
typ = self.map_type(self.schemaview.get_type(t.typeof))
|
100
|
+
return typ
|
101
|
+
else:
|
102
|
+
raise ValueError(f"{t} cannot be mapped to a type")
|
103
|
+
|
104
|
+
def load_template(self, template_filename):
|
105
|
+
jinja_env = Environment(loader=PackageLoader("linkml.generators.panderagen", self.template_path))
|
106
|
+
return jinja_env.get_template(template_filename)
|
107
|
+
|
108
|
+
def compile_pandera(self) -> ModuleType:
|
109
|
+
"""
|
110
|
+
Generates and compiles Pandera model
|
111
|
+
"""
|
112
|
+
pandera_code = self.serialize()
|
113
|
+
|
114
|
+
return compile_python(pandera_code)
|
115
|
+
|
116
|
+
def serialize(self, rendered_module: Optional[OODocument] = None) -> str:
|
117
|
+
"""
|
118
|
+
Serialize the schema to a Pandera module as a string
|
119
|
+
"""
|
120
|
+
if self.template_path is None:
|
121
|
+
self.template_path = PanderaGenerator.DEFAULT_TEMPLATE_PATH
|
122
|
+
|
123
|
+
if rendered_module is not None:
|
124
|
+
module = rendered_module
|
125
|
+
else:
|
126
|
+
module = self.render()
|
127
|
+
|
128
|
+
if self.template_file is None:
|
129
|
+
self.template_file = PanderaGenerator.DEFAULT_TEMPLATE_FILE
|
130
|
+
template_file = self.template_file
|
131
|
+
|
132
|
+
template_obj = self.load_template(template_file)
|
133
|
+
|
134
|
+
code = template_obj.render(
|
135
|
+
doc=module,
|
136
|
+
metamodel_version=self.schema.metamodel_version,
|
137
|
+
model_version=self.schema.version,
|
138
|
+
coerce=self.coerce,
|
139
|
+
type_map=TYPEMAP,
|
140
|
+
template_path=self.template_path,
|
141
|
+
)
|
142
|
+
return code
|
143
|
+
|
144
|
+
def render(self) -> OODocument:
|
145
|
+
"""
|
146
|
+
Create a data structure ready to pass to the serialization templates.
|
147
|
+
"""
|
148
|
+
sv: SchemaView = self.schemaview
|
149
|
+
|
150
|
+
module_name = camelcase(sv.schema.name)
|
151
|
+
|
152
|
+
oodoc = OODocument(name=module_name, package=self.package, source_schema=sv.schema)
|
153
|
+
|
154
|
+
classes = []
|
155
|
+
|
156
|
+
for c in self.ordered_classes():
|
157
|
+
cn = c.name
|
158
|
+
safe_cn = camelcase(cn)
|
159
|
+
ooclass = OOClass(
|
160
|
+
name=safe_cn,
|
161
|
+
description=c.description,
|
162
|
+
package=self.package,
|
163
|
+
fields=[],
|
164
|
+
source_class=c,
|
165
|
+
)
|
166
|
+
classes.append(ooclass)
|
167
|
+
if c.mixin:
|
168
|
+
ooclass.mixin = c.mixin
|
169
|
+
if c.mixins:
|
170
|
+
ooclass.mixins = [(x) for x in c.mixins]
|
171
|
+
if c.is_a:
|
172
|
+
ooclass.is_a = self.get_class_name(c.is_a)
|
173
|
+
parent_slots = sv.class_slots(c.is_a)
|
174
|
+
else:
|
175
|
+
parent_slots = []
|
176
|
+
for sn in sv.class_slots(cn):
|
177
|
+
oofield = self.handle_slot(cn, sn)
|
178
|
+
if sn not in parent_slots:
|
179
|
+
ooclass.fields.append(oofield)
|
180
|
+
ooclass.all_fields.append(oofield)
|
181
|
+
|
182
|
+
oodoc.classes = classes
|
183
|
+
|
184
|
+
return oodoc
|
185
|
+
|
186
|
+
|
187
|
+
@click.option("--package", help="Package name where relevant for generated class files")
|
188
|
+
@click.option("--template-path", help="Optional jinja2 template directory within module")
|
189
|
+
@click.option("--template-file", help="Optional jinja2 template to use for class generation")
|
190
|
+
@click.version_option(__version__, "-V", "--version")
|
191
|
+
@click.argument("yamlfile")
|
192
|
+
@click.command(name="gen-pandera")
|
193
|
+
def cli(
|
194
|
+
yamlfile,
|
195
|
+
package=None,
|
196
|
+
template_path=None,
|
197
|
+
template_file=None,
|
198
|
+
**args,
|
199
|
+
):
|
200
|
+
if template_path is not None and template_path not in TYPEMAP:
|
201
|
+
raise Exception(f"Template {template_path} not supported")
|
202
|
+
|
203
|
+
"""Generate Pandera classes to represent a LinkML model"""
|
204
|
+
gen = PanderaGenerator(
|
205
|
+
yamlfile,
|
206
|
+
package=package,
|
207
|
+
template_path=template_path,
|
208
|
+
template_file=template_file,
|
209
|
+
**args,
|
210
|
+
)
|
211
|
+
|
212
|
+
print(gen.serialize())
|
213
|
+
|
214
|
+
|
215
|
+
if __name__ == "__main__":
|
216
|
+
cli()
|
@@ -0,0 +1,26 @@
|
|
1
|
+
{#-
|
2
|
+
Jinja2 Template for a Pandera class-based model
|
3
|
+
Details at https://pandera.readthedocs.io/en/stable/dataframe_models.html
|
4
|
+
-#}
|
5
|
+
{%- import 'slots.jinja2' as slot_macros -%}
|
6
|
+
|
7
|
+
{%- macro render_class(cls) %}
|
8
|
+
class {{cls.name}}(
|
9
|
+
{%- if cls.is_a -%}
|
10
|
+
{{ cls.is_a }}
|
11
|
+
{%- else -%}
|
12
|
+
pla.DataFrameModel, _LinkmlPanderaValidator
|
13
|
+
{%- endif -%}
|
14
|
+
):
|
15
|
+
{%- if cls.source_class.description %}
|
16
|
+
"""
|
17
|
+
{{ cls.source_class.description }}
|
18
|
+
"""
|
19
|
+
{% endif -%}
|
20
|
+
{%- if (cls.fields | length) == 0 %}
|
21
|
+
pass
|
22
|
+
{% endif -%}
|
23
|
+
{%- for field in cls.fields -%}
|
24
|
+
{{ slot_macros.render_slot(field) }}
|
25
|
+
{%- endfor -%}
|
26
|
+
{% endmacro -%}
|
@@ -0,0 +1,21 @@
|
|
1
|
+
{#-
|
2
|
+
Jinja2 Template for imports used for a Pandera model Python module
|
3
|
+
-#}
|
4
|
+
import pandera.polars as pla
|
5
|
+
from pandera.api.polars.types import PolarsData
|
6
|
+
import polars as pl
|
7
|
+
from typing import Optional, List, Dict
|
8
|
+
|
9
|
+
from pandera.typing import (
|
10
|
+
Index,
|
11
|
+
DataFrame,
|
12
|
+
Series
|
13
|
+
)
|
14
|
+
from pandera.engines.polars_engine import (
|
15
|
+
DateTime,
|
16
|
+
Date,
|
17
|
+
Time,
|
18
|
+
Enum,
|
19
|
+
Struct,
|
20
|
+
Object
|
21
|
+
)
|
@@ -0,0 +1,26 @@
|
|
1
|
+
{#-
|
2
|
+
Jinja2 Template for a mixin class used by the Linkml/Pandera class-based model
|
3
|
+
Inline generation avoids dependencies on LinkML in the generated code.
|
4
|
+
|
5
|
+
-#}
|
6
|
+
class _LinkmlPanderaValidator:
|
7
|
+
|
8
|
+
@classmethod
|
9
|
+
def generate_polars_schema(cls, object_to_validate) -> dict:
|
10
|
+
"""Creates a nested PolaRS schema suitable for loading the object_to_validate.
|
11
|
+
Optional columns that are not present in the data are omitted.
|
12
|
+
This approach is only suitable to enable the test fixtures.
|
13
|
+
"""
|
14
|
+
polars_schema = {}
|
15
|
+
|
16
|
+
for column_name, column in cls.to_schema().columns.items():
|
17
|
+
dtype = column.properties["dtype"]
|
18
|
+
required = column.properties["required"]
|
19
|
+
|
20
|
+
if required or column_name in object_to_validate:
|
21
|
+
if dtype.type == pl.Struct:
|
22
|
+
pass
|
23
|
+
else:
|
24
|
+
polars_schema[column_name] = dtype.type
|
25
|
+
|
26
|
+
return polars_schema
|
@@ -0,0 +1,16 @@
|
|
1
|
+
{#-
|
2
|
+
Jinja2 Template for the top level of a Pandera class-based model
|
3
|
+
Details at https://pandera.readthedocs.io/
|
4
|
+
-#}
|
5
|
+
{%- import 'header.jinja2' as header -%}
|
6
|
+
{%- import 'class.jinja2' as class_macros -%}
|
7
|
+
{%- import 'mixins.jinja2' as mixins -%}
|
8
|
+
{{ header }}
|
9
|
+
|
10
|
+
{{ mixins }}
|
11
|
+
|
12
|
+
{% if metamodel_version %}# metamodel_version: {{metamodel_version}}{% endif %}
|
13
|
+
{% if model_version %}# version: {{model_version}}{% endif %}
|
14
|
+
{%- for cls in doc.classes -%}
|
15
|
+
{{- class_macros.render_class(cls) -}}
|
16
|
+
{%- endfor %}
|