linkml-map 0.1.0__tar.gz
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-0.1.0/PKG-INFO +56 -0
- linkml_map-0.1.0/README.md +36 -0
- linkml_map-0.1.0/pyproject.toml +63 -0
- linkml_map-0.1.0/src/linkml_map/__init__.py +6 -0
- linkml_map-0.1.0/src/linkml_map/cli/__init__.py +0 -0
- linkml_map-0.1.0/src/linkml_map/cli/cli.py +205 -0
- linkml_map-0.1.0/src/linkml_map/compiler/__init__.py +1 -0
- linkml_map-0.1.0/src/linkml_map/compiler/awk_compiler.py +13 -0
- linkml_map-0.1.0/src/linkml_map/compiler/compiler.py +88 -0
- linkml_map-0.1.0/src/linkml_map/compiler/graphviz_compiler.py +109 -0
- linkml_map-0.1.0/src/linkml_map/compiler/j2_based_compiler.py +36 -0
- linkml_map-0.1.0/src/linkml_map/compiler/markdown_compiler.py +12 -0
- linkml_map-0.1.0/src/linkml_map/compiler/python_compiler.py +113 -0
- linkml_map-0.1.0/src/linkml_map/compiler/r2rml_compiler.py +12 -0
- linkml_map-0.1.0/src/linkml_map/compiler/sparql_compiler.py +12 -0
- linkml_map-0.1.0/src/linkml_map/compiler/sql_compiler.py +138 -0
- linkml_map-0.1.0/src/linkml_map/compiler/sssom_compiler.py +13 -0
- linkml_map-0.1.0/src/linkml_map/compiler/templates/__init__.py +3 -0
- linkml_map-0.1.0/src/linkml_map/compiler/templates/markdown.j2 +24 -0
- linkml_map-0.1.0/src/linkml_map/compiler/tr/__init__.py +4 -0
- linkml_map-0.1.0/src/linkml_map/compiler/tr/transformer_to_mapping_tables.tr.yaml +29 -0
- linkml_map-0.1.0/src/linkml_map/datamodel/__init__.py +4 -0
- linkml_map-0.1.0/src/linkml_map/datamodel/sssom.map.yaml +8 -0
- linkml_map-0.1.0/src/linkml_map/datamodel/transformer_model.py +425 -0
- linkml_map-0.1.0/src/linkml_map/datamodel/transformer_model.yaml +369 -0
- linkml_map-0.1.0/src/linkml_map/functions/__init__.py +0 -0
- linkml_map-0.1.0/src/linkml_map/functions/unit_conversion.py +157 -0
- linkml_map-0.1.0/src/linkml_map/importer/__init__.py +0 -0
- linkml_map-0.1.0/src/linkml_map/importer/importer.py +35 -0
- linkml_map-0.1.0/src/linkml_map/inference/__init__.py +0 -0
- linkml_map-0.1.0/src/linkml_map/inference/inference.py +38 -0
- linkml_map-0.1.0/src/linkml_map/inference/inverter.py +181 -0
- linkml_map-0.1.0/src/linkml_map/inference/schema_mapper.py +249 -0
- linkml_map-0.1.0/src/linkml_map/session.py +167 -0
- linkml_map-0.1.0/src/linkml_map/transformer/__init__.py +0 -0
- linkml_map-0.1.0/src/linkml_map/transformer/duckdb_transformer.py +70 -0
- linkml_map-0.1.0/src/linkml_map/transformer/object_transformer.py +368 -0
- linkml_map-0.1.0/src/linkml_map/transformer/transformer.py +269 -0
- linkml_map-0.1.0/src/linkml_map/utils/__init__.py +0 -0
- linkml_map-0.1.0/src/linkml_map/utils/dynamic_object.py +49 -0
- linkml_map-0.1.0/src/linkml_map/utils/eval_utils.py +207 -0
- linkml_map-0.1.0/src/linkml_map/utils/loaders.py +22 -0
- linkml_map-0.1.0/src/linkml_map/utils/multi_file_transformer.py +212 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: linkml-map
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary:
|
|
5
|
+
Author: cmungall
|
|
6
|
+
Author-email: cjm@berkeleybop.org
|
|
7
|
+
Requires-Python: >=3.9,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Requires-Dist: asteval (>=0.9.29,<0.10.0)
|
|
14
|
+
Requires-Dist: deepdiff (>=6.7.1,<7.0.0)
|
|
15
|
+
Requires-Dist: linkml-runtime (>=1.7.2)
|
|
16
|
+
Requires-Dist: pydantic (>=2.5.3,<3.0.0)
|
|
17
|
+
Requires-Dist: ucumvert (>=0.1.1,<0.2.0)
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# linkml-map
|
|
21
|
+
|
|
22
|
+
[](https://pypi.python.org/pypi/linkml-map)
|
|
23
|
+

|
|
24
|
+
[](https://pypi.python.org/pypi/linkml-map)
|
|
25
|
+
[](https://codecov.io/gh/linkml/linkml-map)
|
|
26
|
+
|
|
27
|
+
LinkML Map is a framework for specifying and executing mappings between data models.
|
|
28
|
+
|
|
29
|
+
Features:
|
|
30
|
+
|
|
31
|
+
- YAML-based lightweight syntax
|
|
32
|
+
- Python library for executing mappings on data files
|
|
33
|
+
- Ability to compile to other frameworks (forthcoming)
|
|
34
|
+
- Derivation of target (implicit) schemas, allowing easy customization of data models (*profiling*)
|
|
35
|
+
- Simple YAML dictionaries for simple mappings
|
|
36
|
+
- Automatic unit conversion
|
|
37
|
+
- Use of subset of Python to specify complex mappings
|
|
38
|
+
- Visualizations of mappings
|
|
39
|
+
- Mappings are reversible (provided all expressions used are reversible)
|
|
40
|
+
|
|
41
|
+
For full documentation see:
|
|
42
|
+
|
|
43
|
+
- [linkml.io/linkml-map/](https://linkml.io/linkml-map/)
|
|
44
|
+
|
|
45
|
+
Status:
|
|
46
|
+
|
|
47
|
+
The transformation data model is not yet fully stable, and may be subject to change.
|
|
48
|
+
Not all parts of the model are implemented in the reference Python framework.
|
|
49
|
+
|
|
50
|
+
## Quickstart
|
|
51
|
+
|
|
52
|
+
* [Tutorial Notebook](src/docs/examples/Tutorial.ipynb)
|
|
53
|
+
* [Generated Docs](https://linkml.github.io/linkml-map/)
|
|
54
|
+
* [Compliance Suite](https://linkml.github.io/linkml-map/specification/compliance)
|
|
55
|
+
|
|
56
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# linkml-map
|
|
2
|
+
|
|
3
|
+
[](https://pypi.python.org/pypi/linkml-map)
|
|
4
|
+

|
|
5
|
+
[](https://pypi.python.org/pypi/linkml-map)
|
|
6
|
+
[](https://codecov.io/gh/linkml/linkml-map)
|
|
7
|
+
|
|
8
|
+
LinkML Map is a framework for specifying and executing mappings between data models.
|
|
9
|
+
|
|
10
|
+
Features:
|
|
11
|
+
|
|
12
|
+
- YAML-based lightweight syntax
|
|
13
|
+
- Python library for executing mappings on data files
|
|
14
|
+
- Ability to compile to other frameworks (forthcoming)
|
|
15
|
+
- Derivation of target (implicit) schemas, allowing easy customization of data models (*profiling*)
|
|
16
|
+
- Simple YAML dictionaries for simple mappings
|
|
17
|
+
- Automatic unit conversion
|
|
18
|
+
- Use of subset of Python to specify complex mappings
|
|
19
|
+
- Visualizations of mappings
|
|
20
|
+
- Mappings are reversible (provided all expressions used are reversible)
|
|
21
|
+
|
|
22
|
+
For full documentation see:
|
|
23
|
+
|
|
24
|
+
- [linkml.io/linkml-map/](https://linkml.io/linkml-map/)
|
|
25
|
+
|
|
26
|
+
Status:
|
|
27
|
+
|
|
28
|
+
The transformation data model is not yet fully stable, and may be subject to change.
|
|
29
|
+
Not all parts of the model are implemented in the reference Python framework.
|
|
30
|
+
|
|
31
|
+
## Quickstart
|
|
32
|
+
|
|
33
|
+
* [Tutorial Notebook](src/docs/examples/Tutorial.ipynb)
|
|
34
|
+
* [Generated Docs](https://linkml.github.io/linkml-map/)
|
|
35
|
+
* [Compliance Suite](https://linkml.github.io/linkml-map/specification/compliance)
|
|
36
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "linkml-map"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = ""
|
|
5
|
+
authors = ["cmungall <cjm@berkeleybop.org>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
|
|
8
|
+
[tool.poetry.dependencies]
|
|
9
|
+
python = "^3.9"
|
|
10
|
+
linkml-runtime = ">=1.7.2"
|
|
11
|
+
asteval = "^0.9.29"
|
|
12
|
+
deepdiff = "^6.7.1"
|
|
13
|
+
pydantic = "^2.5.3"
|
|
14
|
+
ucumvert = "^0.1.1"
|
|
15
|
+
|
|
16
|
+
[tool.poetry.dev-dependencies]
|
|
17
|
+
pytest = "^7.3.1"
|
|
18
|
+
pytest-cov = "^4.0.0"
|
|
19
|
+
linkml = ">=1.7.0"
|
|
20
|
+
mkdocs-mermaid2-plugin = "^0.6.0"
|
|
21
|
+
mknotebooks = "^0.8.0"
|
|
22
|
+
mkdocs-windmill = "*"
|
|
23
|
+
tox = "*"
|
|
24
|
+
|
|
25
|
+
[tool.poetry.scripts]
|
|
26
|
+
linkml-map = "linkml_map.cli.cli:main"
|
|
27
|
+
|
|
28
|
+
[tool.poetry.group.dev.dependencies]
|
|
29
|
+
jupyter = "^1.0.0"
|
|
30
|
+
linkml = "^1.6.7"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
[tool.poetry.group.units.dependencies]
|
|
34
|
+
pint = "^0.23"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
[tool.poetry.group.graphviz.dependencies]
|
|
38
|
+
graphviz = "^0.20.1"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
[tool.poetry.group.duckdb.dependencies]
|
|
42
|
+
duckdb = "^0.10.1"
|
|
43
|
+
|
|
44
|
+
[build-system]
|
|
45
|
+
requires = ["poetry-core>=1.0.0"]
|
|
46
|
+
build-backend = "poetry.core.masonry.api"
|
|
47
|
+
|
|
48
|
+
[tool.black]
|
|
49
|
+
line-length = 100
|
|
50
|
+
target-version = ["py39", "py310", "py311"]
|
|
51
|
+
|
|
52
|
+
[tool.isort]
|
|
53
|
+
profile = "black"
|
|
54
|
+
multi_line_output = 3
|
|
55
|
+
include_trailing_comma = true
|
|
56
|
+
reverse_relative = true
|
|
57
|
+
|
|
58
|
+
# Ref: https://github.com/codespell-project/codespell#using-a-config-file
|
|
59
|
+
[tool.codespell]
|
|
60
|
+
skip = '.git,*.pdf,*.svg,go.sum,*.lock'
|
|
61
|
+
check-hidden = true
|
|
62
|
+
ignore-regex = '(^\s*"image/\S+": ".*|\b(KEGG.BRITE|mor.nlm.nih.gov)\b)'
|
|
63
|
+
ignore-words-list = 'infarction,amination'
|
|
File without changes
|
|
@@ -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"
|