linkml 1.8.4__tar.gz → 1.8.6__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-1.8.4 → linkml-1.8.6}/PKG-INFO +2 -2
- {linkml-1.8.4 → linkml-1.8.6}/linkml/cli/main.py +2 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/common/build.py +1 -2
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/common/ifabsent_processor.py +98 -21
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/common/lifecycle.py +18 -2
- linkml-1.8.6/linkml/generators/common/naming.py +106 -0
- linkml-1.8.6/linkml/generators/dbmlgen.py +173 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/docgen.py +16 -7
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/erdiagramgen.py +1 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/graphqlgen.py +34 -2
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/jsonldcontextgen.py +7 -1
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/jsonschemagen.py +73 -53
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/linkmlgen.py +13 -1
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/owlgen.py +11 -1
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/plantumlgen.py +17 -10
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/array.py +21 -61
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/template.py +12 -1
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/templates/attribute.py.jinja +1 -1
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/python/python_ifabsent_processor.py +1 -1
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pythongen.py +123 -21
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/shaclgen.py +16 -5
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/typescriptgen.py +3 -1
- {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/rules.py +3 -1
- {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/converter.py +17 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/deprecation.py +10 -0
- linkml-1.8.6/linkml/utils/helpers.py +81 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/validation.py +2 -1
- {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/__init__.py +2 -2
- {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/plugins/jsonschema_validation_plugin.py +1 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/report.py +4 -1
- {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/validator.py +4 -4
- {linkml-1.8.4 → linkml-1.8.6}/linkml/validators/jsonschemavalidator.py +10 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/validators/sparqlvalidator.py +7 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/workspaces/example_runner.py +20 -1
- {linkml-1.8.4 → linkml-1.8.6}/pyproject.toml +13 -11
- {linkml-1.8.4 → linkml-1.8.6}/setup.py +3 -2
- linkml-1.8.4/linkml/utils/helpers.py +0 -16
- {linkml-1.8.4 → linkml-1.8.6}/LICENSE +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/README.md +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/__init__.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/_version.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/cli/__init__.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/cli/__main__.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/PythonGenNotes.md +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/README.md +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/__init__.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/common/__init__.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/common/template.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/common/type_designators.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/csvgen.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/docgen/class.md.jinja2 +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/docgen/class_diagram.md.jinja2 +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/docgen/common_metadata.md.jinja2 +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/docgen/enum.md.jinja2 +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/docgen/index.md.jinja2 +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/docgen/index.tex.jinja2 +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/docgen/schema.md.jinja2 +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/docgen/slot.md.jinja2 +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/docgen/subset.md.jinja2 +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/docgen/type.md.jinja2 +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/dotgen.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/excelgen.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/golanggen.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/golrgen.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/javagen/example_template.java.jinja2 +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/javagen/java_record_template.jinja2 +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/javagen.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/jsonldgen.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/legacy/__init__.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/markdowngen.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/namespacegen.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/oocodegen.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/prefixmapgen.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/projectgen.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/protogen.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/__init__.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/black.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/build.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/includes.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/pydanticgen.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/templates/base_model.py.jinja +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/templates/class.py.jinja +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/templates/conditional_import.py.jinja +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/templates/enum.py.jinja +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/templates/footer.py.jinja +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/templates/imports.py.jinja +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/templates/module.py.jinja +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/templates/validator.py.jinja +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/python/__init__.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/rdfgen.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/shacl/__init__.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/shacl/shacl_data_type.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/shacl/shacl_ifabsent_processor.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/shexgen.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/sparqlgen.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/sqlalchemy/__init__.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/sqlalchemy/sqlalchemy_declarative_template.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/sqlalchemy/sqlalchemy_imperative_template.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/sqlalchemygen.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/sqltablegen.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/sssomgen.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/string_template.md +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/summarygen.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/terminusdbgen.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/yamlgen.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/yumlgen.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/__init__.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/cli.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/config/datamodel/.linkmllint.yaml +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/config/datamodel/__init__.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/config/datamodel/config.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/config/datamodel/config.yaml +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/config/default.yaml +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/config/recommended.yaml +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/formatters/__init__.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/formatters/formatter.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/formatters/json_formatter.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/formatters/markdown_formatter.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/formatters/terminal_formatter.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/formatters/tsv_formatter.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/linter.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/reporting/__init__.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/reporting/model.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/transformers/__init__.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/transformers/logical_model_transformer.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/transformers/model_transformer.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/transformers/relmodel_transformer.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/transformers/schema_renamer.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/__init__.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/cli_utils.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/datautils.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/datavalidator.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/exceptions.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/execute_tutorial.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/generator.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/logictools.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/mergeutils.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/rawloader.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/schema_builder.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/schema_fixer.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/schemaloader.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/schemasynopsis.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/sqlutils.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/typereferences.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/cli.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/loaders/__init__.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/loaders/delimited_file_loader.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/loaders/json_loader.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/loaders/loader.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/loaders/passthrough_loader.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/loaders/yaml_loader.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/plugins/__init__.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/plugins/pydantic_validation_plugin.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/plugins/recommended_slots_plugin.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/plugins/shacl_validation_plugin.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/plugins/validation_plugin.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/validation_context.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/validators/__init__.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/workspaces/__init__.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/workspaces/datamodel/__init__.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/workspaces/datamodel/workspaces.py +0 -0
- {linkml-1.8.4 → linkml-1.8.6}/linkml/workspaces/datamodel/workspaces.yaml +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: linkml
|
3
|
-
Version: 1.8.
|
3
|
+
Version: 1.8.6
|
4
4
|
Summary: Linked Open Data Modeling Language
|
5
5
|
Home-page: https://linkml.io/linkml/
|
6
6
|
Keywords: schema,linked data,data modeling,rdf,owl,biolink
|
@@ -53,7 +53,7 @@ Requires-Dist: pyyaml
|
|
53
53
|
Requires-Dist: rdflib (>=6.0.0)
|
54
54
|
Requires-Dist: requests (>=2.22)
|
55
55
|
Requires-Dist: sqlalchemy (>=1.4.31)
|
56
|
-
Requires-Dist: typing-extensions (>=4.
|
56
|
+
Requires-Dist: typing-extensions (>=4.6.0) ; python_version < "3.12"
|
57
57
|
Requires-Dist: watchdog (>=0.9.0)
|
58
58
|
Project-URL: Documentation, https://linkml.io/linkml/
|
59
59
|
Project-URL: Repository, https://github.com/linkml/linkml
|
@@ -8,6 +8,7 @@ import click
|
|
8
8
|
|
9
9
|
from linkml._version import __version__
|
10
10
|
from linkml.generators.csvgen import cli as gen_csv
|
11
|
+
from linkml.generators.dbmlgen import cli as gen_dbml
|
11
12
|
from linkml.generators.docgen import cli as gen_doc
|
12
13
|
from linkml.generators.dotgen import cli as gen_graphviz
|
13
14
|
from linkml.generators.erdiagramgen import cli as gen_erdiagram
|
@@ -125,6 +126,7 @@ generate.add_command(gen_project, name="project")
|
|
125
126
|
generate.add_command(gen_excel, name="excel")
|
126
127
|
generate.add_command(gen_sssom, name="sssom")
|
127
128
|
generate.add_command(gen_linkml, name="linkml")
|
129
|
+
generate.add_command(gen_dbml, name="dbml")
|
128
130
|
|
129
131
|
# Dev helpers
|
130
132
|
dev.add_command(run_tutorial, name="tutorial")
|
@@ -5,7 +5,6 @@ Models for intermediate build results
|
|
5
5
|
"""
|
6
6
|
|
7
7
|
import dataclasses
|
8
|
-
from abc import abstractmethod
|
9
8
|
from typing import Any, TypeVar
|
10
9
|
|
11
10
|
try:
|
@@ -57,11 +56,11 @@ class BuildResult(BaseModel):
|
|
57
56
|
|
58
57
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
59
58
|
|
60
|
-
@abstractmethod
|
61
59
|
def merge(self, other: T) -> T:
|
62
60
|
"""
|
63
61
|
Build results should have some means of merging results of a like kind
|
64
62
|
"""
|
63
|
+
raise NotImplementedError("This build result doesn't know how to merge!")
|
65
64
|
|
66
65
|
|
67
66
|
class SchemaResult(BuildResult):
|
@@ -1,7 +1,13 @@
|
|
1
1
|
import abc
|
2
2
|
import re
|
3
|
+
import sys
|
3
4
|
from abc import ABC
|
4
|
-
from typing import Any, Optional
|
5
|
+
from typing import Any, Optional, Type, Union
|
6
|
+
|
7
|
+
if sys.version_info < (3, 10):
|
8
|
+
from typing_extensions import TypeAlias
|
9
|
+
else:
|
10
|
+
from typing import TypeAlias
|
5
11
|
|
6
12
|
from linkml_runtime import SchemaView
|
7
13
|
from linkml_runtime.linkml_model import (
|
@@ -18,6 +24,7 @@ from linkml_runtime.linkml_model import (
|
|
18
24
|
String,
|
19
25
|
Time,
|
20
26
|
Uri,
|
27
|
+
types,
|
21
28
|
)
|
22
29
|
from linkml_runtime.linkml_model.types import (
|
23
30
|
Curie,
|
@@ -31,6 +38,34 @@ from linkml_runtime.linkml_model.types import (
|
|
31
38
|
Uriorcurie,
|
32
39
|
)
|
33
40
|
|
41
|
+
TYPES_TYPE: TypeAlias = Union[
|
42
|
+
Type[Boolean],
|
43
|
+
Type[Curie],
|
44
|
+
Type[Date],
|
45
|
+
Type[DateOrDatetime],
|
46
|
+
Type[Datetime],
|
47
|
+
Type[Decimal],
|
48
|
+
Type[Double],
|
49
|
+
Type[Float],
|
50
|
+
Type[Integer],
|
51
|
+
Type[Jsonpath],
|
52
|
+
Type[Jsonpointer],
|
53
|
+
Type[Ncname],
|
54
|
+
Type[Nodeidentifier],
|
55
|
+
Type[Objectidentifier],
|
56
|
+
Type[Sparqlpath],
|
57
|
+
Type[String],
|
58
|
+
Type[Time],
|
59
|
+
Type[Uri],
|
60
|
+
Type[Uriorcurie],
|
61
|
+
]
|
62
|
+
|
63
|
+
TYPES = [
|
64
|
+
t
|
65
|
+
for t in types.__dict__.values()
|
66
|
+
if isinstance(t, type) and t.__module__ == types.__name__ and hasattr(t, "type_name")
|
67
|
+
]
|
68
|
+
|
34
69
|
|
35
70
|
class IfAbsentProcessor(ABC):
|
36
71
|
"""
|
@@ -39,7 +74,7 @@ class IfAbsentProcessor(ABC):
|
|
39
74
|
See `<https://w3id.org/linkml/ifabsent>`_.
|
40
75
|
"""
|
41
76
|
|
42
|
-
ifabsent_regex = re.compile("""(?:(?P<type>\w+)\()?[\"\']?(?P<default_value>[^\(\)\"\')]*)[\"\']?\)?""")
|
77
|
+
ifabsent_regex = re.compile(r"""(?:(?P<type>\w+)\()?[\"\']?(?P<default_value>[^\(\)\"\')]*)[\"\']?\)?""")
|
43
78
|
|
44
79
|
def __init__(self, schema_view: SchemaView):
|
45
80
|
self.schema_view = schema_view
|
@@ -61,10 +96,12 @@ class IfAbsentProcessor(ABC):
|
|
61
96
|
if mapped:
|
62
97
|
return custom_default_value
|
63
98
|
|
64
|
-
|
99
|
+
base_type = self._base_type(slot.range)
|
100
|
+
|
101
|
+
if base_type is String:
|
65
102
|
return self.map_string_default_value(ifabsent_default_value, slot, cls)
|
66
103
|
|
67
|
-
if
|
104
|
+
if base_type is Boolean:
|
68
105
|
if re.match(r"^[Tt]rue$", ifabsent_default_value):
|
69
106
|
return self.map_boolean_true_default_value(slot, cls)
|
70
107
|
elif re.match(r"^[Ff]alse$", ifabsent_default_value):
|
@@ -75,19 +112,19 @@ class IfAbsentProcessor(ABC):
|
|
75
112
|
f"value"
|
76
113
|
)
|
77
114
|
|
78
|
-
if
|
115
|
+
if base_type is Integer:
|
79
116
|
return self.map_integer_default_value(ifabsent_default_value, slot, cls)
|
80
117
|
|
81
|
-
if
|
118
|
+
if base_type is Float:
|
82
119
|
return self.map_float_default_value(ifabsent_default_value, slot, cls)
|
83
120
|
|
84
|
-
if
|
121
|
+
if base_type is Double:
|
85
122
|
return self.map_double_default_value(ifabsent_default_value, slot, cls)
|
86
123
|
|
87
|
-
if
|
124
|
+
if base_type is Decimal:
|
88
125
|
return self.map_decimal_default_value(ifabsent_default_value, slot, cls)
|
89
126
|
|
90
|
-
if
|
127
|
+
if base_type is Time:
|
91
128
|
match = re.match(r"^(\d{2}):(\d{2}):(\d{2}).*$", ifabsent_default_value)
|
92
129
|
if match:
|
93
130
|
return self.map_time_default_value(match[1], match[2], match[3], slot, cls)
|
@@ -97,7 +134,7 @@ class IfAbsentProcessor(ABC):
|
|
97
134
|
)
|
98
135
|
|
99
136
|
# TODO manage timezones and offsets
|
100
|
-
if
|
137
|
+
if base_type is Date:
|
101
138
|
match = re.match(r"^(\d{4})-(\d{2})-(\d{2})$", ifabsent_default_value)
|
102
139
|
if match:
|
103
140
|
return self.map_date_default_value(match[1], match[2], match[3], slot, cls)
|
@@ -107,7 +144,7 @@ class IfAbsentProcessor(ABC):
|
|
107
144
|
)
|
108
145
|
|
109
146
|
# TODO manage timezones and offsets
|
110
|
-
if
|
147
|
+
if base_type is Datetime:
|
111
148
|
match = re.match(r"^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}).*$", ifabsent_default_value)
|
112
149
|
if match:
|
113
150
|
return self.map_datetime_default_value(
|
@@ -120,7 +157,7 @@ class IfAbsentProcessor(ABC):
|
|
120
157
|
)
|
121
158
|
|
122
159
|
# TODO manage timezones and offsets
|
123
|
-
if
|
160
|
+
if base_type is DateOrDatetime:
|
124
161
|
match = re.match(r"^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2}):(\d{2}))?.*$", ifabsent_default_value)
|
125
162
|
if match and (match[4] is None or match[5] is None or match[6] is None):
|
126
163
|
return self.map_date_default_value(match[1], match[2], match[3], slot, cls)
|
@@ -134,31 +171,31 @@ class IfAbsentProcessor(ABC):
|
|
134
171
|
f"datetime value"
|
135
172
|
)
|
136
173
|
|
137
|
-
if
|
174
|
+
if base_type is Uri:
|
138
175
|
return self.map_uri_default_value(ifabsent_default_value, slot, cls)
|
139
176
|
|
140
|
-
if
|
177
|
+
if base_type is Curie:
|
141
178
|
return self.map_curie_default_value(ifabsent_default_value, slot, cls)
|
142
179
|
|
143
|
-
if
|
180
|
+
if base_type is Uriorcurie:
|
144
181
|
return self.map_uri_or_curie_default_value(ifabsent_default_value, slot, cls)
|
145
182
|
|
146
|
-
if
|
183
|
+
if base_type is Ncname:
|
147
184
|
return self.map_nc_name_default_value(ifabsent_default_value, slot, cls)
|
148
185
|
|
149
|
-
if
|
186
|
+
if base_type is Objectidentifier:
|
150
187
|
return self.map_object_identifier_default_value(ifabsent_default_value, slot, cls)
|
151
188
|
|
152
|
-
if
|
189
|
+
if base_type is Nodeidentifier:
|
153
190
|
return self.map_node_identifier_default_value(ifabsent_default_value, slot, cls)
|
154
191
|
|
155
|
-
if
|
192
|
+
if base_type is Jsonpointer:
|
156
193
|
return self.map_json_pointer_default_value(ifabsent_default_value, slot, cls)
|
157
194
|
|
158
|
-
if
|
195
|
+
if base_type is Jsonpath:
|
159
196
|
return self.map_json_path_default_value(ifabsent_default_value, slot, cls)
|
160
197
|
|
161
|
-
if
|
198
|
+
if base_type is Sparqlpath:
|
162
199
|
return self.map_sparql_path_default_value(ifabsent_default_value, slot, cls)
|
163
200
|
|
164
201
|
# -----------------------
|
@@ -173,6 +210,46 @@ class IfAbsentProcessor(ABC):
|
|
173
210
|
|
174
211
|
raise ValueError(f"The ifabsent value `{slot.ifabsent}` of the `{slot.name}` slot could not be processed")
|
175
212
|
|
213
|
+
def _base_type(self, range_: str) -> Optional[TYPES_TYPE]:
|
214
|
+
"""
|
215
|
+
Find the linkml base type that corresponds to either a matching type_name or custom type
|
216
|
+
|
217
|
+
First check for an explicit match of the range == TypeDefinition.type_name
|
218
|
+
Then check for explicit inheritance via typeof
|
219
|
+
Finally check for implicit matching via matching base
|
220
|
+
|
221
|
+
Don't raise here, just return None in case another method of resolution like enums are
|
222
|
+
available
|
223
|
+
"""
|
224
|
+
# first check for matching type using type_name - ie. range is already a base type
|
225
|
+
|
226
|
+
for typ in TYPES:
|
227
|
+
if range_ == typ.type_name:
|
228
|
+
return typ
|
229
|
+
|
230
|
+
# if we're not a type, return None to indicate that, e.g. if an enum's permissible_value
|
231
|
+
if range_ not in self.schema_view.all_types(imports=True):
|
232
|
+
return
|
233
|
+
|
234
|
+
# then check explicit descendents of types
|
235
|
+
# base types do not inherit from one another, so the last ancestor is always a base type
|
236
|
+
# if it is inheriting from a base type
|
237
|
+
ancestor = self.schema_view.type_ancestors(range_)[-1]
|
238
|
+
for typ in TYPES:
|
239
|
+
if ancestor == typ.type_name:
|
240
|
+
return typ
|
241
|
+
|
242
|
+
# finally check if we have a matching base
|
243
|
+
induced_typ = self.schema_view.induced_type(range_)
|
244
|
+
if induced_typ.repr is None and induced_typ.base is None:
|
245
|
+
return None
|
246
|
+
for typ in TYPES:
|
247
|
+
# types always inherit from a single type, and that type is their base
|
248
|
+
# our range can match it with repr or base
|
249
|
+
typ_base = typ.__mro__[1].__name__
|
250
|
+
if typ_base == induced_typ.base:
|
251
|
+
return typ
|
252
|
+
|
176
253
|
@abc.abstractmethod
|
177
254
|
def map_custom_default_values(self, default_value: str, slot: SlotDefinition, cls: ClassDefinition) -> (bool, str):
|
178
255
|
"""
|
@@ -13,7 +13,7 @@ from linkml_runtime.linkml_model.meta import (
|
|
13
13
|
TypeDefinition,
|
14
14
|
)
|
15
15
|
|
16
|
-
from linkml.generators.common.build import ClassResult, RangeResult, SchemaResult, SlotResult, TypeResult
|
16
|
+
from linkml.generators.common.build import ClassResult, EnumResult, RangeResult, SchemaResult, SlotResult, TypeResult
|
17
17
|
from linkml.generators.common.template import TemplateModel
|
18
18
|
|
19
19
|
TSchema = TypeVar("TSchema", bound=SchemaResult)
|
@@ -21,7 +21,7 @@ TClass = TypeVar("TClass", bound=ClassResult)
|
|
21
21
|
TSlot = TypeVar("TSlot", bound=SlotResult)
|
22
22
|
TRange = TypeVar("TRange", bound=RangeResult)
|
23
23
|
TType = TypeVar("TType", bound=TypeResult)
|
24
|
-
TEnum = TypeVar("TEnum", bound=
|
24
|
+
TEnum = TypeVar("TEnum", bound=EnumResult)
|
25
25
|
TTemplate = TypeVar("TTemplate", bound=TemplateModel)
|
26
26
|
|
27
27
|
|
@@ -93,6 +93,22 @@ class LifecycleMixin:
|
|
93
93
|
def after_generate_slots(self, slot: Iterable[TSlot], sv: SchemaView) -> Iterable[TSlot]:
|
94
94
|
return slot
|
95
95
|
|
96
|
+
def before_generate_class_slot(self, slot: SlotDefinition, cls: ClassDefinition, sv: SchemaView) -> SlotDefinition:
|
97
|
+
return slot
|
98
|
+
|
99
|
+
def after_generate_class_slot(self, slot: TSlot, cls: ClassDefinition, sv: SchemaView) -> TSlot:
|
100
|
+
return slot
|
101
|
+
|
102
|
+
def before_generate_class_slots(
|
103
|
+
self, slot: Iterable[SlotDefinition], cls: ClassDefinition, sv: SchemaView
|
104
|
+
) -> Iterable[SlotDefinition]:
|
105
|
+
return slot
|
106
|
+
|
107
|
+
def after_generate_class_slots(
|
108
|
+
self, slot: Iterable[TSlot], cls: ClassDefinition, sv: SchemaView
|
109
|
+
) -> Iterable[TSlot]:
|
110
|
+
return slot
|
111
|
+
|
96
112
|
def before_generate_type(self, typ: TypeDefinition, sv: SchemaView) -> TypeDefinition:
|
97
113
|
return typ
|
98
114
|
|
@@ -0,0 +1,106 @@
|
|
1
|
+
import logging
|
2
|
+
import re
|
3
|
+
import unicodedata
|
4
|
+
from enum import Enum
|
5
|
+
|
6
|
+
|
7
|
+
class NamingProfiles(str, Enum):
|
8
|
+
# GraphQL naming profile ensures compatibility with the GraphQL specification
|
9
|
+
# WRT names: https://spec.graphql.org/October2021/#Name
|
10
|
+
graphql = "graphql"
|
11
|
+
|
12
|
+
|
13
|
+
class NameCompatibility(object):
|
14
|
+
"""Make a name compatible to the given profile"""
|
15
|
+
|
16
|
+
# heading double underscores and digit reserved to names starting with a digit
|
17
|
+
re_reserved_heading_digit = re.compile("^__[0-9]")
|
18
|
+
# valid name between double underscores is reserved for unicode name transformations
|
19
|
+
re_reserved_unicode_name_transformation = re.compile("__[0-9a-zA-Z][0-9a-zA-Z_]*__")
|
20
|
+
# something like '__U_xxxx_' is reserved for unicode code transformations
|
21
|
+
re_reserved_unicode_code_transformation = re.compile("__U_[0-9a-eA-E]{4}_")
|
22
|
+
# strings starting with a digit
|
23
|
+
re_heading_digit = re.compile("^[0-9]")
|
24
|
+
# character that is neither alphanumeric nor underscore
|
25
|
+
re_no_alphanum_underscore = re.compile("[^0-9a-zA-Z_]")
|
26
|
+
|
27
|
+
def __init__(self, profile: NamingProfiles, do_not_fix: bool = False):
|
28
|
+
"""Specify the naming policy on instantiation"""
|
29
|
+
self.profile = profile
|
30
|
+
self.do_not_fix = do_not_fix
|
31
|
+
|
32
|
+
def _no_heading_digits(self, input: str) -> str:
|
33
|
+
"""Ensure name does not start with a heading digit"""
|
34
|
+
output = input
|
35
|
+
if self.re_heading_digit.match(input):
|
36
|
+
if self.do_not_fix:
|
37
|
+
raise ValueError(f"Name '{input}' starts with digit (illegal GraphQL) and will not be fixed!")
|
38
|
+
else:
|
39
|
+
logging.warning(
|
40
|
+
f"Name '{input}' starts with digit (illegal GraphQL), "
|
41
|
+
+ f"it has been prefixed with '__', resulting in {output}"
|
42
|
+
)
|
43
|
+
output = f"__{input}"
|
44
|
+
return output
|
45
|
+
|
46
|
+
def _transform_char(self, char: str) -> str:
|
47
|
+
"""Transform unsupported characters"""
|
48
|
+
if len(char) != 1:
|
49
|
+
raise Exception(f"Single character expected, but got '{char}'!!")
|
50
|
+
# replace whitespaces with underscores
|
51
|
+
# the transformation cannot be inverted, but is a well-established
|
52
|
+
# and expected transformation
|
53
|
+
if char == " ":
|
54
|
+
return "_"
|
55
|
+
# try to use names for ASCII characters
|
56
|
+
if ord(char) < 128:
|
57
|
+
try:
|
58
|
+
# unicodedata.lookup should be able to invert the transformation
|
59
|
+
return f"__{unicodedata.name(char).replace(' ', '_').replace('-', '_')}__"
|
60
|
+
except ValueError:
|
61
|
+
pass
|
62
|
+
# fallback to code-transformation if none of the previous has worked
|
63
|
+
return f"__U_{ord(char):04X}_"
|
64
|
+
|
65
|
+
def _only_alphanum_underscore(self, input: str) -> str:
|
66
|
+
"""Ensure name does not contain any unsupported characters"""
|
67
|
+
output = input
|
68
|
+
# with re.split and re.findall we get in the same order and separated in two arrays
|
69
|
+
# the substrings between special characters and the special characters
|
70
|
+
no_alphanum_underscore_match = self.re_no_alphanum_underscore.findall(input)
|
71
|
+
if no_alphanum_underscore_match:
|
72
|
+
if self.do_not_fix:
|
73
|
+
raise ValueError(f"Name '{input}' contains a character illegal in GraphQL and will not be fixed!")
|
74
|
+
else:
|
75
|
+
logging.warning(
|
76
|
+
f"Name '{input}' contains a character illegal in GraphQL, "
|
77
|
+
+ f"the resulting encoded name is {output}"
|
78
|
+
)
|
79
|
+
to_keep = self.re_no_alphanum_underscore.split(input)
|
80
|
+
# first comes first substring to keep
|
81
|
+
output = to_keep[0]
|
82
|
+
# each char replacement is followed by the next substring to keep
|
83
|
+
for offset in range(0, len(to_keep) - 1):
|
84
|
+
output = output + self._transform_char(no_alphanum_underscore_match[offset])
|
85
|
+
output = output + to_keep[offset + 1]
|
86
|
+
return output
|
87
|
+
|
88
|
+
def _graphql_compatibility(self, input: str) -> str:
|
89
|
+
"""Ensure name compatibility with GraphQL name policies"""
|
90
|
+
# as of now, some (hopefully) very rare patterns are reserved to mark transformations
|
91
|
+
if self.re_reserved_heading_digit.match(input):
|
92
|
+
raise NotImplementedError("Names starting with a double underscore followed by a digit are not supported!!")
|
93
|
+
if self.re_reserved_unicode_name_transformation.match(input):
|
94
|
+
raise NotImplementedError("Names containing valid names between double underscores are not supported!!")
|
95
|
+
if self.re_reserved_unicode_code_transformation.match(input):
|
96
|
+
raise NotImplementedError("Names containing strings like '__U_xxxx_' are not supported!!")
|
97
|
+
# apply transformation
|
98
|
+
output = input
|
99
|
+
output = self._no_heading_digits(output)
|
100
|
+
output = self._only_alphanum_underscore(output)
|
101
|
+
return output
|
102
|
+
|
103
|
+
def compatible(self, input: str) -> str:
|
104
|
+
"""Make given name compatible with the given naming policy."""
|
105
|
+
if self.profile == "graphql":
|
106
|
+
return self._graphql_compatibility(input)
|
@@ -0,0 +1,173 @@
|
|
1
|
+
import logging
|
2
|
+
|
3
|
+
import click
|
4
|
+
from linkml_runtime.utils.formatutils import camelcase, underscore
|
5
|
+
from linkml_runtime.utils.schemaview import SchemaView
|
6
|
+
|
7
|
+
from linkml.utils.generator import Generator
|
8
|
+
|
9
|
+
|
10
|
+
def _map_range_to_dbml_type(range_name: str) -> str:
|
11
|
+
"""
|
12
|
+
Map LinkML range types to DBML types.
|
13
|
+
|
14
|
+
:param range_name: LinkML range name
|
15
|
+
:return: Corresponding DBML type
|
16
|
+
"""
|
17
|
+
type_mapping = {
|
18
|
+
"string": "varchar",
|
19
|
+
"integer": "int",
|
20
|
+
"float": "float",
|
21
|
+
"boolean": "boolean",
|
22
|
+
"date": "date",
|
23
|
+
"datetime": "datetime",
|
24
|
+
}
|
25
|
+
return type_mapping.get(range_name, "varchar") # Default to varchar
|
26
|
+
|
27
|
+
|
28
|
+
class DBMLGenerator(Generator):
|
29
|
+
"""
|
30
|
+
A generator for converting a LinkML schema into DBML (Database Markup Language).
|
31
|
+
"""
|
32
|
+
|
33
|
+
generatorname = "dbmlgen"
|
34
|
+
generatorversion = "0.2.0"
|
35
|
+
valid_formats = ["dbml"]
|
36
|
+
|
37
|
+
def __post_init__(self) -> None:
|
38
|
+
super().__post_init__()
|
39
|
+
self.logger = logging.getLogger(__name__)
|
40
|
+
self.schemaview = SchemaView(self.schema)
|
41
|
+
|
42
|
+
def serialize(self) -> str:
|
43
|
+
"""
|
44
|
+
Generate DBML representation of the LinkML schema.
|
45
|
+
|
46
|
+
:return: DBML as a string
|
47
|
+
"""
|
48
|
+
dbml_lines = [
|
49
|
+
"// DBML generated from LinkML schema\n",
|
50
|
+
f"Project {{\n name: '{self.schemaview.schema.name}'\n}}\n",
|
51
|
+
]
|
52
|
+
|
53
|
+
for class_name, class_def in self.schemaview.all_classes().items():
|
54
|
+
dbml_lines.append(self._generate_table(class_name, class_def))
|
55
|
+
|
56
|
+
# Generate relationships if applicable
|
57
|
+
relationships = self._generate_relationships()
|
58
|
+
if relationships:
|
59
|
+
dbml_lines.append(relationships)
|
60
|
+
|
61
|
+
return "\n".join(dbml_lines)
|
62
|
+
|
63
|
+
def _generate_table(self, class_name: str, class_def) -> str:
|
64
|
+
"""
|
65
|
+
Generate the DBML for a single class (table).
|
66
|
+
|
67
|
+
:param class_name: Name of the class
|
68
|
+
:param class_def: ClassDefinition object
|
69
|
+
:return: DBML representation of the class
|
70
|
+
"""
|
71
|
+
dbml = [f"Table {camelcase(class_name)} {{"]
|
72
|
+
|
73
|
+
for slot_name in self.schemaview.class_induced_slots(class_name):
|
74
|
+
slot = self.schemaview.get_slot(slot_name.name)
|
75
|
+
dbml.append(self._generate_column(slot))
|
76
|
+
|
77
|
+
dbml.append("}\n")
|
78
|
+
return "\n".join(dbml)
|
79
|
+
|
80
|
+
def _generate_column(self, slot) -> str:
|
81
|
+
"""
|
82
|
+
Generate the DBML for a single slot (column).
|
83
|
+
|
84
|
+
:param slot: SlotDefinition object
|
85
|
+
:return: DBML representation of the column
|
86
|
+
"""
|
87
|
+
column_name = slot.name
|
88
|
+
data_type = _map_range_to_dbml_type(slot.range or "string")
|
89
|
+
constraints = []
|
90
|
+
constraints_str = ""
|
91
|
+
|
92
|
+
if slot.required:
|
93
|
+
constraints.append("not null")
|
94
|
+
if slot.identifier:
|
95
|
+
constraints.append("primary key")
|
96
|
+
|
97
|
+
if constraints:
|
98
|
+
constraints_str = f"{', '.join(constraints)}"
|
99
|
+
constraints_str = "[" + constraints_str + "]"
|
100
|
+
|
101
|
+
return f" {underscore(column_name)} {data_type} {constraints_str}".strip()
|
102
|
+
|
103
|
+
def _generate_relationships(self) -> str:
|
104
|
+
"""
|
105
|
+
Generate DBML relationships based on slot ranges referencing other classes.
|
106
|
+
|
107
|
+
:return: DBML representation of relationships
|
108
|
+
"""
|
109
|
+
relationships = []
|
110
|
+
for class_name, class_def in self.schemaview.all_classes().items():
|
111
|
+
for slot_name in self.schemaview.class_induced_slots(class_name):
|
112
|
+
slot = self.schemaview.get_slot(slot_name.name)
|
113
|
+
|
114
|
+
# Check if the slot references another class
|
115
|
+
if slot.range in self.schemaview.all_classes():
|
116
|
+
|
117
|
+
# Find the identifier slot of the referenced class
|
118
|
+
identifier_slot_name = next(
|
119
|
+
(
|
120
|
+
slot_name.name
|
121
|
+
for slot_name in self.schemaview.class_induced_slots(slot.range)
|
122
|
+
if self.schemaview.get_slot(slot_name.name).identifier
|
123
|
+
),
|
124
|
+
None,
|
125
|
+
)
|
126
|
+
|
127
|
+
if identifier_slot_name is None:
|
128
|
+
raise ValueError(f"Referenced class '{slot.range}' does not have an identifier slot.")
|
129
|
+
|
130
|
+
# Generate the DBML relationship
|
131
|
+
relationships.append(
|
132
|
+
f"Ref: {camelcase(class_name)}.{underscore(slot.name)} > "
|
133
|
+
f"{camelcase(slot.range)}.{underscore(identifier_slot_name)}"
|
134
|
+
)
|
135
|
+
return "\n".join(relationships)
|
136
|
+
|
137
|
+
|
138
|
+
# CLI Definition
|
139
|
+
@click.command()
|
140
|
+
@click.option(
|
141
|
+
"--schema",
|
142
|
+
"-s",
|
143
|
+
required=True,
|
144
|
+
type=click.Path(exists=True, dir_okay=False, file_okay=True),
|
145
|
+
help="Path to the LinkML schema YAML file",
|
146
|
+
)
|
147
|
+
@click.option(
|
148
|
+
"--output",
|
149
|
+
"-o",
|
150
|
+
required=False,
|
151
|
+
type=click.Path(dir_okay=False, writable=True),
|
152
|
+
help="Path to save the generated DBML file. If not specified, DBML will be printed to stdout.",
|
153
|
+
)
|
154
|
+
def cli(schema, output):
|
155
|
+
"""
|
156
|
+
CLI for LinkML to DBML generator.
|
157
|
+
"""
|
158
|
+
generator = DBMLGenerator(schema)
|
159
|
+
|
160
|
+
# Generate the DBML
|
161
|
+
dbml_output = generator.serialize()
|
162
|
+
|
163
|
+
# Save to file or print to stdout
|
164
|
+
if output:
|
165
|
+
with open(output, "w", encoding="utf-8") as f:
|
166
|
+
f.write(dbml_output)
|
167
|
+
click.echo(f"DBML has been saved to {output}")
|
168
|
+
else:
|
169
|
+
click.echo(dbml_output)
|
170
|
+
|
171
|
+
|
172
|
+
if __name__ == "__main__":
|
173
|
+
cli()
|