linkml 1.9.1rc2__py3-none-any.whl → 1.9.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- linkml/cli/main.py +2 -0
- linkml/generators/__init__.py +2 -0
- linkml/generators/docgen/class_diagram.md.jinja2 +10 -8
- linkml/generators/docgen/common_metadata.md.jinja2 +1 -1
- linkml/generators/docgen/index.md.jinja2 +1 -1
- linkml/generators/docgen.py +33 -7
- linkml/generators/jsonldgen.py +1 -1
- linkml/generators/markdowngen.py +24 -1
- linkml/generators/mermaidclassdiagramgen.py +3 -2
- linkml/generators/panderagen/__init__.py +9 -0
- linkml/generators/panderagen/class_generator_mixin.py +20 -0
- linkml/generators/panderagen/enum_generator_mixin.py +17 -0
- linkml/generators/panderagen/panderagen.py +216 -0
- linkml/generators/panderagen/panderagen_class_based/class.jinja2 +26 -0
- linkml/generators/panderagen/panderagen_class_based/enums.jinja2 +9 -0
- linkml/generators/panderagen/panderagen_class_based/header.jinja2 +21 -0
- linkml/generators/panderagen/panderagen_class_based/mixins.jinja2 +26 -0
- linkml/generators/panderagen/panderagen_class_based/pandera.jinja2 +16 -0
- linkml/generators/panderagen/panderagen_class_based/slots.jinja2 +36 -0
- linkml/generators/panderagen/slot_generator_mixin.py +77 -0
- linkml/generators/pydanticgen/templates/enum.py.jinja +6 -4
- linkml/generators/pydanticgen/templates/validator.py.jinja +7 -6
- linkml/generators/sqltablegen.py +143 -86
- linkml/generators/yumlgen.py +33 -1
- linkml/transformers/rollup_transformer.py +115 -0
- linkml/utils/deprecation.py +26 -0
- linkml/utils/execute_tutorial.py +71 -9
- linkml/validator/plugins/recommended_slots_plugin.py +4 -4
- {linkml-1.9.1rc2.dist-info → linkml-1.9.2.dist-info}/METADATA +19 -16
- {linkml-1.9.1rc2.dist-info → linkml-1.9.2.dist-info}/RECORD +33 -21
- {linkml-1.9.1rc2.dist-info → linkml-1.9.2.dist-info}/WHEEL +1 -1
- {linkml-1.9.1rc2.dist-info → linkml-1.9.2.dist-info}/entry_points.txt +1 -0
- {linkml-1.9.1rc2.dist-info → linkml-1.9.2.dist-info}/LICENSE +0 -0
@@ -0,0 +1,36 @@
|
|
1
|
+
{#-
|
2
|
+
Jinja2 macros for rendering slots in a Pandera class-based model
|
3
|
+
|
4
|
+
-#}
|
5
|
+
{%- import 'enums.jinja2' as enum_macros -%}
|
6
|
+
|
7
|
+
{%- macro constraint_parameters(field, slot) -%}
|
8
|
+
{%- if coerce is true -%}coerce=True, {% endif -%}
|
9
|
+
{%- if field.default_value is not none -%}default={{ field.default_value }}, {% endif -%}
|
10
|
+
{%- if slot.minimum_value is not none -%}ge={{ slot.minimum_value }}, {% endif -%}
|
11
|
+
{%- if slot.maximum_value is not none -%}le={{ slot.maximum_value }}, {% endif -%}
|
12
|
+
{%- if slot.pattern is not none -%}str_matches=r"{{ slot.pattern }}", {% endif -%}
|
13
|
+
{%- if (slot.required is none or slot.required is false) and slot.identifier is not true -%}nullable=True, {% endif -%}
|
14
|
+
{%- endmacro -%}
|
15
|
+
|
16
|
+
{%- macro render_slot(field) -%}
|
17
|
+
{%- with slot = field.source_slot %}
|
18
|
+
{{ field.name }}:{{ ' ' }}
|
19
|
+
{%- if (slot.required is none or slot.required is false) and slot.identifier is not true -%}
|
20
|
+
Optional[
|
21
|
+
{%- endif -%}
|
22
|
+
{{field.range}}
|
23
|
+
{%- if (slot.required is none or slot.required is false) and slot.identifier is not true -%}
|
24
|
+
{{ '] ' }}
|
25
|
+
{%- endif -%}
|
26
|
+
= pla.Field(
|
27
|
+
{{- constraint_parameters(field, slot) -}}
|
28
|
+
{{- enum_macros.enum_parameter(slot) -}}
|
29
|
+
)
|
30
|
+
{%- if slot.description %}
|
31
|
+
"""
|
32
|
+
{{ slot.description }}
|
33
|
+
"""
|
34
|
+
{% endif -%}
|
35
|
+
{%- endwith -%}
|
36
|
+
{%- endmacro %}
|
@@ -0,0 +1,77 @@
|
|
1
|
+
import logging
|
2
|
+
|
3
|
+
from linkml.generators.oocodegen import OOField
|
4
|
+
|
5
|
+
logger = logging.getLogger(__file__)
|
6
|
+
|
7
|
+
|
8
|
+
class SlotGeneratorMixin:
|
9
|
+
LINKML_ANY_CURIE = "linkml:Any"
|
10
|
+
ANY_RANGE_STRING = "Object"
|
11
|
+
CLASS_RANGE_STRING = "Struct"
|
12
|
+
ENUM_RANGE_STRING = "Enum"
|
13
|
+
DEFAULT_RANGE_STRING = "str"
|
14
|
+
|
15
|
+
# to be implemented by the class
|
16
|
+
def make_multivalued(self, range: str):
|
17
|
+
raise NotImplementedError("please implement make multivalued in the class")
|
18
|
+
|
19
|
+
def handle_none_slot(self, slot, range: str) -> str:
|
20
|
+
range = self.schema.default_range # need to figure this out, set at the beginning?
|
21
|
+
if range is None:
|
22
|
+
range = SlotGeneratorMixin.DEFAULT_RANGE_STRING
|
23
|
+
|
24
|
+
return range
|
25
|
+
|
26
|
+
def handle_class_slot(self, slot, range: str) -> str:
|
27
|
+
logger.warning(f"PanderaGen does not support class range slots. Using Struct {slot.name}")
|
28
|
+
return SlotGeneratorMixin.CLASS_RANGE_STRING
|
29
|
+
|
30
|
+
def handle_type_slot(self, slot, range: str) -> str:
|
31
|
+
del slot # unused for now
|
32
|
+
|
33
|
+
t = self.schemaview.all_types().get(range)
|
34
|
+
range = self.map_type(t)
|
35
|
+
|
36
|
+
return range
|
37
|
+
|
38
|
+
def handle_enum_slot(self, slot, range: str) -> str:
|
39
|
+
enum_definition = self.schemaview.all_enums().get(range)
|
40
|
+
range = SlotGeneratorMixin.ENUM_RANGE_STRING
|
41
|
+
slot.annotations["permissible_values"] = self.get_enum_permissible_values(enum_definition)
|
42
|
+
|
43
|
+
return range
|
44
|
+
|
45
|
+
def handle_multivalued_slot(self, slot, range: str) -> str:
|
46
|
+
if slot.multivalued:
|
47
|
+
if slot.inlined_as_list and range != SlotGeneratorMixin.CLASS_RANGE_STRING:
|
48
|
+
range = self.make_multivalued(range)
|
49
|
+
|
50
|
+
return range
|
51
|
+
|
52
|
+
def handle_slot(self, cn: str, sn: str):
|
53
|
+
safe_sn = self.get_slot_name(sn)
|
54
|
+
slot = self.schemaview.induced_slot(sn, cn)
|
55
|
+
range = slot.range
|
56
|
+
|
57
|
+
if slot.alias is not None:
|
58
|
+
safe_sn = self.get_slot_name(slot.alias)
|
59
|
+
|
60
|
+
if range is None:
|
61
|
+
range = self.handle_none_slot(slot, range)
|
62
|
+
elif range in self.schemaview.all_classes():
|
63
|
+
range = self.handle_class_slot(slot, range)
|
64
|
+
elif range in self.schemaview.all_types():
|
65
|
+
range = self.handle_type_slot(slot, range)
|
66
|
+
elif range in self.schemaview.all_enums():
|
67
|
+
range = self.handle_enum_slot(slot, range)
|
68
|
+
else:
|
69
|
+
raise Exception(f"Unknown range {range}")
|
70
|
+
|
71
|
+
range = self.handle_multivalued_slot(slot, range)
|
72
|
+
|
73
|
+
return OOField(
|
74
|
+
name=safe_sn,
|
75
|
+
source_slot=slot,
|
76
|
+
range=range,
|
77
|
+
)
|
@@ -5,12 +5,14 @@ class {{ name }}(str{% if values %}, Enum{% endif %}):
|
|
5
5
|
"""
|
6
6
|
{% endif %}
|
7
7
|
{% if values %}
|
8
|
-
{% for pv in values.values()
|
8
|
+
{% for pv in values.values() %}
|
9
|
+
{{pv.label}} = "{{pv.value}}"
|
9
10
|
{% if pv.description %}
|
10
|
-
|
11
|
+
"""
|
12
|
+
{{ pv.description | indent(width=4) }}
|
13
|
+
"""
|
11
14
|
{% endif %}
|
12
|
-
{{pv.label}} = "{{pv.value}}"
|
13
15
|
{% endfor %}
|
14
16
|
{% else %}
|
15
17
|
pass
|
16
|
-
{% endif %}
|
18
|
+
{% endif %}
|
@@ -1,11 +1,12 @@
|
|
1
1
|
@field_validator('{{name}}')
|
2
2
|
def pattern_{{name}}(cls, v):
|
3
3
|
pattern=re.compile(r"{{pattern}}")
|
4
|
-
if isinstance(v,list):
|
4
|
+
if isinstance(v, list):
|
5
5
|
for element in v:
|
6
|
-
if isinstance(
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
6
|
+
if isinstance(element, str) and not pattern.match(element):
|
7
|
+
err_msg = f"Invalid {{name}} format: {element}"
|
8
|
+
raise ValueError(err_msg)
|
9
|
+
elif isinstance(v, str) and not pattern.match(v):
|
10
|
+
err_msg = f"Invalid {{name}} format: {v}"
|
11
|
+
raise ValueError(err_msg)
|
11
12
|
return v
|
linkml/generators/sqltablegen.py
CHANGED
@@ -1,7 +1,10 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import logging
|
2
4
|
import os
|
5
|
+
import re
|
3
6
|
from dataclasses import dataclass
|
4
|
-
from typing import
|
7
|
+
from typing import Any
|
5
8
|
|
6
9
|
import click
|
7
10
|
from linkml_runtime.dumpers import yaml_dumper
|
@@ -9,12 +12,12 @@ from linkml_runtime.linkml_model import SchemaDefinition, SlotDefinition
|
|
9
12
|
from linkml_runtime.utils.formatutils import camelcase, underscore
|
10
13
|
from linkml_runtime.utils.schemaview import SchemaView
|
11
14
|
from sqlalchemy import Column, ForeignKey, MetaData, Table, UniqueConstraint, create_mock_engine
|
15
|
+
from sqlalchemy.dialects.oracle import VARCHAR2
|
12
16
|
from sqlalchemy.types import Boolean, Date, DateTime, Enum, Float, Integer, Text, Time
|
13
17
|
|
14
18
|
from linkml._version import __version__
|
15
19
|
from linkml.transformers.relmodel_transformer import ForeignKeyPolicy, RelationalModelTransformer
|
16
20
|
from linkml.utils.generator import Generator, shared_arguments
|
17
|
-
from linkml.utils.schemaloader import SchemaLoader
|
18
21
|
|
19
22
|
logger = logging.getLogger(__name__)
|
20
23
|
|
@@ -46,6 +49,7 @@ METAMODEL_TYPE_TO_BASE = {
|
|
46
49
|
RANGEMAP = {
|
47
50
|
"str": Text(),
|
48
51
|
"string": Text(),
|
52
|
+
"String": Text(),
|
49
53
|
"NCName": Text(),
|
50
54
|
"URIorCURIE": Text(),
|
51
55
|
"int": Integer(),
|
@@ -60,29 +64,33 @@ RANGEMAP = {
|
|
60
64
|
}
|
61
65
|
|
62
66
|
|
67
|
+
VARCHAR_REGEX = re.compile(r"VARCHAR2?(\((\d+)\))?")
|
68
|
+
ORACLE_MAX_VARCHAR_LENGTH = 4096
|
69
|
+
|
70
|
+
|
63
71
|
@dataclass
|
64
72
|
class SQLTableGenerator(Generator):
|
65
73
|
"""
|
66
|
-
A :class:`~linkml.utils.generator.Generator` for creating SQL DDL
|
74
|
+
A :class:`~linkml.utils.generator.Generator` for creating SQL DDL.
|
67
75
|
|
68
76
|
The basic algorithm for mapping a linkml schema S is as follows:
|
69
77
|
|
70
78
|
- Each schema S corresponds to one database schema D (see SQLSchema)
|
71
|
-
- Each Class C in S is mapped to a table T (see
|
72
|
-
- Each slot S in each C is mapped to a column Col (see
|
79
|
+
- Each Class C in S is mapped to a table T (see sqlalchemy.Table)
|
80
|
+
- Each slot S in each C is mapped to a column Col (see sqlalchemy.Column)
|
73
81
|
|
74
|
-
|
82
|
+
If the direct_mapping attribute is set to true, then no further transformations
|
75
83
|
are applied. Note that this means:
|
76
84
|
|
77
85
|
- inline objects are modeled as Text strings
|
78
86
|
- multivalued fields are modeled as single Text strings
|
79
87
|
|
80
|
-
|
81
|
-
|
88
|
+
This direct mapping is useful for simple spreadsheet/denormalized representations of complex data.
|
89
|
+
However, for other applications, additional transformations should occur. These are:
|
82
90
|
|
83
91
|
MULTIVALUED SLOTS
|
84
92
|
|
85
|
-
The relational model does not have direct representation of lists. These are normalized as follows
|
93
|
+
The relational model does not have direct representation of lists. These are normalized as follows:
|
86
94
|
|
87
95
|
If the range of the slot is a class, and there are no other slots whose range is this class,
|
88
96
|
and the slot is for a class that has a singular primary key, then a backref is added.
|
@@ -90,11 +98,11 @@ class SQLTableGenerator(Generator):
|
|
90
98
|
E.g. if we have User 0..* Address,
|
91
99
|
then add a field User_id to Address.
|
92
100
|
|
93
|
-
When SQLAlchemy bindings are created, a backref mapping is added
|
101
|
+
When SQLAlchemy bindings are created, a backref mapping is added.
|
94
102
|
|
95
|
-
If the range of the slot is an enum or type, then a new linktable is created, and a backref added
|
103
|
+
If the range of the slot is an enum or type, then a new linktable is created, and a backref added.
|
96
104
|
|
97
|
-
E.g. if a class User has a
|
105
|
+
E.g. if a class User has a multivalued slot alias whose range is a string,
|
98
106
|
then create a table user_aliases, with two columns (1) alias [a string] and (2) a backref to user
|
99
107
|
|
100
108
|
Each mapped slot C.S has a range R
|
@@ -139,11 +147,21 @@ class SQLTableGenerator(Generator):
|
|
139
147
|
rename_foreign_keys: bool = False
|
140
148
|
direct_mapping: bool = False
|
141
149
|
relative_slot_num: bool = False
|
150
|
+
default_length_oracle: int = ORACLE_MAX_VARCHAR_LENGTH
|
151
|
+
generate_abstract_class_ddl: bool = True
|
142
152
|
|
143
|
-
def serialize(self, **kwargs) -> str:
|
153
|
+
def serialize(self, **kwargs: dict[str, Any]) -> str:
|
144
154
|
return self.generate_ddl(**kwargs)
|
145
155
|
|
146
|
-
def generate_ddl(self, naming_policy: SqlNamingPolicy = None, **kwargs) -> str:
|
156
|
+
def generate_ddl(self, naming_policy: SqlNamingPolicy = None, **kwargs: dict[str, Any]) -> str:
|
157
|
+
"""
|
158
|
+
Generate a DDL using the schema in self.schema.
|
159
|
+
|
160
|
+
:param naming_policy: naming policy for columns, defaults to None
|
161
|
+
:type naming_policy: SqlNamingPolicy, optional
|
162
|
+
:return: the DDL as a string
|
163
|
+
:rtype: str
|
164
|
+
"""
|
147
165
|
ddl_str = ""
|
148
166
|
|
149
167
|
def dump(sql, *multiparams, **params):
|
@@ -155,25 +173,20 @@ class SQLTableGenerator(Generator):
|
|
155
173
|
sqltr = RelationalModelTransformer(SchemaView(self.schema))
|
156
174
|
if not self.use_foreign_keys:
|
157
175
|
sqltr.foreign_key_policy = ForeignKeyPolicy.NO_FOREIGN_KEYS
|
158
|
-
tr_result = sqltr.transform(
|
159
|
-
tgt_schema_name=kwargs.get("tgt_schema_name", None), top_class=kwargs.get("top_class", None)
|
160
|
-
)
|
176
|
+
tr_result = sqltr.transform(tgt_schema_name=kwargs.get("tgt_schema_name"), top_class=kwargs.get("top_class"))
|
161
177
|
schema = tr_result.schema
|
162
178
|
|
163
179
|
def sql_name(n: str) -> str:
|
164
|
-
if naming_policy
|
165
|
-
if naming_policy == SqlNamingPolicy.underscore:
|
166
|
-
return underscore(n)
|
167
|
-
elif naming_policy == SqlNamingPolicy.camelcase:
|
168
|
-
return camelcase(n)
|
169
|
-
elif naming_policy == SqlNamingPolicy.preserve:
|
170
|
-
return n
|
171
|
-
else:
|
172
|
-
raise Exception(f"Unknown: {naming_policy}")
|
173
|
-
else:
|
180
|
+
if not naming_policy or naming_policy == SqlNamingPolicy.preserve:
|
174
181
|
return n
|
175
|
-
|
176
|
-
|
182
|
+
if naming_policy == SqlNamingPolicy.underscore:
|
183
|
+
return underscore(n)
|
184
|
+
if naming_policy == SqlNamingPolicy.camelcase:
|
185
|
+
return camelcase(n)
|
186
|
+
msg = f"Unknown: {naming_policy}"
|
187
|
+
raise Exception(msg)
|
188
|
+
|
189
|
+
def strip_newlines(txt: str | None) -> str:
|
177
190
|
if txt is None:
|
178
191
|
return ""
|
179
192
|
return txt.replace("\n", "")
|
@@ -183,9 +196,16 @@ class SQLTableGenerator(Generator):
|
|
183
196
|
# As a workaround we add these as "--" comments via direct string manipulation
|
184
197
|
include_comments = self.dialect == "sqlite"
|
185
198
|
sv = SchemaView(schema)
|
199
|
+
|
200
|
+
# Iterate through the attributes in each class, creating Column objects.
|
201
|
+
# This includes generating the appropriate column name, converting the range
|
202
|
+
# into an SQL type, and adding a foreign key notation if appropriate.
|
186
203
|
for cn, c in schema.classes.items():
|
187
204
|
if include_comments:
|
188
|
-
|
205
|
+
if c.abstract:
|
206
|
+
ddl_str += f'-- # Abstract Class: "{cn}" Description: "{strip_newlines(c.description)}"\n'
|
207
|
+
else:
|
208
|
+
ddl_str += f'-- # Class: "{cn}" Description: "{strip_newlines(c.description)}"\n'
|
189
209
|
pk_slot = sv.get_identifier_slot(cn)
|
190
210
|
if c.attributes:
|
191
211
|
cols = []
|
@@ -197,7 +217,7 @@ class SQLTableGenerator(Generator):
|
|
197
217
|
# is_pk = True ## TODO: use unique key
|
198
218
|
args = []
|
199
219
|
if s.range in schema.classes and self.use_foreign_keys:
|
200
|
-
fk = sql_name(self.
|
220
|
+
fk = sql_name(self.get_id_or_key(s.range, sv))
|
201
221
|
args = [ForeignKey(fk)]
|
202
222
|
field_type = self.get_sql_range(s, schema)
|
203
223
|
col = Column(
|
@@ -212,82 +232,107 @@ class SQLTableGenerator(Generator):
|
|
212
232
|
if s.description:
|
213
233
|
col.comment = s.description
|
214
234
|
cols.append(col)
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
if sn in c.attributes:
|
219
|
-
return sql_name(sn)
|
220
|
-
else:
|
221
|
-
# for candidate in c.attributes.values():
|
222
|
-
# if "original_slot" in candidate.annotations:
|
223
|
-
# original = candidate.annotations["original_slot"]
|
224
|
-
# if original.value == sn:
|
225
|
-
# return sql_name(candidate.name)
|
226
|
-
return None
|
227
|
-
|
228
|
-
sql_names = [_sql_name(sn) for sn in uc.unique_key_slots]
|
235
|
+
# convert unique_keys into a uniqueness constraint for the table
|
236
|
+
for uc in c.unique_keys.values():
|
237
|
+
sql_names = [sql_name(sn) if sn in c.attributes else None for sn in uc.unique_key_slots]
|
229
238
|
if any(sn is None for sn in sql_names):
|
230
239
|
continue
|
231
240
|
sql_uc = UniqueConstraint(*sql_names)
|
232
241
|
cols.append(sql_uc)
|
242
|
+
if not c.abstract or (c.abstract and self.generate_abstract_class_ddl):
|
233
243
|
Table(sql_name(cn), schema_metadata, *cols, comment=str(c.description))
|
234
244
|
schema_metadata.create_all(engine)
|
235
245
|
return ddl_str
|
236
246
|
|
237
|
-
def
|
247
|
+
def get_oracle_sql_range(self, slot: SlotDefinition) -> Text | VARCHAR2 | None:
|
238
248
|
"""
|
239
|
-
|
249
|
+
Generate the appropriate range for Oracle SQL.
|
250
|
+
|
251
|
+
:param slot: the slot under examination
|
252
|
+
:type slot: SlotDefinition
|
253
|
+
:return: appropriate type for Oracle SQL or None
|
254
|
+
:rtype: Text | VARCHAR2 | None
|
240
255
|
"""
|
241
|
-
|
256
|
+
slot_range = slot.range
|
257
|
+
if not slot_range:
|
258
|
+
return None
|
259
|
+
|
260
|
+
if slot_range.lower() in ["str", "string"]:
|
261
|
+
# string type data should be represented as a VARCHAR2
|
262
|
+
return VARCHAR2(self.default_length_oracle)
|
263
|
+
|
264
|
+
# check whether the slot range matches the regex "VARCHAR2?(\((\d+)\))?"
|
265
|
+
match = re.match(VARCHAR_REGEX, slot_range)
|
266
|
+
if match:
|
267
|
+
# match.group(2) is the digits in brackets after VARCHAR
|
268
|
+
# i.e. a defined length for the VARCHAR
|
269
|
+
if match.group(2) and int(match.group(2)) > ORACLE_MAX_VARCHAR_LENGTH:
|
270
|
+
msg = (
|
271
|
+
"Warning: range exceeds maximum Oracle VARCHAR length, "
|
272
|
+
f"CLOB type will be returned: {slot_range} for {slot.name} = {slot.range}"
|
273
|
+
)
|
274
|
+
logger.info(msg)
|
275
|
+
return Text()
|
276
|
+
# set the length to either the varchar length (as defined in the slot_range)
|
277
|
+
# or the default
|
278
|
+
return VARCHAR2(match.group(2) or self.default_length_oracle)
|
279
|
+
|
280
|
+
# use standard SQL range matching for anything else
|
281
|
+
return None
|
282
|
+
|
283
|
+
def get_sql_range(self, slot: SlotDefinition, schema: SchemaDefinition = None):
|
284
|
+
"""Get the slot range as a SQL Alchemy column type."""
|
285
|
+
slot_range = slot.range
|
286
|
+
|
287
|
+
if self.dialect == "oracle":
|
288
|
+
range_type = self.get_oracle_sql_range(slot)
|
289
|
+
if range_type:
|
290
|
+
return range_type
|
291
|
+
|
292
|
+
if slot_range is None:
|
293
|
+
return Text()
|
242
294
|
|
243
295
|
# if no SchemaDefinition is explicitly provided as an argument
|
244
296
|
# then simply use the schema that is provided to the SQLTableGenerator() object
|
245
297
|
if not schema:
|
246
|
-
schema =
|
298
|
+
schema = self.schema
|
247
299
|
|
248
|
-
|
300
|
+
sv = SchemaView(schema)
|
301
|
+
if slot_range in sv.all_classes():
|
249
302
|
# FK type should be the same as the identifier of the foreign key
|
250
|
-
fk =
|
303
|
+
fk = sv.get_identifier_slot(slot_range)
|
251
304
|
if fk:
|
252
|
-
return self.get_sql_range(fk, schema)
|
253
|
-
|
254
|
-
|
255
|
-
if
|
256
|
-
e =
|
305
|
+
return self.get_sql_range(fk, sv.schema)
|
306
|
+
return Text()
|
307
|
+
|
308
|
+
if slot_range in sv.all_enums():
|
309
|
+
e = sv.all_enums()[slot_range]
|
257
310
|
if e.permissible_values is not None:
|
258
311
|
vs = [str(v) for v in e.permissible_values]
|
259
312
|
return Enum(name=e.name, *vs)
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
return Text()
|
313
|
+
|
314
|
+
if slot_range in METAMODEL_TYPE_TO_BASE:
|
315
|
+
range_base = METAMODEL_TYPE_TO_BASE[slot_range]
|
316
|
+
elif slot_range in sv.all_types():
|
317
|
+
range_base = sv.all_types()[slot_range].base
|
266
318
|
else:
|
267
|
-
logger.error(f"Unknown range: {
|
319
|
+
logger.error(f"Unknown range: {slot_range} for {slot.name} = {slot.range}")
|
268
320
|
return Text()
|
321
|
+
|
269
322
|
if range_base in RANGEMAP:
|
270
323
|
return RANGEMAP[range_base]
|
271
|
-
|
272
|
-
|
273
|
-
|
324
|
+
|
325
|
+
logger.error(f"Unknown range base: {range_base} for {slot.name} = {slot.range}")
|
326
|
+
return Text()
|
274
327
|
|
275
328
|
@staticmethod
|
276
|
-
def
|
277
|
-
|
278
|
-
|
329
|
+
def get_id_or_key(cn: str, sv: SchemaView) -> str:
|
330
|
+
"""Given a named class, retrieve the identifier or key slot."""
|
331
|
+
pk = sv.get_identifier_slot(cn, use_key=True)
|
279
332
|
if pk is None:
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
pk = s
|
284
|
-
break
|
285
|
-
if pk is None:
|
286
|
-
raise Exception(f"No PK for {cn}")
|
287
|
-
if pk.alias:
|
288
|
-
pk_name = pk.alias
|
289
|
-
else:
|
290
|
-
pk_name = pk.name
|
333
|
+
msg = f"No PK for {cn}"
|
334
|
+
raise Exception(msg)
|
335
|
+
pk_name = pk.alias if pk.alias else pk.name
|
291
336
|
return f"{cn}.{pk_name}"
|
292
337
|
|
293
338
|
|
@@ -311,17 +356,29 @@ class SQLTableGenerator(Generator):
|
|
311
356
|
show_default=True,
|
312
357
|
help="Emit FK declarations",
|
313
358
|
)
|
359
|
+
@click.option(
|
360
|
+
"--default_length_oracle",
|
361
|
+
default=ORACLE_MAX_VARCHAR_LENGTH,
|
362
|
+
show_default=True,
|
363
|
+
help="Default length of varchar based arguments for oracle dialects",
|
364
|
+
)
|
365
|
+
@click.option(
|
366
|
+
"--generate_abstract_class_ddl",
|
367
|
+
default=True,
|
368
|
+
show_default=True,
|
369
|
+
help="A manual override to omit the abstract classes, set to true as a default for testing sake",
|
370
|
+
)
|
314
371
|
@click.version_option(__version__, "-V", "--version")
|
315
372
|
def cli(
|
316
|
-
yamlfile,
|
317
|
-
relmodel_output,
|
318
|
-
sqla_file: str = None,
|
373
|
+
yamlfile: str,
|
374
|
+
relmodel_output: str,
|
375
|
+
sqla_file: str | None = None,
|
319
376
|
python_import: str = None,
|
320
|
-
dialect=None,
|
321
|
-
use_foreign_keys=True,
|
377
|
+
dialect: str | None = None,
|
378
|
+
use_foreign_keys: bool = True,
|
322
379
|
**args,
|
323
380
|
):
|
324
|
-
"""Generate SQL DDL representation"""
|
381
|
+
"""Generate SQL DDL representation."""
|
325
382
|
if relmodel_output:
|
326
383
|
sv = SchemaView(yamlfile)
|
327
384
|
rtr = RelationalModelTransformer(sv)
|
linkml/generators/yumlgen.py
CHANGED
@@ -15,6 +15,7 @@ from linkml_runtime.utils.formatutils import camelcase, underscore
|
|
15
15
|
from rdflib import Namespace
|
16
16
|
|
17
17
|
from linkml import REQUESTS_TIMEOUT
|
18
|
+
from linkml.utils.deprecation import deprecation_warning
|
18
19
|
from linkml.utils.generator import Generator, shared_arguments
|
19
20
|
|
20
21
|
yuml_is_a = "^-"
|
@@ -34,6 +35,30 @@ YUML = Namespace(yuml_base + yuml_scale + yuml_dir + yuml_class)
|
|
34
35
|
|
35
36
|
@dataclass
|
36
37
|
class YumlGenerator(Generator):
|
38
|
+
"""
|
39
|
+
.. admonition:: Deprecated
|
40
|
+
:class: warning
|
41
|
+
|
42
|
+
The `yuml` generator is being deprecated and is no longer supported.
|
43
|
+
|
44
|
+
Going forward, we recommend using one of the following alternatives that offer improved visualization
|
45
|
+
capabilities:
|
46
|
+
|
47
|
+
- `gen-doc` – Generates documentation with **embedded Mermaid class diagrams**.
|
48
|
+
- `gen-plantuml` – Produces **PlantUML diagrams**.
|
49
|
+
- `gen-mermaid-class-diagram` – Creates **standalone Mermaid class diagrams**.
|
50
|
+
- `gen-erdiagram` – For **Entity-Relationship (ER) diagrams**.
|
51
|
+
|
52
|
+
.. deprecated:: v1.8.7
|
53
|
+
|
54
|
+
Recommendation: Migrate to one of the supported generators listed above.
|
55
|
+
|
56
|
+
"""
|
57
|
+
|
58
|
+
def __post_init__(self) -> None:
|
59
|
+
deprecation_warning("gen-yuml")
|
60
|
+
super().__post_init__()
|
61
|
+
|
37
62
|
generatorname = os.path.basename(__file__)
|
38
63
|
generatorversion = "0.1.1"
|
39
64
|
valid_formats = ["yuml", "png", "pdf", "jpg", "json", "svg"]
|
@@ -87,6 +112,7 @@ class YumlGenerator(Generator):
|
|
87
112
|
|
88
113
|
file_suffix = ".svg" if self.format == "yuml" else "." + self.format
|
89
114
|
file_name = diagram_name or camelcase(sorted(classes)[0] if classes else self.schema.name)
|
115
|
+
|
90
116
|
if directory:
|
91
117
|
self.output_file_name = os.path.join(
|
92
118
|
directory,
|
@@ -274,7 +300,13 @@ class YumlGenerator(Generator):
|
|
274
300
|
help="Name of the diagram in the output directory (without suffix!)",
|
275
301
|
)
|
276
302
|
def cli(yamlfile, **args):
|
277
|
-
"""Generate a
|
303
|
+
"""Generate a yUML representation of a LinkML model
|
304
|
+
|
305
|
+
.. warning::
|
306
|
+
`gen-yuml` is deprecated. Please use `gen-doc`, `gen-plantuml` or `gen-mermaid-class-diagram`.
|
307
|
+
"""
|
308
|
+
deprecation_warning("gen-yuml")
|
309
|
+
|
278
310
|
print(YumlGenerator(yamlfile, **args).serialize(**args), end="")
|
279
311
|
|
280
312
|
|