linkml-map 0.1.0__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_map/__init__.py +6 -0
- linkml_map/cli/__init__.py +0 -0
- linkml_map/cli/cli.py +205 -0
- linkml_map/compiler/__init__.py +1 -0
- linkml_map/compiler/awk_compiler.py +13 -0
- linkml_map/compiler/compiler.py +88 -0
- linkml_map/compiler/graphviz_compiler.py +109 -0
- linkml_map/compiler/j2_based_compiler.py +36 -0
- linkml_map/compiler/markdown_compiler.py +12 -0
- linkml_map/compiler/python_compiler.py +113 -0
- linkml_map/compiler/r2rml_compiler.py +12 -0
- linkml_map/compiler/sparql_compiler.py +12 -0
- linkml_map/compiler/sql_compiler.py +138 -0
- linkml_map/compiler/sssom_compiler.py +13 -0
- linkml_map/compiler/templates/__init__.py +3 -0
- linkml_map/compiler/templates/markdown.j2 +24 -0
- linkml_map/compiler/tr/__init__.py +4 -0
- linkml_map/compiler/tr/transformer_to_mapping_tables.tr.yaml +29 -0
- linkml_map/datamodel/__init__.py +4 -0
- linkml_map/datamodel/sssom.map.yaml +8 -0
- linkml_map/datamodel/transformer_model.py +425 -0
- linkml_map/datamodel/transformer_model.yaml +369 -0
- linkml_map/functions/__init__.py +0 -0
- linkml_map/functions/unit_conversion.py +157 -0
- linkml_map/importer/__init__.py +0 -0
- linkml_map/importer/importer.py +35 -0
- linkml_map/inference/__init__.py +0 -0
- linkml_map/inference/inference.py +38 -0
- linkml_map/inference/inverter.py +181 -0
- linkml_map/inference/schema_mapper.py +249 -0
- linkml_map/session.py +167 -0
- linkml_map/transformer/__init__.py +0 -0
- linkml_map/transformer/duckdb_transformer.py +70 -0
- linkml_map/transformer/object_transformer.py +368 -0
- linkml_map/transformer/transformer.py +269 -0
- linkml_map/utils/__init__.py +0 -0
- linkml_map/utils/dynamic_object.py +49 -0
- linkml_map/utils/eval_utils.py +207 -0
- linkml_map/utils/loaders.py +22 -0
- linkml_map/utils/multi_file_transformer.py +212 -0
- linkml_map-0.1.0.dist-info/METADATA +56 -0
- linkml_map-0.1.0.dist-info/RECORD +44 -0
- linkml_map-0.1.0.dist-info/WHEEL +4 -0
- linkml_map-0.1.0.dist-info/entry_points.txt +3 -0
linkml_map/__init__.py
ADDED
|
File without changes
|
linkml_map/cli/cli.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Command line interface for linkml-transformer."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"main",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
import yaml
|
|
13
|
+
from linkml_runtime import SchemaView
|
|
14
|
+
from linkml_runtime.dumpers import yaml_dumper
|
|
15
|
+
|
|
16
|
+
from linkml_map.compiler.markdown_compiler import MarkdownCompiler
|
|
17
|
+
from linkml_map.compiler.python_compiler import PythonCompiler
|
|
18
|
+
from linkml_map.inference.inverter import TransformationSpecificationInverter
|
|
19
|
+
from linkml_map.inference.schema_mapper import SchemaMapper
|
|
20
|
+
from linkml_map.transformer.object_transformer import ObjectTransformer
|
|
21
|
+
|
|
22
|
+
schema_option = click.option("-s", "--schema", help="Path to source schema.")
|
|
23
|
+
transformer_specification_option = click.option(
|
|
24
|
+
"-T", "--transformer-specification", help="Path to transformer specification."
|
|
25
|
+
)
|
|
26
|
+
output_option_wb = click.option(
|
|
27
|
+
"-o",
|
|
28
|
+
"--output",
|
|
29
|
+
type=click.File(mode="wb"),
|
|
30
|
+
default=sys.stdout,
|
|
31
|
+
help="Output file.",
|
|
32
|
+
)
|
|
33
|
+
output_option = click.option("-o", "--output", help="Output file.")
|
|
34
|
+
output_format_options = click.option(
|
|
35
|
+
"-O",
|
|
36
|
+
"--output-format",
|
|
37
|
+
type=click.Choice(["json", "yaml", "pickle", "md"]),
|
|
38
|
+
default="yaml",
|
|
39
|
+
help="Output format.",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@click.group()
|
|
44
|
+
@click.option("-v", "--verbose", count=True)
|
|
45
|
+
@click.option("-q", "--quiet")
|
|
46
|
+
# @click.version_option(__version__)
|
|
47
|
+
def main(verbose: int, quiet: bool):
|
|
48
|
+
"""CLI for linkml-transformer."""
|
|
49
|
+
logger = logging.getLogger()
|
|
50
|
+
if verbose >= 2:
|
|
51
|
+
logger.setLevel(level=logging.DEBUG)
|
|
52
|
+
elif verbose == 1:
|
|
53
|
+
logger.setLevel(level=logging.INFO)
|
|
54
|
+
else:
|
|
55
|
+
logger.setLevel(level=logging.WARNING)
|
|
56
|
+
if quiet:
|
|
57
|
+
logger.setLevel(level=logging.ERROR)
|
|
58
|
+
logger.info(f"Logger {logger.name} set to level {logger.level}")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@main.command()
|
|
62
|
+
@output_option
|
|
63
|
+
@transformer_specification_option
|
|
64
|
+
@schema_option
|
|
65
|
+
@output_format_options
|
|
66
|
+
@click.option("--source-type")
|
|
67
|
+
@click.option(
|
|
68
|
+
"--unrestricted-eval/--no-unrestricted-eval",
|
|
69
|
+
default=False,
|
|
70
|
+
show_default=True,
|
|
71
|
+
help="Allow unrestricted eval of python expressions.",
|
|
72
|
+
)
|
|
73
|
+
@click.argument("input")
|
|
74
|
+
def map_data(
|
|
75
|
+
input,
|
|
76
|
+
schema,
|
|
77
|
+
source_type,
|
|
78
|
+
transformer_specification,
|
|
79
|
+
output,
|
|
80
|
+
output_format,
|
|
81
|
+
**kwargs,
|
|
82
|
+
):
|
|
83
|
+
"""
|
|
84
|
+
Map data from a source schema to a target schema using a transformation specification.
|
|
85
|
+
|
|
86
|
+
Example:
|
|
87
|
+
|
|
88
|
+
linkml-tr map-data -T X-to-Y-tr.yaml -s X.yaml X-data.yaml
|
|
89
|
+
"""
|
|
90
|
+
logging.info(f"Transforming {input} conforming to {schema} using {transformer_specification}")
|
|
91
|
+
tr = ObjectTransformer(**kwargs)
|
|
92
|
+
tr.source_schemaview = SchemaView(schema)
|
|
93
|
+
tr.load_transformer_specification(transformer_specification)
|
|
94
|
+
with open(input) as file:
|
|
95
|
+
input_obj = yaml.safe_load(file)
|
|
96
|
+
tr.index(input_obj, source_type)
|
|
97
|
+
tr_obj = tr.map_object(input_obj, source_type)
|
|
98
|
+
if output:
|
|
99
|
+
outfile = open(output, "w", encoding="utf-8")
|
|
100
|
+
else:
|
|
101
|
+
outfile = sys.stdout
|
|
102
|
+
outfile.write(yaml_dumper.dumps(tr_obj))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@main.command()
|
|
106
|
+
@output_option
|
|
107
|
+
@transformer_specification_option
|
|
108
|
+
@schema_option
|
|
109
|
+
@click.option("--source-type")
|
|
110
|
+
@click.option("--target", default="python", show_default=True, help="Target representation.")
|
|
111
|
+
def compile(
|
|
112
|
+
schema,
|
|
113
|
+
source_type,
|
|
114
|
+
transformer_specification,
|
|
115
|
+
target,
|
|
116
|
+
output,
|
|
117
|
+
**kwargs,
|
|
118
|
+
):
|
|
119
|
+
"""
|
|
120
|
+
Compiles a schema to another representation.
|
|
121
|
+
|
|
122
|
+
Example:
|
|
123
|
+
|
|
124
|
+
linkml-tr compile -T X-to-Y-tr.yaml -s X.yaml
|
|
125
|
+
"""
|
|
126
|
+
logging.info(f"Compiling {transformer_specification} with {schema}")
|
|
127
|
+
compiler_args = {}
|
|
128
|
+
if schema:
|
|
129
|
+
compiler_args["source_schemaview"] = SchemaView(schema)
|
|
130
|
+
if target == "python":
|
|
131
|
+
compiler = PythonCompiler(**compiler_args)
|
|
132
|
+
elif target == "markdown":
|
|
133
|
+
compiler = MarkdownCompiler(**compiler_args)
|
|
134
|
+
else:
|
|
135
|
+
raise NotImplementedError(f"Compiler {target} not implemented")
|
|
136
|
+
tr = ObjectTransformer()
|
|
137
|
+
tr.source_schemaview = SchemaView(schema)
|
|
138
|
+
tr.load_transformer_specification(transformer_specification)
|
|
139
|
+
result = compiler.compile(tr.specification)
|
|
140
|
+
print(result.serialization)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@main.command()
|
|
144
|
+
@output_option
|
|
145
|
+
@transformer_specification_option
|
|
146
|
+
@output_format_options
|
|
147
|
+
@click.argument("schema")
|
|
148
|
+
def derive_schema(schema, transformer_specification, output, output_format, **kwargs):
|
|
149
|
+
"""Derive a schema from a source schema and a transformation specification.
|
|
150
|
+
|
|
151
|
+
This can be thought of as "copying" the source to a target, using the transformation
|
|
152
|
+
specification as a "patch"
|
|
153
|
+
|
|
154
|
+
Notes:
|
|
155
|
+
|
|
156
|
+
the implementation is currently incomplete; the derived schema may not be valid
|
|
157
|
+
linkml, e.g. there may be "dangling" references.
|
|
158
|
+
|
|
159
|
+
Example:
|
|
160
|
+
|
|
161
|
+
linkml-tr derive-schema -T transform/personinfo-to-agent.transform.yaml source/personinfo.yaml
|
|
162
|
+
"""
|
|
163
|
+
logging.info(f"Transforming {schema} using {transformer_specification}")
|
|
164
|
+
tr = ObjectTransformer()
|
|
165
|
+
tr.load_transformer_specification(transformer_specification)
|
|
166
|
+
mapper = SchemaMapper(transformer=tr)
|
|
167
|
+
mapper.source_schemaview = SchemaView(schema)
|
|
168
|
+
target_schema = mapper.derive_schema()
|
|
169
|
+
if output:
|
|
170
|
+
outfile = open(output, "w", encoding="utf-8")
|
|
171
|
+
else:
|
|
172
|
+
outfile = sys.stdout
|
|
173
|
+
outfile.write(yaml_dumper.dumps(target_schema))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@main.command()
|
|
177
|
+
@output_option
|
|
178
|
+
@transformer_specification_option
|
|
179
|
+
@output_format_options
|
|
180
|
+
@click.option("--strict/--no-strict", default=True, show_default=True, help="Strict mode.")
|
|
181
|
+
@click.argument("schema")
|
|
182
|
+
def invert(schema, transformer_specification, output, output_format, **kwargs):
|
|
183
|
+
"""Invert a transformation specification.
|
|
184
|
+
|
|
185
|
+
Example:
|
|
186
|
+
|
|
187
|
+
linkml-tr invert -T transform/personinfo-to-agent.transform.yaml source/personinfo.yaml
|
|
188
|
+
"""
|
|
189
|
+
logging.info(f"Inverting {transformer_specification} using {schema} as source")
|
|
190
|
+
tr = ObjectTransformer()
|
|
191
|
+
tr.load_transformer_specification(transformer_specification)
|
|
192
|
+
inverter = TransformationSpecificationInverter(
|
|
193
|
+
source_schemaview=SchemaView(schema),
|
|
194
|
+
**kwargs,
|
|
195
|
+
)
|
|
196
|
+
inverted_spec = inverter.invert(tr.specification)
|
|
197
|
+
if output:
|
|
198
|
+
outfile = open(output, "w", encoding="utf-8")
|
|
199
|
+
else:
|
|
200
|
+
outfile = sys.stdout
|
|
201
|
+
outfile.write(yaml_dumper.dumps(inverted_spec))
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
if __name__ == "__main__":
|
|
205
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Compiles schemas into other transformation frameworks."""
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from linkml_map.compiler.compiler import CompiledSpecification, Compiler
|
|
2
|
+
from linkml_map.datamodel.transformer_model import TransformationSpecification
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AWKCompiler(Compiler):
|
|
6
|
+
"""
|
|
7
|
+
Compiles a Transformation Specification to an Awk script.
|
|
8
|
+
|
|
9
|
+
Note: this is only expected to work for flat schemas.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def compile(self, specification: TransformationSpecification) -> CompiledSpecification:
|
|
13
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Compilers are responsible for compiling a transformation specification into an alternative representation.
|
|
3
|
+
|
|
4
|
+
This is the opposite of an importer.
|
|
5
|
+
|
|
6
|
+
For example:
|
|
7
|
+
|
|
8
|
+
- LinkML-Transformer Specifications to R2RML
|
|
9
|
+
- LinkML-Transformer Specifications to awk scripts
|
|
10
|
+
- LinkML-Transformer Specifications to SQL
|
|
11
|
+
- LinkML-Transformer Specifications to Python (OO)
|
|
12
|
+
- LinkML-Transformer Specifications to Pandas
|
|
13
|
+
- LinkML-Transformer Specifications to Hamilton
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from abc import ABC
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from types import ModuleType
|
|
19
|
+
from typing import Iterator, Optional
|
|
20
|
+
|
|
21
|
+
from linkml_runtime import SchemaView
|
|
22
|
+
from linkml_runtime.dumpers import yaml_dumper
|
|
23
|
+
from linkml_runtime.utils.compile_python import compile_python
|
|
24
|
+
|
|
25
|
+
from linkml_map.datamodel.transformer_model import TransformationSpecification
|
|
26
|
+
from linkml_map.inference.schema_mapper import SchemaMapper
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class CompiledSpecification:
|
|
31
|
+
serialization: str = field(default="")
|
|
32
|
+
|
|
33
|
+
_module: Optional[ModuleType] = None
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def module(self) -> ModuleType:
|
|
37
|
+
if not self._module:
|
|
38
|
+
self._module = compile_python(self.serialization)
|
|
39
|
+
return self._module
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class Compiler(ABC):
|
|
44
|
+
"""
|
|
45
|
+
Base class for all compilers.
|
|
46
|
+
|
|
47
|
+
A compiler will compile a transformation specification into
|
|
48
|
+
an alternative representation.
|
|
49
|
+
|
|
50
|
+
An example compiler would be a R2RML compiler.
|
|
51
|
+
|
|
52
|
+
Note: Compilers and Importers will in general be implemented by providing
|
|
53
|
+
mapping specifications
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
source_schemaview: SchemaView = None
|
|
57
|
+
"""A view over the schema describing the source."""
|
|
58
|
+
|
|
59
|
+
source_python_module: str = None
|
|
60
|
+
"""The python module containing the source classes."""
|
|
61
|
+
|
|
62
|
+
target_python_module: str = None
|
|
63
|
+
"""The python module containing the target classes."""
|
|
64
|
+
|
|
65
|
+
def compile(self, specification: TransformationSpecification) -> CompiledSpecification:
|
|
66
|
+
"""
|
|
67
|
+
Transform source object into an instance of the target class.
|
|
68
|
+
|
|
69
|
+
:param specification:
|
|
70
|
+
:return:
|
|
71
|
+
"""
|
|
72
|
+
s = self._compile_header(specification)
|
|
73
|
+
for chunk in self._compile_iterator(specification):
|
|
74
|
+
s += chunk
|
|
75
|
+
return CompiledSpecification(serialization=s)
|
|
76
|
+
|
|
77
|
+
def _compile_header(self, specification: TransformationSpecification) -> str:
|
|
78
|
+
return ""
|
|
79
|
+
|
|
80
|
+
def _compile_iterator(self, specification: TransformationSpecification) -> Iterator[str]:
|
|
81
|
+
raise NotImplementedError
|
|
82
|
+
|
|
83
|
+
def derived_target_schemaview(self, specification: TransformationSpecification):
|
|
84
|
+
"""
|
|
85
|
+
Returns a view over the target schema, including any derived classes.
|
|
86
|
+
"""
|
|
87
|
+
mapper = SchemaMapper(source_schemaview=self.source_schemaview)
|
|
88
|
+
return SchemaView(yaml_dumper.dumps(mapper.derive_schema(specification)))
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import List, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
from graphviz import Digraph
|
|
6
|
+
from linkml_runtime import SchemaView
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from linkml_map.compiler.compiler import CompiledSpecification, Compiler
|
|
10
|
+
from linkml_map.datamodel.transformer_model import TransformationSpecification
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Record(BaseModel):
|
|
14
|
+
"""
|
|
15
|
+
A simplified representation of a class, UML-style.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
name: str
|
|
19
|
+
source: str
|
|
20
|
+
fields: List[Tuple[str, str]] = []
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def id(self):
|
|
24
|
+
return f"{self.source}{self.name}"
|
|
25
|
+
|
|
26
|
+
def __str__(self):
|
|
27
|
+
return (
|
|
28
|
+
f"""<
|
|
29
|
+
<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
|
|
30
|
+
<TR><TD><B>{self.name}</B></TD></TR>
|
|
31
|
+
"""
|
|
32
|
+
+ "".join(
|
|
33
|
+
[
|
|
34
|
+
f'<TR><TD SIDES="LRB" PORT="{f[0]}">{f[0]} : {f[1]}</TD></TR>'
|
|
35
|
+
for f in self.fields
|
|
36
|
+
]
|
|
37
|
+
)
|
|
38
|
+
+ """
|
|
39
|
+
</TABLE>>"""
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class GraphvizObject(CompiledSpecification):
|
|
45
|
+
digraph: Digraph = None
|
|
46
|
+
|
|
47
|
+
def render(self, file_path: str, format="png", view=False) -> None:
|
|
48
|
+
"""
|
|
49
|
+
Renders a graphviz graph to a file.
|
|
50
|
+
:param file_path:
|
|
51
|
+
:return:
|
|
52
|
+
"""
|
|
53
|
+
self.digraph.render(file_path, format=format, view=view)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class GraphvizCompiler(Compiler):
|
|
57
|
+
"""
|
|
58
|
+
Compiles a Transformation Specification to GraphViz.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def compile(
|
|
62
|
+
self, specification: TransformationSpecification, elements: Optional[List[str]] = None
|
|
63
|
+
) -> GraphvizObject:
|
|
64
|
+
dg = Digraph(comment="UML Class Diagram", format="png")
|
|
65
|
+
dg.attr(rankdir="LR") # Set graph direction from left to right
|
|
66
|
+
target_schemaview = self.derived_target_schemaview(specification)
|
|
67
|
+
source_schemaview = self.source_schemaview
|
|
68
|
+
|
|
69
|
+
records = []
|
|
70
|
+
records += self.add_records(source_schemaview, "source")
|
|
71
|
+
records += self.add_records(target_schemaview, "target")
|
|
72
|
+
|
|
73
|
+
for record in records:
|
|
74
|
+
dg.node(record.id, str(record), shape="plaintext")
|
|
75
|
+
|
|
76
|
+
# Define the class nodes with fields in UML format using HTML-like labels
|
|
77
|
+
# for precise control over the stacking of the fields
|
|
78
|
+
for target_cn, cd in specification.class_derivations.items():
|
|
79
|
+
source_cn = cd.populated_from
|
|
80
|
+
if source_cn is None:
|
|
81
|
+
source_cn = cd.name
|
|
82
|
+
if elements is not None and target_cn not in elements:
|
|
83
|
+
continue
|
|
84
|
+
source_record = Record(name=source_cn, source="source")
|
|
85
|
+
target_record = Record(name=target_cn, source="target")
|
|
86
|
+
for sd in cd.slot_derivations.values():
|
|
87
|
+
target_slot = sd.name
|
|
88
|
+
target_id = f"{target_record.id}:{target_slot}"
|
|
89
|
+
source_slot = sd.populated_from
|
|
90
|
+
if source_slot:
|
|
91
|
+
source_id = f"{source_record.id}:{source_slot}"
|
|
92
|
+
dg.edge(source_id, target_id)
|
|
93
|
+
elif sd.expr:
|
|
94
|
+
# TODO: do this in a less hacky way
|
|
95
|
+
tokens = re.findall(r"\w+", sd.expr)
|
|
96
|
+
for token in tokens:
|
|
97
|
+
if token not in source_schemaview.all_slots():
|
|
98
|
+
continue
|
|
99
|
+
dg.edge(f"{source_record.id}:{token}", target_id, style="dashed")
|
|
100
|
+
return GraphvizObject(digraph=dg, serialization=dg.source)
|
|
101
|
+
|
|
102
|
+
def add_records(self, schemaview: SchemaView, source: str) -> List[Record]:
|
|
103
|
+
records = []
|
|
104
|
+
for cn in schemaview.all_classes():
|
|
105
|
+
record = Record(name=cn, source=source)
|
|
106
|
+
for induced_slot in schemaview.class_induced_slots(cn):
|
|
107
|
+
record.fields.append((induced_slot.name, induced_slot.range))
|
|
108
|
+
records.append(record)
|
|
109
|
+
return records
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from jinja2 import Environment, FileSystemLoader
|
|
4
|
+
|
|
5
|
+
from linkml_map.compiler.compiler import CompiledSpecification, Compiler
|
|
6
|
+
from linkml_map.compiler.templates import TEMPLATE_DIR
|
|
7
|
+
from linkml_map.datamodel.transformer_model import TransformationSpecification
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class J2BasedCompiler(Compiler):
|
|
12
|
+
"""
|
|
13
|
+
Compiles a Transformation Specification using a Jinja2 template.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
template_dir: str = None
|
|
17
|
+
"""The directory containing the Jinja2 template."""
|
|
18
|
+
|
|
19
|
+
template_name: str = None
|
|
20
|
+
"""The name of the Jinja2 template."""
|
|
21
|
+
|
|
22
|
+
def compile(self, specification: TransformationSpecification) -> CompiledSpecification:
|
|
23
|
+
template_dir = self.template_dir
|
|
24
|
+
if not template_dir:
|
|
25
|
+
template_dir = TEMPLATE_DIR
|
|
26
|
+
if not template_dir:
|
|
27
|
+
raise ValueError("template_dir must be set")
|
|
28
|
+
loader = FileSystemLoader(template_dir)
|
|
29
|
+
env = Environment(loader=loader, autoescape=True)
|
|
30
|
+
if not self.template_name:
|
|
31
|
+
raise ValueError("template_name must be set")
|
|
32
|
+
template = env.get_template(self.template_name)
|
|
33
|
+
rendered = template.render(
|
|
34
|
+
spec=specification,
|
|
35
|
+
)
|
|
36
|
+
return CompiledSpecification(serialization=rendered)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from linkml_map.compiler.j2_based_compiler import J2BasedCompiler
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class MarkdownCompiler(J2BasedCompiler):
|
|
8
|
+
"""
|
|
9
|
+
Compiles a Transformation Specification to Markdown.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
template_name: str = "markdown.j2"
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from copy import deepcopy
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Iterator
|
|
4
|
+
|
|
5
|
+
from jinja2 import Template
|
|
6
|
+
|
|
7
|
+
from linkml_map.compiler.compiler import Compiler
|
|
8
|
+
from linkml_map.datamodel.transformer_model import (
|
|
9
|
+
ClassDerivation,
|
|
10
|
+
TransformationSpecification,
|
|
11
|
+
)
|
|
12
|
+
from linkml_map.inference.inference import induce_missing_values
|
|
13
|
+
|
|
14
|
+
CD_TEMPLATE = """
|
|
15
|
+
{% macro gen_slot_derivation_value(sd, var) -%}
|
|
16
|
+
{%- if sd.range -%}
|
|
17
|
+
derive_{{ sd.range }}({{ var }})
|
|
18
|
+
{%- else -%}
|
|
19
|
+
{%- if var is not none -%}
|
|
20
|
+
{{ var }}
|
|
21
|
+
{%- else -%}
|
|
22
|
+
None
|
|
23
|
+
{%- endif -%}
|
|
24
|
+
{%- endif -%}
|
|
25
|
+
{%- endmacro %}
|
|
26
|
+
{% macro gen_slot_derivation(sd, force_singlevalued=False) -%}
|
|
27
|
+
{%- if not force_singlevalued and sd.populated_from and induced_slots[sd.populated_from].multivalued -%}
|
|
28
|
+
[ {{ gen_slot_derivation_value(sd, "x") }} for x in {{ gen_slot_derivation(sd, force_singlevalued=True) }} ]
|
|
29
|
+
{%- else -%}
|
|
30
|
+
{%- if sd.populated_from -%}
|
|
31
|
+
source_object.{{ sd.populated_from }}
|
|
32
|
+
{%- elif sd.expr -%}
|
|
33
|
+
{%- if '\n' in sd.expr -%}
|
|
34
|
+
gen_{{ sd.name }}(source_object)
|
|
35
|
+
{%- elif '{' in sd.expr and '}' in sd.expr -%}
|
|
36
|
+
{{ sd.expr|replace('{', '')|replace('}', '') }}
|
|
37
|
+
{%- else -%}
|
|
38
|
+
{{ sd.expr }}
|
|
39
|
+
{%- endif -%}
|
|
40
|
+
{%- else -%}
|
|
41
|
+
None
|
|
42
|
+
{%- endif -%}
|
|
43
|
+
{%- endif -%}
|
|
44
|
+
{%- endmacro %}
|
|
45
|
+
{% macro gen_slot_derivation_defs(sd) -%}
|
|
46
|
+
{% if sd.expr and '\n' in sd.expr %}
|
|
47
|
+
|
|
48
|
+
def gen_{{ sd.name }}(src):
|
|
49
|
+
target = None
|
|
50
|
+
{%- for line in sd.expr.split('\n') %}
|
|
51
|
+
{{ line }}
|
|
52
|
+
{%- endfor -%}
|
|
53
|
+
return target
|
|
54
|
+
{% endif %}
|
|
55
|
+
{%- endmacro %}
|
|
56
|
+
def derive_{{ cd.name }}(
|
|
57
|
+
source_object: {{ source_module }}.{{ cd.populated_from }}
|
|
58
|
+
) -> {{ target_module }}.{{ cd.name }}:
|
|
59
|
+
# assign slots
|
|
60
|
+
{%- for slot in source_slots %}
|
|
61
|
+
{{ slot.name }} = source_object.{{ slot.name }}
|
|
62
|
+
{%- endfor %}
|
|
63
|
+
{%- for sd in cd.slot_derivations.values() -%}
|
|
64
|
+
{{ gen_slot_derivation_defs(sd) }}
|
|
65
|
+
{%- endfor %}
|
|
66
|
+
|
|
67
|
+
return tgt.{{ cd.name }}(
|
|
68
|
+
{%- for sd in cd.slot_derivations.values() %}
|
|
69
|
+
{{ sd.name }}={{ gen_slot_derivation(sd) }},
|
|
70
|
+
{%- endfor %}
|
|
71
|
+
)
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class PythonCompiler(Compiler):
|
|
77
|
+
"""
|
|
78
|
+
Compiles a Transformation Specification to Python code.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def _compile_header(self, specification: TransformationSpecification) -> str:
|
|
82
|
+
s = ""
|
|
83
|
+
if self.source_python_module:
|
|
84
|
+
s += f"import {self.source_python_module} as src\n"
|
|
85
|
+
if self.target_python_module:
|
|
86
|
+
s += f"import {self.target_python_module} as tgt\n"
|
|
87
|
+
s += "\nNULL = None\n\n"
|
|
88
|
+
return s
|
|
89
|
+
|
|
90
|
+
def _compile_iterator(self, specification: TransformationSpecification) -> Iterator[str]:
|
|
91
|
+
specification = deepcopy(specification)
|
|
92
|
+
induce_missing_values(specification, self.source_schemaview)
|
|
93
|
+
for cd in specification.class_derivations.values():
|
|
94
|
+
yield from self._compiled_class_derivations_iter(cd)
|
|
95
|
+
|
|
96
|
+
def _compiled_class_derivations_iter(self, cd: ClassDerivation) -> Iterator[str]:
|
|
97
|
+
sv = self.source_schemaview
|
|
98
|
+
if cd.populated_from:
|
|
99
|
+
populated_from = cd.populated_from
|
|
100
|
+
else:
|
|
101
|
+
populated_from = cd.name
|
|
102
|
+
if populated_from not in sv.all_classes():
|
|
103
|
+
return
|
|
104
|
+
induced_slots = {s.name: s for s in sv.class_induced_slots(populated_from)}
|
|
105
|
+
t = Template(CD_TEMPLATE)
|
|
106
|
+
yield t.render(
|
|
107
|
+
cd=cd,
|
|
108
|
+
source_module="src",
|
|
109
|
+
target_module="tgt",
|
|
110
|
+
induced_slots=induced_slots,
|
|
111
|
+
schemaview=sv,
|
|
112
|
+
source_slots=sv.class_induced_slots(populated_from),
|
|
113
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from linkml_map.compiler.compiler import CompiledSpecification, Compiler
|
|
2
|
+
from linkml_map.datamodel.transformer_model import TransformationSpecification
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class R2RMLCompiler(Compiler):
|
|
6
|
+
"""
|
|
7
|
+
Compiles a Transformation Specification to R2RML.
|
|
8
|
+
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def compile(self, specification: TransformationSpecification) -> CompiledSpecification:
|
|
12
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from linkml_map.compiler.compiler import CompiledSpecification, Compiler
|
|
2
|
+
from linkml_map.datamodel.transformer_model import TransformationSpecification
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SPARQLCompiler(Compiler):
|
|
6
|
+
"""
|
|
7
|
+
Compiles a Transformation Specification to SPARQL Construct.
|
|
8
|
+
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def compile(self, specification: TransformationSpecification) -> CompiledSpecification:
|
|
12
|
+
raise NotImplementedError
|