linkml 1.9.3rc1__py3-none-any.whl → 1.9.3rc2__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 (60) hide show
  1. linkml/generators/PythonGenNotes.md +49 -49
  2. linkml/generators/README.md +2 -2
  3. linkml/generators/dbmlgen.py +0 -1
  4. linkml/generators/docgen/class.md.jinja2 +35 -11
  5. linkml/generators/docgen/class_diagram.md.jinja2 +15 -15
  6. linkml/generators/docgen/common_metadata.md.jinja2 +3 -5
  7. linkml/generators/docgen/enum.md.jinja2 +4 -5
  8. linkml/generators/docgen/index.md.jinja2 +5 -5
  9. linkml/generators/docgen/index.tex.jinja2 +1 -1
  10. linkml/generators/docgen/schema.md.jinja2 +1 -3
  11. linkml/generators/docgen/slot.md.jinja2 +6 -8
  12. linkml/generators/docgen/subset.md.jinja2 +4 -7
  13. linkml/generators/docgen/type.md.jinja2 +2 -3
  14. linkml/generators/docgen.py +92 -4
  15. linkml/generators/erdiagramgen.py +1 -1
  16. linkml/generators/graphqlgen.py +1 -1
  17. linkml/generators/javagen/example_template.java.jinja2 +0 -1
  18. linkml/generators/jsonldcontextgen.py +0 -1
  19. linkml/generators/jsonldgen.py +3 -1
  20. linkml/generators/jsonschemagen.py +2 -2
  21. linkml/generators/linkmlgen.py +1 -1
  22. linkml/generators/markdowngen.py +20 -9
  23. linkml/generators/mermaidclassdiagramgen.py +4 -0
  24. linkml/generators/projectgen.py +17 -20
  25. linkml/generators/pydanticgen/includes.py +5 -5
  26. linkml/generators/pydanticgen/pydanticgen.py +1 -2
  27. linkml/generators/pydanticgen/template.py +1 -1
  28. linkml/generators/pydanticgen/templates/base_model.py.jinja +1 -1
  29. linkml/generators/pydanticgen/templates/class.py.jinja +1 -1
  30. linkml/generators/pydanticgen/templates/conditional_import.py.jinja +1 -1
  31. linkml/generators/pydanticgen/templates/footer.py.jinja +1 -1
  32. linkml/generators/pydanticgen/templates/imports.py.jinja +1 -1
  33. linkml/generators/python/python_ifabsent_processor.py +1 -1
  34. linkml/generators/pythongen.py +9 -8
  35. linkml/generators/shacl/shacl_ifabsent_processor.py +0 -1
  36. linkml/generators/shaclgen.py +1 -1
  37. linkml/generators/sparqlgen.py +1 -1
  38. linkml/generators/sqlalchemy/__init__.py +0 -4
  39. linkml/generators/sqlalchemygen.py +9 -15
  40. linkml/generators/sqltablegen.py +70 -4
  41. linkml/generators/string_template.md +3 -4
  42. linkml/generators/terminusdbgen.py +1 -2
  43. linkml/linter/cli.py +3 -4
  44. linkml/linter/config/datamodel/config.py +286 -135
  45. linkml/linter/config/datamodel/config.yaml +26 -11
  46. linkml/linter/config/default.yaml +6 -0
  47. linkml/linter/config/recommended.yaml +6 -0
  48. linkml/linter/linter.py +10 -6
  49. linkml/linter/rules.py +144 -46
  50. linkml/transformers/relmodel_transformer.py +2 -1
  51. linkml/utils/generator.py +3 -0
  52. linkml/utils/logictools.py +5 -5
  53. linkml/utils/schemaloader.py +4 -4
  54. linkml/utils/schemasynopsis.py +11 -7
  55. linkml/workspaces/datamodel/workspaces.yaml +0 -5
  56. {linkml-1.9.3rc1.dist-info → linkml-1.9.3rc2.dist-info}/LICENSE +1 -1
  57. {linkml-1.9.3rc1.dist-info → linkml-1.9.3rc2.dist-info}/METADATA +1 -1
  58. {linkml-1.9.3rc1.dist-info → linkml-1.9.3rc2.dist-info}/RECORD +60 -60
  59. {linkml-1.9.3rc1.dist-info → linkml-1.9.3rc2.dist-info}/WHEEL +0 -0
  60. {linkml-1.9.3rc1.dist-info → linkml-1.9.3rc2.dist-info}/entry_points.txt +0 -0
