linkml 1.7.7__py3-none-any.whl → 1.7.9__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/__init__.py +14 -0
- linkml/generators/csvgen.py +15 -5
- linkml/generators/docgen/class_diagram.md.jinja2 +19 -4
- linkml/generators/docgen/subset.md.jinja2 +7 -7
- linkml/generators/docgen.py +59 -14
- linkml/generators/graphqlgen.py +14 -16
- linkml/generators/jsonldcontextgen.py +3 -3
- linkml/generators/jsonldgen.py +4 -3
- linkml/generators/jsonschemagen.py +9 -0
- linkml/generators/markdowngen.py +341 -301
- linkml/generators/owlgen.py +87 -20
- linkml/generators/plantumlgen.py +9 -8
- linkml/generators/prefixmapgen.py +15 -23
- linkml/generators/protogen.py +23 -18
- linkml/generators/pydanticgen/array.py +15 -3
- linkml/generators/pydanticgen/pydanticgen.py +13 -2
- linkml/generators/pydanticgen/template.py +6 -4
- linkml/generators/pythongen.py +5 -5
- linkml/generators/rdfgen.py +14 -5
- linkml/generators/shaclgen.py +18 -6
- linkml/generators/shexgen.py +9 -7
- linkml/generators/sqlalchemygen.py +1 -0
- linkml/generators/sqltablegen.py +16 -1
- linkml/generators/summarygen.py +8 -2
- linkml/generators/terminusdbgen.py +2 -2
- linkml/generators/yumlgen.py +2 -2
- linkml/transformers/relmodel_transformer.py +21 -1
- linkml/utils/__init__.py +3 -0
- linkml/utils/deprecation.py +255 -0
- linkml/utils/generator.py +87 -61
- linkml/utils/rawloader.py +5 -1
- linkml/utils/schemaloader.py +2 -1
- linkml/utils/sqlutils.py +13 -14
- linkml/validator/cli.py +11 -0
- linkml/validator/plugins/jsonschema_validation_plugin.py +2 -0
- linkml/validator/plugins/shacl_validation_plugin.py +10 -4
- linkml/validator/report.py +1 -0
- linkml/workspaces/example_runner.py +2 -0
- {linkml-1.7.7.dist-info → linkml-1.7.9.dist-info}/METADATA +2 -1
- {linkml-1.7.7.dist-info → linkml-1.7.9.dist-info}/RECORD +43 -42
- {linkml-1.7.7.dist-info → linkml-1.7.9.dist-info}/LICENSE +0 -0
- {linkml-1.7.7.dist-info → linkml-1.7.9.dist-info}/WHEEL +0 -0
- {linkml-1.7.7.dist-info → linkml-1.7.9.dist-info}/entry_points.txt +0 -0
@@ -121,6 +121,7 @@ class SQLAlchemyGenerator(Generator):
|
|
121
121
|
is_join_table=lambda c: any(tag for tag in c.annotations.keys() if tag == "linkml:derived_from"),
|
122
122
|
classes=rel_schema_classes_ordered,
|
123
123
|
)
|
124
|
+
logging.debug(f"# Generated code:\n{code}")
|
124
125
|
return code
|
125
126
|
|
126
127
|
def serialize(self, **kwargs) -> str:
|
linkml/generators/sqltablegen.py
CHANGED
@@ -211,7 +211,22 @@ class SQLTableGenerator(Generator):
|
|
211
211
|
col.comment = s.description
|
212
212
|
cols.append(col)
|
213
213
|
for uc_name, uc in c.unique_keys.items():
|
214
|
-
|
214
|
+
|
215
|
+
def _sql_name(sn: str):
|
216
|
+
if sn in c.attributes:
|
217
|
+
return sql_name(sn)
|
218
|
+
else:
|
219
|
+
# for candidate in c.attributes.values():
|
220
|
+
# if "original_slot" in candidate.annotations:
|
221
|
+
# original = candidate.annotations["original_slot"]
|
222
|
+
# if original.value == sn:
|
223
|
+
# return sql_name(candidate.name)
|
224
|
+
return None
|
225
|
+
|
226
|
+
sql_names = [_sql_name(sn) for sn in uc.unique_key_slots]
|
227
|
+
if any(sn is None for sn in sql_names):
|
228
|
+
continue
|
229
|
+
sql_uc = UniqueConstraint(*sql_names)
|
215
230
|
cols.append(sql_uc)
|
216
231
|
Table(sql_name(cn), schema_metadata, *cols, comment=str(c.description))
|
217
232
|
schema_metadata.create_all(engine)
|
linkml/generators/summarygen.py
CHANGED
@@ -3,9 +3,9 @@
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
import os
|
6
|
-
import sys
|
7
6
|
from csv import DictWriter
|
8
7
|
from dataclasses import dataclass
|
8
|
+
from io import StringIO
|
9
9
|
from typing import Optional
|
10
10
|
|
11
11
|
import click
|
@@ -28,9 +28,12 @@ class SummaryGenerator(Generator):
|
|
28
28
|
slottab: Optional[DictWriter] = None
|
29
29
|
dialect: str = "excel-tab"
|
30
30
|
|
31
|
+
_str_io: Optional[StringIO] = None
|
32
|
+
|
31
33
|
def visit_schema(self, **_) -> None:
|
34
|
+
self._str_io = StringIO()
|
32
35
|
self.classtab = DictWriter(
|
33
|
-
|
36
|
+
self._str_io,
|
34
37
|
[
|
35
38
|
"Class Name",
|
36
39
|
"Parent Class",
|
@@ -79,6 +82,9 @@ class SummaryGenerator(Generator):
|
|
79
82
|
}
|
80
83
|
)
|
81
84
|
|
85
|
+
def end_schema(self, **kwargs) -> Optional[str]:
|
86
|
+
return self._str_io.getvalue()
|
87
|
+
|
82
88
|
|
83
89
|
@shared_arguments(SummaryGenerator)
|
84
90
|
@click.version_option(__version__, "-V", "--version")
|
@@ -71,8 +71,8 @@ class TerminusdbGenerator(Generator):
|
|
71
71
|
self.classes = []
|
72
72
|
self.raw_additions = []
|
73
73
|
|
74
|
-
def end_schema(self, **_) ->
|
75
|
-
|
74
|
+
def end_schema(self, **_) -> str:
|
75
|
+
return json.dumps(WQ().woql_and(*self.classes, *self.raw_additions).to_dict(), indent=2)
|
76
76
|
|
77
77
|
def visit_class(self, cls: ClassDefinition) -> bool:
|
78
78
|
self.clswq = WQ().add_class(camelcase(cls.name)).label(camelcase(cls.name)).description(be(cls.description))
|
linkml/generators/yumlgen.py
CHANGED
@@ -59,7 +59,7 @@ class YumlGenerator(Generator):
|
|
59
59
|
diagram_name: Optional[str] = None,
|
60
60
|
load_image: bool = True,
|
61
61
|
**_,
|
62
|
-
) ->
|
62
|
+
) -> Optional[str]:
|
63
63
|
if directory:
|
64
64
|
os.makedirs(directory, exist_ok=True)
|
65
65
|
if classes is not None:
|
@@ -106,7 +106,7 @@ class YumlGenerator(Generator):
|
|
106
106
|
else:
|
107
107
|
self.logger.error(f"{resp.reason} accessing {url}: {resp!r}")
|
108
108
|
else:
|
109
|
-
|
109
|
+
return str(YUML) + ",".join(yumlclassdef)
|
110
110
|
|
111
111
|
def class_box(self, cn: ClassDefinitionName) -> str:
|
112
112
|
"""Generate a box for the class. Populate its interior only if (a) it hasn't previously been generated and
|
@@ -226,6 +226,7 @@ class RelationalModelTransformer:
|
|
226
226
|
|
227
227
|
# TODO: separate out the logic into separate testable methods
|
228
228
|
target_sv.set_modified()
|
229
|
+
multivalued_slots_original = []
|
229
230
|
# post-process target schema
|
230
231
|
for cn, c in target_sv.all_classes().items():
|
231
232
|
if self.foreign_key_policy == ForeignKeyPolicy.NO_FOREIGN_KEYS:
|
@@ -243,6 +244,7 @@ class RelationalModelTransformer:
|
|
243
244
|
slot.inlined or slot.inlined_as_list or "shared" in slot.annotations
|
244
245
|
)
|
245
246
|
if slot.multivalued:
|
247
|
+
multivalued_slots_original.append(slot.name)
|
246
248
|
slot.multivalued = False
|
247
249
|
slot_name = slot.name
|
248
250
|
sn_singular = slot.singular_name if slot.singular_name else slot.name
|
@@ -332,6 +334,7 @@ class RelationalModelTransformer:
|
|
332
334
|
# add PK and FK anns
|
333
335
|
target_sv.set_modified()
|
334
336
|
fk_policy = self.foreign_key_policy
|
337
|
+
forward_map = {}
|
335
338
|
for c in target.classes.values():
|
336
339
|
if self.foreign_key_policy == ForeignKeyPolicy.NO_FOREIGN_KEYS:
|
337
340
|
continue
|
@@ -356,14 +359,31 @@ class RelationalModelTransformer:
|
|
356
359
|
# if it is already an injected backref, no need to re-inject
|
357
360
|
if "backref" not in a.annotations:
|
358
361
|
del c.attributes[a.name]
|
362
|
+
original_name = a.name
|
359
363
|
if "forwardref" not in a.annotations:
|
360
|
-
add_annotation(a, "original_slot",
|
364
|
+
add_annotation(a, "original_slot", original_name)
|
361
365
|
a.alias = f"{a.name}_{tc_pk_slot.name}"
|
362
366
|
a.name = a.alias
|
363
367
|
c.attributes[a.name] = a
|
368
|
+
forward_map[original_name] = a.name
|
364
369
|
ann = Annotation("foreign_key", f"{tc.name}.{tc_pk_slot.name}")
|
365
370
|
a.annotations[ann.tag] = ann
|
366
371
|
target_sv.set_modified()
|
372
|
+
# Rewrite unique key constraints
|
373
|
+
# - if a slot has a range of object, it may be renamed, e.g. person => person_id
|
374
|
+
# - if a slot is multivalued then it is translated to backref and the UC must be dropped
|
375
|
+
removed_ucs = []
|
376
|
+
for uc_name, uc in c.unique_keys.items():
|
377
|
+
if any(sn in multivalued_slots_original for sn in uc.unique_key_slots):
|
378
|
+
logging.warning(
|
379
|
+
f"Cannot represent uniqueness constraint {uc_name}. "
|
380
|
+
f"one of the slots {uc.unique_key_slots} is multivalued"
|
381
|
+
)
|
382
|
+
removed_ucs.append(uc_name)
|
383
|
+
new_slot_names = [forward_map.get(sn, sn) for sn in uc.unique_key_slots]
|
384
|
+
uc.unique_key_slots = new_slot_names
|
385
|
+
for uc_name in removed_ucs:
|
386
|
+
del c.unique_keys[uc_name]
|
367
387
|
|
368
388
|
result = TransformationResult(target, mappings=mappings)
|
369
389
|
return result
|
linkml/utils/__init__.py
CHANGED
@@ -0,0 +1,255 @@
|
|
1
|
+
"""
|
2
|
+
Utilities for deprecating functionality and dependencies.
|
3
|
+
|
4
|
+
- Emitting DeprecationWarnings
|
5
|
+
- Tracking deprecated and removed in versions
|
6
|
+
- Fail tests when something marked as removed_in is still present in the specified version
|
7
|
+
|
8
|
+
Initial draft for deprecating Pydantic 1, to make more general, needs
|
9
|
+
- function wrapper version
|
10
|
+
- ...
|
11
|
+
|
12
|
+
To deprecate something:
|
13
|
+
|
14
|
+
- Create a :class:`.Deprecation` object within the `DEPRECATIONS` tuple
|
15
|
+
- Use the :func:`.deprecation_warning` function wherever the deprecated feature would be used to emit the warning
|
16
|
+
|
17
|
+
"""
|
18
|
+
|
19
|
+
import re
|
20
|
+
import warnings
|
21
|
+
from dataclasses import dataclass
|
22
|
+
from importlib.metadata import version
|
23
|
+
from typing import Optional
|
24
|
+
|
25
|
+
# Stolen from https://github.com/pypa/packaging/blob/main/src/packaging/version.py
|
26
|
+
# Updated to include major, minor, and patch versions
|
27
|
+
PEP440_PATTERN = r"""
|
28
|
+
v?
|
29
|
+
(?:
|
30
|
+
(?:(?P<epoch>[0-9]+)!)? # epoch
|
31
|
+
(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)
|
32
|
+
(?P<pre> # pre-release
|
33
|
+
[-_\.]?
|
34
|
+
(?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
|
35
|
+
[-_\.]?
|
36
|
+
(?P<pre_n>[0-9]+)?
|
37
|
+
)?
|
38
|
+
(?P<post> # post release
|
39
|
+
(?:-(?P<post_n1>[0-9]+))
|
40
|
+
|
|
41
|
+
(?:
|
42
|
+
[-_\.]?
|
43
|
+
(?P<post_l>post|rev|r)
|
44
|
+
[-_\.]?
|
45
|
+
(?P<post_n2>[0-9]+)?
|
46
|
+
)
|
47
|
+
)?
|
48
|
+
(?P<dev> # dev release
|
49
|
+
[-_\.]?
|
50
|
+
(?P<dev_l>dev)
|
51
|
+
[-_\.]?
|
52
|
+
(?P<dev_n>[0-9]+)?
|
53
|
+
)?
|
54
|
+
)
|
55
|
+
(?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
|
56
|
+
"""
|
57
|
+
PEP440 = re.compile(r"^\s*" + PEP440_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE)
|
58
|
+
|
59
|
+
|
60
|
+
@dataclass
|
61
|
+
class SemVer:
|
62
|
+
"""
|
63
|
+
Representation of semantic version that supports inequality comparisons.
|
64
|
+
|
65
|
+
.. note::
|
66
|
+
|
67
|
+
The inequality methods _only_ test the numeric major, minor, and patch components
|
68
|
+
of the version - ie. they do not evaluate the prerelease versions as described in the semver
|
69
|
+
spec. This is not intended to be a general SemVer inequality calculator, but used
|
70
|
+
only for testing deprecations
|
71
|
+
|
72
|
+
"""
|
73
|
+
|
74
|
+
major: int = 0
|
75
|
+
minor: int = 0
|
76
|
+
patch: int = 0
|
77
|
+
epoch: Optional[int] = None
|
78
|
+
pre: Optional[str] = None
|
79
|
+
pre_l: Optional[str] = None
|
80
|
+
pre_n: Optional[str] = None
|
81
|
+
post: Optional[str] = None
|
82
|
+
post_n1: Optional[str] = None
|
83
|
+
post_l: Optional[str] = None
|
84
|
+
post_n2: Optional[str] = None
|
85
|
+
dev: Optional[str] = None
|
86
|
+
dev_l: Optional[str] = None
|
87
|
+
dev_n: Optional[str] = None
|
88
|
+
local: Optional[str] = None
|
89
|
+
|
90
|
+
def __post_init__(self):
|
91
|
+
self.major = int(self.major)
|
92
|
+
self.minor = int(self.minor)
|
93
|
+
self.patch = int(self.patch)
|
94
|
+
|
95
|
+
@classmethod
|
96
|
+
def from_str(cls, v: str) -> Optional["SemVer"]:
|
97
|
+
"""
|
98
|
+
Create a SemVer from a string using `PEP 440 <https://peps.python.org/pep-0440/>`_
|
99
|
+
syntax.
|
100
|
+
|
101
|
+
Examples:
|
102
|
+
|
103
|
+
.. code-block:: python
|
104
|
+
|
105
|
+
>>> version = SemVer.from_str("v0.1.0")
|
106
|
+
>>> print(version)
|
107
|
+
0.1.0
|
108
|
+
|
109
|
+
"""
|
110
|
+
match = PEP440.search(v)
|
111
|
+
if match is None:
|
112
|
+
return None
|
113
|
+
return SemVer(**match.groupdict())
|
114
|
+
|
115
|
+
@classmethod
|
116
|
+
def from_package(cls, package: str) -> "SemVer":
|
117
|
+
"""Get semver from package name"""
|
118
|
+
v = version(package)
|
119
|
+
return SemVer.from_str(v)
|
120
|
+
|
121
|
+
def __eq__(self, other: "SemVer"):
|
122
|
+
return self.major == other.major and self.minor == other.minor and self.patch == other.patch
|
123
|
+
|
124
|
+
def __lt__(self, other: "SemVer"):
|
125
|
+
# fall through each if elif only if version component is equal
|
126
|
+
for field in ("major", "minor", "patch"):
|
127
|
+
if getattr(self, field) < getattr(other, field):
|
128
|
+
return True
|
129
|
+
elif getattr(self, field) > getattr(other, field):
|
130
|
+
return False
|
131
|
+
|
132
|
+
# otherwise, equal (which is False)
|
133
|
+
return False
|
134
|
+
|
135
|
+
def __gt__(self, other: "SemVer"):
|
136
|
+
return not (self < other) and not (self == other)
|
137
|
+
|
138
|
+
def __le__(self, other: "SemVer"):
|
139
|
+
return (self < other) or (self == other)
|
140
|
+
|
141
|
+
def __ge__(self, other: "SemVer"):
|
142
|
+
return (self > other) or (self == other)
|
143
|
+
|
144
|
+
def __str__(self) -> str:
|
145
|
+
return ".".join([str(item) for item in [self.major, self.minor, self.patch]])
|
146
|
+
|
147
|
+
|
148
|
+
@dataclass
|
149
|
+
class Deprecation:
|
150
|
+
"""
|
151
|
+
Parameterization of a deprecation.
|
152
|
+
"""
|
153
|
+
|
154
|
+
name: str
|
155
|
+
"""Shorthand, unique name used to refer to this deprecation"""
|
156
|
+
message: str
|
157
|
+
"""Message to be displayed explaining the deprecation"""
|
158
|
+
deprecated_in: SemVer
|
159
|
+
"""Version that the feature was deprecated in"""
|
160
|
+
removed_in: Optional[SemVer] = None
|
161
|
+
"""Version that the feature will be removed in"""
|
162
|
+
recommendation: Optional[str] = None
|
163
|
+
"""Recommendation about what to do to replace the deprecated behavior"""
|
164
|
+
issue: Optional[int] = None
|
165
|
+
"""GitHub version describing deprecation"""
|
166
|
+
|
167
|
+
def __post_init__(self):
|
168
|
+
if self.deprecated_in is not None and isinstance(self.deprecated_in, str):
|
169
|
+
self.deprecated_in = SemVer.from_str(self.deprecated_in)
|
170
|
+
if self.removed_in is not None and isinstance(self.removed_in, str):
|
171
|
+
self.removed_in = SemVer.from_str(self.removed_in)
|
172
|
+
|
173
|
+
def __str__(self) -> str:
|
174
|
+
msg = f"[{self.name}] "
|
175
|
+
if self.removed:
|
176
|
+
msg += "REMOVED"
|
177
|
+
elif self.deprecated:
|
178
|
+
msg += "DEPRECATED"
|
179
|
+
|
180
|
+
msg += f"\n{self.message}"
|
181
|
+
msg += f"\nDeprecated In: {str(self.deprecated_in)}"
|
182
|
+
if self.removed_in is not None:
|
183
|
+
msg += f"\nRemoved In: {str(self.removed_in)}"
|
184
|
+
if self.recommendation is not None:
|
185
|
+
msg += f"\nRecommendation: {self.recommendation}"
|
186
|
+
if self.issue is not None:
|
187
|
+
msg += f"\nSee: https://github.com/linkml/linkml/issues/{self.issue}"
|
188
|
+
return msg
|
189
|
+
|
190
|
+
@property
|
191
|
+
def deprecated(self) -> bool:
|
192
|
+
return SemVer.from_package("linkml") >= self.deprecated_in
|
193
|
+
|
194
|
+
@property
|
195
|
+
def removed(self) -> bool:
|
196
|
+
if self.removed_in is None:
|
197
|
+
return False
|
198
|
+
return SemVer.from_package("linkml") >= self.removed_in
|
199
|
+
|
200
|
+
def warn(self, **kwargs):
|
201
|
+
if self.deprecated:
|
202
|
+
warnings.warn(message=str(self), category=DeprecationWarning, stacklevel=3, **kwargs)
|
203
|
+
|
204
|
+
|
205
|
+
DEPRECATIONS = (
|
206
|
+
Deprecation(
|
207
|
+
name="pydanticgen-v1",
|
208
|
+
deprecated_in=SemVer.from_str("1.7.5"),
|
209
|
+
removed_in=SemVer.from_str("1.8.0"),
|
210
|
+
message="Support for generating Pydantic v1.*.* models with pydanticgen is deprecated",
|
211
|
+
recommendation="Migrate any existing models to Pydantic v2",
|
212
|
+
issue=1925,
|
213
|
+
),
|
214
|
+
Deprecation(
|
215
|
+
name="pydantic-v1",
|
216
|
+
deprecated_in=SemVer.from_str("1.7.5"),
|
217
|
+
removed_in=SemVer.from_str("1.9.0"),
|
218
|
+
message=(
|
219
|
+
"LinkML will set a dependency of pydantic>=2 and become incompatible "
|
220
|
+
"with packages with pydantic<2 as a runtime dependency"
|
221
|
+
),
|
222
|
+
recommendation="Update dependent packages to use pydantic>=2",
|
223
|
+
issue=1925,
|
224
|
+
),
|
225
|
+
) # type: tuple[Deprecation, ...]
|
226
|
+
|
227
|
+
EMITTED = set() # type: set[str]
|
228
|
+
|
229
|
+
|
230
|
+
def deprecation_warning(name: str):
|
231
|
+
"""
|
232
|
+
Call this with the name of the deprecation object wherever the deprecated functionality will be used
|
233
|
+
|
234
|
+
This function will
|
235
|
+
|
236
|
+
- emit a warning if the current version is greater than ``deprecated_in``
|
237
|
+
- log that the deprecated feature was accessed in ``EMITTED`` for testing deprecations and muting warnings
|
238
|
+
|
239
|
+
"""
|
240
|
+
global DEPRECATIONS
|
241
|
+
global EMITTED
|
242
|
+
|
243
|
+
dep = [dep for dep in DEPRECATIONS if dep.name == name]
|
244
|
+
if len(dep) == 1:
|
245
|
+
dep = dep[0]
|
246
|
+
elif len(dep) > 1:
|
247
|
+
raise RuntimeError(f"Duplicate deprecations found with name {name}")
|
248
|
+
else:
|
249
|
+
EMITTED.add(name)
|
250
|
+
return
|
251
|
+
|
252
|
+
if dep.name not in EMITTED:
|
253
|
+
dep.warn()
|
254
|
+
|
255
|
+
EMITTED.add(name)
|