linkml 1.9.1rc3__py3-none-any.whl → 1.9.2rc1__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.
Files changed (33) hide show
  1. linkml/cli/main.py +2 -0
  2. linkml/generators/__init__.py +2 -0
  3. linkml/generators/docgen/class_diagram.md.jinja2 +10 -8
  4. linkml/generators/docgen/common_metadata.md.jinja2 +1 -1
  5. linkml/generators/docgen/index.md.jinja2 +1 -1
  6. linkml/generators/docgen.py +33 -7
  7. linkml/generators/jsonldgen.py +1 -1
  8. linkml/generators/markdowngen.py +24 -1
  9. linkml/generators/mermaidclassdiagramgen.py +3 -2
  10. linkml/generators/panderagen/__init__.py +9 -0
  11. linkml/generators/panderagen/class_generator_mixin.py +20 -0
  12. linkml/generators/panderagen/enum_generator_mixin.py +17 -0
  13. linkml/generators/panderagen/panderagen.py +216 -0
  14. linkml/generators/panderagen/panderagen_class_based/class.jinja2 +26 -0
  15. linkml/generators/panderagen/panderagen_class_based/enums.jinja2 +9 -0
  16. linkml/generators/panderagen/panderagen_class_based/header.jinja2 +21 -0
  17. linkml/generators/panderagen/panderagen_class_based/mixins.jinja2 +26 -0
  18. linkml/generators/panderagen/panderagen_class_based/pandera.jinja2 +16 -0
  19. linkml/generators/panderagen/panderagen_class_based/slots.jinja2 +36 -0
  20. linkml/generators/panderagen/slot_generator_mixin.py +77 -0
  21. linkml/generators/pydanticgen/templates/enum.py.jinja +6 -4
  22. linkml/generators/pydanticgen/templates/validator.py.jinja +7 -6
  23. linkml/generators/sqltablegen.py +143 -86
  24. linkml/generators/yumlgen.py +33 -1
  25. linkml/transformers/rollup_transformer.py +115 -0
  26. linkml/utils/deprecation.py +26 -0
  27. linkml/utils/execute_tutorial.py +71 -9
  28. linkml/validator/plugins/recommended_slots_plugin.py +4 -4
  29. {linkml-1.9.1rc3.dist-info → linkml-1.9.2rc1.dist-info}/METADATA +19 -16
  30. {linkml-1.9.1rc3.dist-info → linkml-1.9.2rc1.dist-info}/RECORD +33 -21
  31. {linkml-1.9.1rc3.dist-info → linkml-1.9.2rc1.dist-info}/WHEEL +1 -1
  32. {linkml-1.9.1rc3.dist-info → linkml-1.9.2rc1.dist-info}/entry_points.txt +1 -0
  33. {linkml-1.9.1rc3.dist-info → linkml-1.9.2rc1.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
- # {{pv.description}}
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(v, str) and not pattern.match(element):
7
- raise ValueError(f"Invalid {{name}} format: {element}")
8
- elif isinstance(v,str):
9
- if not pattern.match(v):
10
- raise ValueError(f"Invalid {{name}} format: {v}")
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
@@ -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 Optional
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 SQLTable)
72
- - Each slot S in each C is mapped to a column Col (see SQLColumn)
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
- if the direct_mapping attribute is set to true, then no further transformations
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
- this direct mapping is useful for simple spreadsheet/denormalized representations of complex data.
81
- however, for other applications, additional transformations should occur. these are:
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 multivalues slot alias whose range is a string,
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 is not None:
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
- def strip_newlines(txt: Optional[str]) -> str:
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
- ddl_str += f'-- # Class: "{cn}" Description: "{strip_newlines(c.description)}"\n'
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.get_foreign_key(s.range, sv))
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
- for uc_name, uc in c.unique_keys.items():
216
-
217
- def _sql_name(sn: str):
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 get_sql_range(self, slot: SlotDefinition, schema: SchemaDefinition = None):
247
+ def get_oracle_sql_range(self, slot: SlotDefinition) -> Text | VARCHAR2 | None:
238
248
  """
239
- returns a SQL Alchemy column type
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
- range = slot.range
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 = SchemaLoader(data=self.schema).resolve()
298
+ schema = self.schema
247
299
 
248
- if range in schema.classes:
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 = SchemaView(schema).get_identifier_slot(range)
303
+ fk = sv.get_identifier_slot(slot_range)
251
304
  if fk:
252
- return self.get_sql_range(fk, schema)
253
- else:
254
- return Text()
255
- if range in schema.enums:
256
- e = schema.enums[range]
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
- if range in METAMODEL_TYPE_TO_BASE:
261
- range_base = METAMODEL_TYPE_TO_BASE[range]
262
- elif range in schema.types:
263
- range_base = schema.types[range].base
264
- elif range is None:
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: {range} for {slot.name} = {slot.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
- else:
272
- logger.error(f"UNKNOWN range base: {range_base} for {slot.name} = {slot.range}")
273
- return Text()
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 get_foreign_key(cn: str, sv: SchemaView) -> str:
277
- pk = sv.get_identifier_slot(cn)
278
- # TODO: move this to SV
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
- for sn in sv.class_slots(cn):
281
- s = sv.induced_slot(sn, cn)
282
- if s.key:
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)
@@ -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 UML representation of a LinkML model"""
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