@@ -151,7 +151,7 @@ class SparqlGenerator(Generator):
151
151
  template_obj = Template(template)
152
152
  extra = ""
153
153
  if named_graphs is not None:
154
- extra += f'FILTER( ?graph in ( {",".join(named_graphs)} ))'
154
+ extra += f"FILTER( ?graph in ( {','.join(named_graphs)} ))"
155
155
  logger.info(f"Named Graphs = {named_graphs} // extra={extra}")
156
156
  if limit is not None and isinstance(limit, int):
157
157
  limit = f"LIMIT {limit}"
@@ -1,4 +0,0 @@
1
- from .sqlalchemy_declarative_template import sqlalchemy_declarative_template_str
2
- from .sqlalchemy_imperative_template import sqlalchemy_imperative_template_str
3
-
4
- __all__ = ["sqlalchemy_declarative_template_str", "sqlalchemy_imperative_template_str"]
@@ -1,9 +1,10 @@
1
+ from __future__ import annotations
2
+
1
3
  import logging
2
4
  import os
3
5
  from collections import defaultdict
4
6
  from dataclasses import dataclass
5
7
  from types import ModuleType
6
- from typing import Optional, Union
7
8
 
8
9
  import click
9
10
  from jinja2 import Template
@@ -16,7 +17,8 @@ from sqlalchemy import Enum
16
17
  from linkml._version import __version__
17
18
  from linkml.generators.pydanticgen import PydanticGenerator
18
19
  from linkml.generators.pythongen import PythonGenerator
19
- from linkml.generators.sqlalchemy import sqlalchemy_declarative_template_str, sqlalchemy_imperative_template_str
20
+ from linkml.generators.sqlalchemy.sqlalchemy_declarative_template import sqlalchemy_declarative_template_str
21
+ from linkml.generators.sqlalchemy.sqlalchemy_imperative_template import sqlalchemy_imperative_template_str
20
22
  from linkml.generators.sqltablegen import SQLTableGenerator
21
23
  from linkml.transformers.relmodel_transformer import ForeignKeyPolicy, RelationalModelTransformer
22
24
  from linkml.utils.generator import Generator, shared_arguments
@@ -43,10 +45,10 @@ class SQLAlchemyGenerator(Generator):
43
45
  valid_formats = ["sqla"]
44
46
  file_extension = "py"
45
47
  uses_schemaloader = False
46
- template: Optional[TemplateEnum] = None
48
+ template: TemplateEnum | None = None
47
49
 
48
50
  # ObjectVars
49
- original_schema: Union[SchemaDefinition, str] = None
51
+ original_schema: SchemaDefinition | str = None
50
52
 
51
53
  def __post_init__(self) -> None:
52
54
  self.original_schema = self.schema
@@ -55,14 +57,12 @@ class SQLAlchemyGenerator(Generator):
55
57
 
56
58
  def generate_sqla(
57
59
  self,
58
- model_path: Optional[str] = None,
60
+ model_path: str | None = None,
59
61
  no_model_import: bool = False,
60
- template: Optional[TemplateEnum] = None,
61
- foreign_key_policy: Optional[ForeignKeyPolicy] = None,
62
+ template: TemplateEnum | None = None,
63
+ foreign_key_policy: ForeignKeyPolicy | None = None,
62
64
  **kwargs,
63
65
  ) -> str:
64
- # src_sv = SchemaView(self.schema)
65
- # self.schema = src_sv.schema
66
66
  template = template or self.template or TemplateEnum.IMPERATIVE
67
67
  sqltr = RelationalModelTransformer(self.schemaview)
68
68
  if foreign_key_policy:
