linkml 1.9.2rc1__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.
- linkml/generators/PythonGenNotes.md +49 -49
- linkml/generators/README.md +2 -2
- linkml/generators/dbmlgen.py +0 -1
- linkml/generators/docgen/class.md.jinja2 +35 -11
- linkml/generators/docgen/class_diagram.md.jinja2 +15 -15
- linkml/generators/docgen/common_metadata.md.jinja2 +3 -5
- linkml/generators/docgen/enum.md.jinja2 +4 -5
- linkml/generators/docgen/index.md.jinja2 +5 -5
- linkml/generators/docgen/index.tex.jinja2 +1 -1
- linkml/generators/docgen/schema.md.jinja2 +1 -3
- linkml/generators/docgen/slot.md.jinja2 +6 -8
- linkml/generators/docgen/subset.md.jinja2 +4 -7
- linkml/generators/docgen/type.md.jinja2 +2 -3
- linkml/generators/docgen.py +92 -5
- linkml/generators/erdiagramgen.py +1 -1
- linkml/generators/graphqlgen.py +1 -1
- linkml/generators/javagen/example_template.java.jinja2 +0 -1
- linkml/generators/jsonldcontextgen.py +0 -1
- linkml/generators/jsonldgen.py +3 -1
- linkml/generators/jsonschemagen.py +2 -2
- linkml/generators/linkmlgen.py +1 -1
- linkml/generators/markdowngen.py +20 -9
- linkml/generators/mermaidclassdiagramgen.py +4 -0
- linkml/generators/projectgen.py +17 -20
- linkml/generators/pydanticgen/includes.py +5 -5
- linkml/generators/pydanticgen/pydanticgen.py +2 -3
- linkml/generators/pydanticgen/template.py +1 -1
- linkml/generators/pydanticgen/templates/base_model.py.jinja +1 -1
- linkml/generators/pydanticgen/templates/class.py.jinja +1 -1
- linkml/generators/pydanticgen/templates/conditional_import.py.jinja +1 -1
- linkml/generators/pydanticgen/templates/footer.py.jinja +1 -1
- linkml/generators/pydanticgen/templates/imports.py.jinja +1 -1
- linkml/generators/python/python_ifabsent_processor.py +1 -1
- linkml/generators/pythongen.py +9 -8
- linkml/generators/shacl/shacl_ifabsent_processor.py +0 -1
- linkml/generators/shaclgen.py +1 -1
- linkml/generators/sparqlgen.py +1 -1
- linkml/generators/sqlalchemy/__init__.py +0 -4
- linkml/generators/sqlalchemygen.py +9 -15
- linkml/generators/sqltablegen.py +70 -4
- linkml/generators/string_template.md +3 -4
- linkml/generators/terminusdbgen.py +1 -2
- linkml/linter/cli.py +3 -4
- linkml/linter/config/datamodel/config.py +286 -135
- linkml/linter/config/datamodel/config.yaml +26 -11
- linkml/linter/config/default.yaml +6 -0
- linkml/linter/config/recommended.yaml +6 -0
- linkml/linter/linter.py +10 -6
- linkml/linter/rules.py +144 -46
- linkml/transformers/relmodel_transformer.py +2 -1
- linkml/utils/generator.py +3 -0
- linkml/utils/logictools.py +5 -5
- linkml/utils/schemaloader.py +4 -4
- linkml/utils/schemasynopsis.py +11 -7
- linkml/workspaces/datamodel/workspaces.yaml +0 -5
- {linkml-1.9.2rc1.dist-info → linkml-1.9.3rc2.dist-info}/LICENSE +1 -1
- {linkml-1.9.2rc1.dist-info → linkml-1.9.3rc2.dist-info}/METADATA +2 -2
- {linkml-1.9.2rc1.dist-info → linkml-1.9.3rc2.dist-info}/RECORD +60 -60
- {linkml-1.9.2rc1.dist-info → linkml-1.9.3rc2.dist-info}/WHEEL +0 -0
- {linkml-1.9.2rc1.dist-info → linkml-1.9.3rc2.dist-info}/entry_points.txt +0 -0
@@ -6,7 +6,6 @@ from linkml.generators.shacl.shacl_data_type import ShaclDataType
|
|
6
6
|
|
7
7
|
|
8
8
|
class ShaclIfAbsentProcessor(IfAbsentProcessor):
|
9
|
-
|
10
9
|
def map_custom_default_values(self, default_value: str, slot: SlotDefinition, cls: ClassDefinition) -> (bool, str):
|
11
10
|
if default_value == "class_curie":
|
12
11
|
class_uri = self.schema_view.get_uri(cls, expand=True)
|
linkml/generators/shaclgen.py
CHANGED
@@ -141,7 +141,7 @@ class ShaclGenerator(Generator):
|
|
141
141
|
# slot definition, as both are mapped to sh:in in SHACL
|
142
142
|
if s.equals_string or s.equals_string_in:
|
143
143
|
error = "'equals_string'/'equals_string_in' and 'any_of' are mutually exclusive"
|
144
|
-
raise ValueError(f
|
144
|
+
raise ValueError(f"{TypedNode.yaml_loc(str(s), suffix='')} {error}")
|
145
145
|
|
146
146
|
or_node = BNode()
|
147
147
|
prop_pv(SH["or"], or_node)
|
linkml/generators/sparqlgen.py
CHANGED
@@ -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
|
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,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
|
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:
|
48
|
+
template: TemplateEnum | None = None
|
47
49
|
|
48
50
|
# ObjectVars
|
49
|
-
original_schema:
|
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:
|
60
|
+
model_path: str | None = None,
|
59
61
|
no_model_import: bool = False,
|
60
|
-
template:
|
61
|
-
foreign_key_policy:
|
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
|
|
linkml/generators/sqltablegen.py
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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 .
|
12
|
-
from .
|
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"]
|