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.
Files changed (162) hide show
  1. {linkml-1.8.4 → linkml-1.8.6}/PKG-INFO +2 -2
  2. {linkml-1.8.4 → linkml-1.8.6}/linkml/cli/main.py +2 -0
  3. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/common/build.py +1 -2
  4. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/common/ifabsent_processor.py +98 -21
  5. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/common/lifecycle.py +18 -2
  6. linkml-1.8.6/linkml/generators/common/naming.py +106 -0
  7. linkml-1.8.6/linkml/generators/dbmlgen.py +173 -0
  8. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/docgen.py +16 -7
  9. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/erdiagramgen.py +1 -0
  10. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/graphqlgen.py +34 -2
  11. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/jsonldcontextgen.py +7 -1
  12. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/jsonschemagen.py +73 -53
  13. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/linkmlgen.py +13 -1
  14. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/owlgen.py +11 -1
  15. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/plantumlgen.py +17 -10
  16. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/array.py +21 -61
  17. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/template.py +12 -1
  18. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/templates/attribute.py.jinja +1 -1
  19. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/python/python_ifabsent_processor.py +1 -1
  20. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pythongen.py +123 -21
  21. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/shaclgen.py +16 -5
  22. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/typescriptgen.py +3 -1
  23. {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/rules.py +3 -1
  24. {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/converter.py +17 -0
  25. {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/deprecation.py +10 -0
  26. linkml-1.8.6/linkml/utils/helpers.py +81 -0
  27. {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/validation.py +2 -1
  28. {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/__init__.py +2 -2
  29. {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/plugins/jsonschema_validation_plugin.py +1 -0
  30. {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/report.py +4 -1
  31. {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/validator.py +4 -4
  32. {linkml-1.8.4 → linkml-1.8.6}/linkml/validators/jsonschemavalidator.py +10 -0
  33. {linkml-1.8.4 → linkml-1.8.6}/linkml/validators/sparqlvalidator.py +7 -0
  34. {linkml-1.8.4 → linkml-1.8.6}/linkml/workspaces/example_runner.py +20 -1
  35. {linkml-1.8.4 → linkml-1.8.6}/pyproject.toml +13 -11
  36. {linkml-1.8.4 → linkml-1.8.6}/setup.py +3 -2
  37. linkml-1.8.4/linkml/utils/helpers.py +0 -16
  38. {linkml-1.8.4 → linkml-1.8.6}/LICENSE +0 -0
  39. {linkml-1.8.4 → linkml-1.8.6}/README.md +0 -0
  40. {linkml-1.8.4 → linkml-1.8.6}/linkml/__init__.py +0 -0
  41. {linkml-1.8.4 → linkml-1.8.6}/linkml/_version.py +0 -0
  42. {linkml-1.8.4 → linkml-1.8.6}/linkml/cli/__init__.py +0 -0
  43. {linkml-1.8.4 → linkml-1.8.6}/linkml/cli/__main__.py +0 -0
  44. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/PythonGenNotes.md +0 -0
  45. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/README.md +0 -0
  46. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/__init__.py +0 -0
  47. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/common/__init__.py +0 -0
  48. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/common/template.py +0 -0
  49. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/common/type_designators.py +0 -0
  50. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/csvgen.py +0 -0
  51. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/docgen/class.md.jinja2 +0 -0
  52. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/docgen/class_diagram.md.jinja2 +0 -0
  53. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/docgen/common_metadata.md.jinja2 +0 -0
  54. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/docgen/enum.md.jinja2 +0 -0
  55. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/docgen/index.md.jinja2 +0 -0
  56. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/docgen/index.tex.jinja2 +0 -0
  57. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/docgen/schema.md.jinja2 +0 -0
  58. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/docgen/slot.md.jinja2 +0 -0
  59. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/docgen/subset.md.jinja2 +0 -0
  60. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/docgen/type.md.jinja2 +0 -0
  61. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/dotgen.py +0 -0
  62. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/excelgen.py +0 -0
  63. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/golanggen.py +0 -0
  64. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/golrgen.py +0 -0
  65. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/javagen/example_template.java.jinja2 +0 -0
  66. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/javagen/java_record_template.jinja2 +0 -0
  67. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/javagen.py +0 -0
  68. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/jsonldgen.py +0 -0
  69. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/legacy/__init__.py +0 -0
  70. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/markdowngen.py +0 -0
  71. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/namespacegen.py +0 -0
  72. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/oocodegen.py +0 -0
  73. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/prefixmapgen.py +0 -0
  74. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/projectgen.py +0 -0
  75. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/protogen.py +0 -0
  76. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/__init__.py +0 -0
  77. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/black.py +0 -0
  78. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/build.py +0 -0
  79. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/includes.py +0 -0
  80. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/pydanticgen.py +0 -0
  81. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/templates/base_model.py.jinja +0 -0
  82. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/templates/class.py.jinja +0 -0
  83. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/templates/conditional_import.py.jinja +0 -0
  84. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/templates/enum.py.jinja +0 -0
  85. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/templates/footer.py.jinja +0 -0
  86. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/templates/imports.py.jinja +0 -0
  87. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/templates/module.py.jinja +0 -0
  88. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/pydanticgen/templates/validator.py.jinja +0 -0
  89. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/python/__init__.py +0 -0
  90. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/rdfgen.py +0 -0
  91. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/shacl/__init__.py +0 -0
  92. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/shacl/shacl_data_type.py +0 -0
  93. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/shacl/shacl_ifabsent_processor.py +0 -0
  94. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/shexgen.py +0 -0
  95. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/sparqlgen.py +0 -0
  96. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/sqlalchemy/__init__.py +0 -0
  97. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/sqlalchemy/sqlalchemy_declarative_template.py +0 -0
  98. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/sqlalchemy/sqlalchemy_imperative_template.py +0 -0
  99. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/sqlalchemygen.py +0 -0
  100. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/sqltablegen.py +0 -0
  101. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/sssomgen.py +0 -0
  102. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/string_template.md +0 -0
  103. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/summarygen.py +0 -0
  104. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/terminusdbgen.py +0 -0
  105. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/yamlgen.py +0 -0
  106. {linkml-1.8.4 → linkml-1.8.6}/linkml/generators/yumlgen.py +0 -0
  107. {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/__init__.py +0 -0
  108. {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/cli.py +0 -0
  109. {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/config/datamodel/.linkmllint.yaml +0 -0
  110. {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/config/datamodel/__init__.py +0 -0
  111. {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/config/datamodel/config.py +0 -0
  112. {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/config/datamodel/config.yaml +0 -0
  113. {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/config/default.yaml +0 -0
  114. {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/config/recommended.yaml +0 -0
  115. {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/formatters/__init__.py +0 -0
  116. {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/formatters/formatter.py +0 -0
  117. {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/formatters/json_formatter.py +0 -0
  118. {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/formatters/markdown_formatter.py +0 -0
  119. {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/formatters/terminal_formatter.py +0 -0
  120. {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/formatters/tsv_formatter.py +0 -0
  121. {linkml-1.8.4 → linkml-1.8.6}/linkml/linter/linter.py +0 -0
  122. {linkml-1.8.4 → linkml-1.8.6}/linkml/reporting/__init__.py +0 -0
  123. {linkml-1.8.4 → linkml-1.8.6}/linkml/reporting/model.py +0 -0
  124. {linkml-1.8.4 → linkml-1.8.6}/linkml/transformers/__init__.py +0 -0
  125. {linkml-1.8.4 → linkml-1.8.6}/linkml/transformers/logical_model_transformer.py +0 -0
  126. {linkml-1.8.4 → linkml-1.8.6}/linkml/transformers/model_transformer.py +0 -0
  127. {linkml-1.8.4 → linkml-1.8.6}/linkml/transformers/relmodel_transformer.py +0 -0
  128. {linkml-1.8.4 → linkml-1.8.6}/linkml/transformers/schema_renamer.py +0 -0
  129. {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/__init__.py +0 -0
  130. {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/cli_utils.py +0 -0
  131. {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/datautils.py +0 -0
  132. {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/datavalidator.py +0 -0
  133. {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/exceptions.py +0 -0
  134. {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/execute_tutorial.py +0 -0
  135. {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/generator.py +0 -0
  136. {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/logictools.py +0 -0
  137. {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/mergeutils.py +0 -0
  138. {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/rawloader.py +0 -0
  139. {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/schema_builder.py +0 -0
  140. {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/schema_fixer.py +0 -0
  141. {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/schemaloader.py +0 -0
  142. {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/schemasynopsis.py +0 -0
  143. {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/sqlutils.py +0 -0
  144. {linkml-1.8.4 → linkml-1.8.6}/linkml/utils/typereferences.py +0 -0
  145. {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/cli.py +0 -0
  146. {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/loaders/__init__.py +0 -0
  147. {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/loaders/delimited_file_loader.py +0 -0
  148. {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/loaders/json_loader.py +0 -0
  149. {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/loaders/loader.py +0 -0
  150. {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/loaders/passthrough_loader.py +0 -0
  151. {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/loaders/yaml_loader.py +0 -0
  152. {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/plugins/__init__.py +0 -0
  153. {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/plugins/pydantic_validation_plugin.py +0 -0
  154. {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/plugins/recommended_slots_plugin.py +0 -0
  155. {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/plugins/shacl_validation_plugin.py +0 -0
  156. {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/plugins/validation_plugin.py +0 -0
  157. {linkml-1.8.4 → linkml-1.8.6}/linkml/validator/validation_context.py +0 -0
  158. {linkml-1.8.4 → linkml-1.8.6}/linkml/validators/__init__.py +0 -0
  159. {linkml-1.8.4 → linkml-1.8.6}/linkml/workspaces/__init__.py +0 -0
  160. {linkml-1.8.4 → linkml-1.8.6}/linkml/workspaces/datamodel/__init__.py +0 -0
  161. {linkml-1.8.4 → linkml-1.8.6}/linkml/workspaces/datamodel/workspaces.py +0 -0
  162. {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.4
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.4.0) ; python_version < "3.9"
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
- if slot.range == String.type_name:
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 slot.range == Boolean.type_name:
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 slot.range == Integer.type_name:
115
+ if base_type is Integer:
79
116
  return self.map_integer_default_value(ifabsent_default_value, slot, cls)
80
117
 
81
- if slot.range == Float.type_name:
118
+ if base_type is Float:
82
119
  return self.map_float_default_value(ifabsent_default_value, slot, cls)
83
120
 
84
- if slot.range == Double.type_name:
121
+ if base_type is Double:
85
122
  return self.map_double_default_value(ifabsent_default_value, slot, cls)
86
123
 
87
- if slot.range == Decimal.type_name:
124
+ if base_type is Decimal:
88
125
  return self.map_decimal_default_value(ifabsent_default_value, slot, cls)
89
126
 
90
- if slot.range == Time.type_name:
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 slot.range == Date.type_name:
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 slot.range == Datetime.type_name:
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 slot.range == DateOrDatetime.type_name:
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 slot.range == Uri.type_name:
174
+ if base_type is Uri:
138
175
  return self.map_uri_default_value(ifabsent_default_value, slot, cls)
139
176
 
140
- if slot.range == Curie.type_name:
177
+ if base_type is Curie:
141
178
  return self.map_curie_default_value(ifabsent_default_value, slot, cls)
142
179
 
143
- if slot.range == Uriorcurie.type_name:
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 slot.range == Ncname.type_name:
183
+ if base_type is Ncname:
147
184
  return self.map_nc_name_default_value(ifabsent_default_value, slot, cls)
148
185
 
149
- if slot.range == Objectidentifier.type_name:
186
+ if base_type is Objectidentifier:
150
187
  return self.map_object_identifier_default_value(ifabsent_default_value, slot, cls)
151
188
 
152
- if slot.range == Nodeidentifier.type_name:
189
+ if base_type is Nodeidentifier:
153
190
  return self.map_node_identifier_default_value(ifabsent_default_value, slot, cls)
154
191
 
155
- if slot.range == Jsonpointer.type_name:
192
+ if base_type is Jsonpointer:
156
193
  return self.map_json_pointer_default_value(ifabsent_default_value, slot, cls)
157
194
 
158
- if slot.range == Jsonpath.type_name:
195
+ if base_type is Jsonpath:
159
196
  return self.map_json_path_default_value(ifabsent_default_value, slot, cls)
160
197
 
161
- if slot.range == Sparqlpath.type_name:
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=EnumDefinition)
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()