@@ -76,7 +76,6 @@ class SQLAlchemyGenerator(Generator):
76
76
  sql_type = sql_range.__repr__()
77
77
  ann = Annotation("sql_type", sql_type)
78
78
  a.annotations[ann.tag] = ann
79
- # a.sql_type = sql_type
80
79
  if template == TemplateEnum.IMPERATIVE:
81
80
  template_str = sqlalchemy_imperative_template_str
82
81
  elif template == TemplateEnum.DECLARATIVE:
@@ -90,10 +89,6 @@ class SQLAlchemyGenerator(Generator):
90
89
  backrefs = defaultdict(list)
91
90
  for m in tr_result.mappings:
92
91
  backrefs[m.source_class].append(m)
93
- # for c in tr_schema.classes.values():
94
- # if len(c.attributes) == 0:
95
- # skip[c.name] = True
96
- # #raise ValueError(f'Class must have attrs: {c.name}')
97
92
  self.add_safe_aliases(tr_schema)
98
93
  tr_sv = SchemaView(tr_schema)
99
94
  rel_schema_classes_ordered = [tr_sv.get_class(cn, strict=True) for cn in self.order_classes_by_hierarchy(tr_sv)]
@@ -167,7 +162,6 @@ class SQLAlchemyGenerator(Generator):
167
162
  @staticmethod
168
163
  def add_safe_aliases(schema: SchemaDefinition) -> None:
169
164
  for c in schema.classes.values():
170
- # c.alias = underscore(c.name)
171
165
  for a in c.attributes.values():
172
166
  a.alias = underscore(a.name)
173
167
 
@@ -11,7 +11,7 @@ from linkml_runtime.dumpers import yaml_dumper
11
11
  from linkml_runtime.linkml_model import SchemaDefinition, SlotDefinition
12
12
  from linkml_runtime.utils.formatutils import camelcase, underscore
13
13
  from linkml_runtime.utils.schemaview import SchemaView
14
- from sqlalchemy import Column, ForeignKey, MetaData, Table, UniqueConstraint, create_mock_engine
14
+ from sqlalchemy import Column, ForeignKey, Index, MetaData, Table, UniqueConstraint, create_mock_engine
15
15
  from sqlalchemy.dialects.oracle import VARCHAR2
16
16
  from sqlalchemy.types import Boolean, Date, DateTime, Enum, Float, Integer, Text, Time
17
17
 
@@ -149,6 +149,8 @@ class SQLTableGenerator(Generator):
149
149
  relative_slot_num: bool = False
150
150
  default_length_oracle: int = ORACLE_MAX_VARCHAR_LENGTH
151
151
  generate_abstract_class_ddl: bool = True
152
+ autogenerate_pk_index: bool = True
153
+ autogenerate_fk_index: bool = True
152
154
 
153
155
  def serialize(self, **kwargs: dict[str, Any]) -> str:
154
156
  return self.generate_ddl(**kwargs)
@@ -191,21 +193,34 @@ class SQLTableGenerator(Generator):
191
193
  return ""
192
194
  return txt.replace("\n", "")
193
195
 
196
+ def strip_dangling_whitespace(txt: str) -> str:
197
+ return "\n".join(line.rstrip() for line in txt.splitlines())
198
+
194
199
  # Currently SQLite dialect in SQLA does not generate comments; see
195
200
  # https://github.com/sqlalchemy/sqlalchemy/issues/1546#issuecomment-1067389172
196
201
  # As a workaround we add these as "--" comments via direct string manipulation
197
202
  include_comments = self.dialect == "sqlite"
198
203
  sv = SchemaView(schema)
199
204
 
205
+ # This is a safeguard for checking duplicate item names within a table
206
+ autogenerated_item_names = []
207
+
200
208
  # Iterate through the attributes in each class, creating Column objects.
201
209
  # This includes generating the appropriate column name, converting the range
202
210
  # into an SQL type, and adding a foreign key notation if appropriate.
203
211
  for cn, c in schema.classes.items():
212
+ desc = strip_newlines(c.description)
204
213
  if include_comments:
205
214
  if c.abstract:
