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.
Files changed (43) hide show
  1. linkml/__init__.py +14 -0
  2. linkml/generators/csvgen.py +15 -5
  3. linkml/generators/docgen/class_diagram.md.jinja2 +19 -4
  4. linkml/generators/docgen/subset.md.jinja2 +7 -7
  5. linkml/generators/docgen.py +59 -14
  6. linkml/generators/graphqlgen.py +14 -16
  7. linkml/generators/jsonldcontextgen.py +3 -3
  8. linkml/generators/jsonldgen.py +4 -3
  9. linkml/generators/jsonschemagen.py +9 -0
  10. linkml/generators/markdowngen.py +341 -301
  11. linkml/generators/owlgen.py +87 -20
  12. linkml/generators/plantumlgen.py +9 -8
  13. linkml/generators/prefixmapgen.py +15 -23
  14. linkml/generators/protogen.py +23 -18
  15. linkml/generators/pydanticgen/array.py +15 -3
  16. linkml/generators/pydanticgen/pydanticgen.py +13 -2
  17. linkml/generators/pydanticgen/template.py +6 -4
  18. linkml/generators/pythongen.py +5 -5
  19. linkml/generators/rdfgen.py +14 -5
  20. linkml/generators/shaclgen.py +18 -6
  21. linkml/generators/shexgen.py +9 -7
  22. linkml/generators/sqlalchemygen.py +1 -0
  23. linkml/generators/sqltablegen.py +16 -1
  24. linkml/generators/summarygen.py +8 -2
  25. linkml/generators/terminusdbgen.py +2 -2
  26. linkml/generators/yumlgen.py +2 -2
  27. linkml/transformers/relmodel_transformer.py +21 -1
  28. linkml/utils/__init__.py +3 -0
  29. linkml/utils/deprecation.py +255 -0
  30. linkml/utils/generator.py +87 -61
  31. linkml/utils/rawloader.py +5 -1
  32. linkml/utils/schemaloader.py +2 -1
  33. linkml/utils/sqlutils.py +13 -14
  34. linkml/validator/cli.py +11 -0
  35. linkml/validator/plugins/jsonschema_validation_plugin.py +2 -0
  36. linkml/validator/plugins/shacl_validation_plugin.py +10 -4
  37. linkml/validator/report.py +1 -0
  38. linkml/workspaces/example_runner.py +2 -0
  39. {linkml-1.7.7.dist-info → linkml-1.7.9.dist-info}/METADATA +2 -1
  40. {linkml-1.7.7.dist-info → linkml-1.7.9.dist-info}/RECORD +43 -42
  41. {linkml-1.7.7.dist-info → linkml-1.7.9.dist-info}/LICENSE +0 -0
  42. {linkml-1.7.7.dist-info → linkml-1.7.9.dist-info}/WHEEL +0 -0
  43. {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:
@@ -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
- sql_uc = UniqueConstraint(*[sql_name(sn) for sn in uc.unique_key_slots])
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)
@@ -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
- sys.stdout,
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, **_) -> None:
75
- print(json.dumps(WQ().woql_and(*self.classes, *self.raw_additions).to_dict(), indent=2))
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))
@@ -59,7 +59,7 @@ class YumlGenerator(Generator):
59
59
  diagram_name: Optional[str] = None,
60
60
  load_image: bool = True,
61
61
  **_,
62
- ) -> None:
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
- print(str(YUML) + ",".join(yumlclassdef), end="")
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", a.name)
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,3 @@
1
+ from linkml.utils.deprecation import deprecation_warning
2
+
3
+ __all__ = ["deprecation_warning"]
@@ -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)