206
- ddl_str += f'-- # Abstract Class: "{cn}" Description: "{strip_newlines(c.description)}"\n'
215
+ if desc:
216
+ ddl_str += f"-- # Abstract Class: {cn} Description: {desc}\n"
217
+ else:
218
+ ddl_str += f"-- # Abstract Class: {cn}\n"
207
219
  else:
208
- ddl_str += f'-- # Class: "{cn}" Description: "{strip_newlines(c.description)}"\n'
220
+ if desc:
221
+ ddl_str += f"-- # Class: {cn} Description: {desc}\n"
222
+ else:
223
+ ddl_str += f"-- # Class: {cn}\n"
209
224
  pk_slot = sv.get_identifier_slot(cn)
210
225
  if c.attributes:
211
226
  cols = []
@@ -220,15 +235,26 @@ class SQLTableGenerator(Generator):
220
235
  fk = sql_name(self.get_id_or_key(s.range, sv))
221
236
  args = [ForeignKey(fk)]
222
237
  field_type = self.get_sql_range(s, schema)
238
+ fk_index_cond = (s.key or s.identifier) and self.autogenerate_fk_index
239
+ pk_index_cond = is_pk and self.autogenerate_pk_index
240
+ is_index = fk_index_cond or pk_index_cond
223
241
  col = Column(
224
242
  sql_name(sn),
225
243
  field_type,
226
244
  *args,
227
245
  primary_key=is_pk,
228
246
  nullable=not s.required,
247
+ index=is_index,
229
248
  )
249
+ if is_index:
250
+ # this is what SQLAlchemy generates, TODO: perhaps we need to expand this
251
+ autogenerated_item_names.append(sql_name("ix_" + sql_name(cn) + "_" + sql_name(sn)))
230
252
  if include_comments:
231
- ddl_str += f"-- * Slot: {sn} Description: {strip_newlines(s.description)}\n"
253
+ desc = strip_newlines(s.description)
254
+ comment_str = f"-- * Slot: {sn}"
255
+ if desc:
256
+ comment_str += f" Description: {desc}"
257
+ ddl_str += comment_str + "\n"
232
258
  if s.description:
233
259
  col.comment = s.description
234
260
  cols.append(col)
@@ -239,9 +265,34 @@ class SQLTableGenerator(Generator):
239
265
  continue
240
266
  sql_uc = UniqueConstraint(*sql_names)
241
267
  cols.append(sql_uc)
268
+ # Anything that has a unique constraint should have an associated index with it
269
+ uc_index_name = sql_name(cn)
270
+ for name in sql_names:
271
+ uc_index_name = uc_index_name + "_" + name
272
+ uc_index_name = uc_index_name + "_idx"
273
+ is_duplicate = self.check_duplicate_entry_names(autogenerated_item_names, sql_name(uc_index_name))
274
+ if not is_duplicate:
275
+ sql_names = [sql_name(uc_index_name)] + sql_names
276
+ uc_index = Index(*sql_names)
277
+ cols.append(uc_index)
278
+ autogenerated_item_names.append(sql_name(uc_index_name))
242
279
  if not c.abstract or (c.abstract and self.generate_abstract_class_ddl):
280
+ for tag, annotation in c.annotations.items():
281
+ if tag == "index":
282
+ value_dict = {k: annotation for k, annotation in annotation.value._items()}
283
+ for key, value in value_dict.items():
284
+ test_name = sql_name(key)
285
+ name_exists = self.check_duplicate_entry_names(autogenerated_item_names, test_name)
286
+ if not name_exists:
287
+ value_sql_name = [sql_name(col_name) for col_name in value]
288
+ annotation_ind = Index(test_name, *value_sql_name)
289
+ autogenerated_item_names.append(test_name)
290
+ cols.append(annotation_ind)
243
291
  Table(sql_name(cn), schema_metadata, *cols, comment=str(c.description))
244
292
  schema_metadata.create_all(engine)
293
+ ddl_str = strip_dangling_whitespace(ddl_str)
294
+ if ddl_str[-1:] != "\n":
295
+ ddl_str += "\n"
245
296
  return ddl_str
246
297
 
247
298
  def get_oracle_sql_range(self, slot: SlotDefinition) -> Text | VARCHAR2 | None:
@@ -335,6 +386,15 @@ class SQLTableGenerator(Generator):
335
386
  pk_name = pk.alias if pk.alias else pk.name
336
387
  return f"{cn}.{pk_name}"
337
388
 
389
+ # Scans the schema for duplicate names that exist as a best practice
390
+ @staticmethod
391
+ def check_duplicate_entry_names(autogen: list, item_name: str) -> bool:
392
+ if item_name in autogen:
393
+ msg = f"Warning: {item_name} already exists, please generate a new name"
394
+ logger.warning(msg)
395
+ return True
396
+ return False
397
+
338
398
 
339
399
  @shared_arguments(SQLTableGenerator)
340
400
  @click.command(name="sqltables")
@@ -362,6 +422,12 @@ class SQLTableGenerator(Generator):
362
422
  show_default=True,
363
423
  help="Default length of varchar based arguments for oracle dialects",
364
424
  )
425
+ @click.option(
426
+ "--autogenerate_index",
427
+ default=False,
428
+ show_default=True,
429
+ help="Enable the creation of indexes on all columns generated",
430
+ )
365
431
  @click.option(
366
432
  "--generate_abstract_class_ddl",
367
433
  default=True,
@@ -1,6 +1,6 @@
1
1
  # String Template proposal
2
2
  The `string_template` attribute allows a Python [format string](https://docs.python.org/3/library/string.html#formatstrings)
3
- to be associated with a LinkML model `Element`.
3
+ to be associated with a LinkML model `Element`.
4
4
 
5
5
  ## Syntax
6
6
  ```yaml
@@ -29,7 +29,7 @@ class FirstClass(YAMLRoot):
29
29
  name: str = None
30
30
  age: Optional[int] = None
31
31
  gender: Optional[str] = None
32
-
32
+
33
33
  ...
34
34
 
35
35
  def __str__(self):
@@ -54,7 +54,7 @@ inst2 = FirstClass.parse("Jillian Johnson - a 93 year old female")
54
54
  print(str(inst2))
55
55
  > 'Jillian Johnson - a 93 year old female'
56
56
 
57
- # repr gives you the non-templated
57
+ # repr gives you the non-templated
58
58
  print(repr(inst2))
59
59
  > "FirstClass(name='Jillian Johnson', age=93, gender='female')"
60
60
 
@@ -71,4 +71,3 @@ name: Freddy Buster Jones
71
71
  age: 11
72
72
  gender: Undetermined
73
73
  ```
74
-
@@ -114,8 +114,7 @@ class TerminusdbGenerator(Generator):
114
114
 
115
115
  if rng not in XSD_Ok and slot.range not in self.schema.classes:
116
116
  raise Exception(
117
- f"slot range for {name} must be schema class or supported xsd type. "
118
- f"Range {rng} is of type {type(rng)}."
117
+ f"slot range for {name} must be schema class or supported xsd type. Range {rng} is of type {type(rng)}."
119
118
  )
120
119
 
121
120
  self.clswq.property(underscore(name), rng, label=name, description=slot.description)
linkml/linter/cli.py CHANGED
@@ -7,10 +7,9 @@ import click
7
7
  import yaml
8
8
 
9
9
  from linkml._version import __version__
10
-
11
- from .config.datamodel.config import RuleLevel
12
- from .formatters import JsonFormatter, MarkdownFormatter, TerminalFormatter, TsvFormatter
13
- from .linter import Linter
10
+ from linkml.linter.config.datamodel.config import RuleLevel
11
+ from linkml.linter.formatters import JsonFormatter, MarkdownFormatter, TerminalFormatter, TsvFormatter
12
+ from linkml.linter.linter import Linter
14
13
 
15
14
  YAML_SUFFIXES = [".yml", ".yaml"]
16
15
  DEFAULT_CONFIG_FILES = [".linkmllint.yaml", ".linkmllint.yml"]