linkml 1.7.6__py3-none-any.whl → 1.7.7__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/docgen/slot.md.jinja2 +4 -3
- linkml/generators/jsonldcontextgen.py +32 -12
- linkml/generators/owlgen.py +2 -0
- linkml/generators/pydanticgen/array.py +457 -0
- linkml/generators/pydanticgen/black.py +29 -0
- linkml/generators/pydanticgen/build.py +79 -0
- linkml/generators/pydanticgen/pydanticgen.py +70 -15
- linkml/generators/pydanticgen/template.py +83 -10
- linkml/generators/pydanticgen/templates/base_model.py.jinja +3 -1
- linkml/generators/shacl/ifabsent_processor.py +22 -4
- linkml/generators/shaclgen.py +2 -2
- linkml/utils/generator.py +1 -1
- {linkml-1.7.6.dist-info → linkml-1.7.7.dist-info}/METADATA +6 -4
- {linkml-1.7.6.dist-info → linkml-1.7.7.dist-info}/RECORD +17 -14
- {linkml-1.7.6.dist-info → linkml-1.7.7.dist-info}/LICENSE +0 -0
- {linkml-1.7.6.dist-info → linkml-1.7.7.dist-info}/WHEEL +0 -0
- {linkml-1.7.6.dist-info → linkml-1.7.7.dist-info}/entry_points.txt +0 -0
@@ -46,14 +46,15 @@ URI: {{ gen.uri_link(element) }}
|
|
46
46
|
<!-- no inheritance hierarchy -->
|
47
47
|
{% endif %}
|
48
48
|
|
49
|
-
{%
|
49
|
+
{% set classes_by_slot = schemaview.get_classes_by_slot(element, include_induced=True) %}
|
50
|
+
{% if classes_by_slot %}
|
50
51
|
|
51
52
|
## Applicable Classes
|
52
53
|
|
53
54
|
| Name | Description | Modifies Slot |
|
54
55
|
| --- | --- | --- |
|
55
|
-
{% for c in
|
56
|
-
{{ gen.link(c) }} | {{ schemaview.get_class(c).description|enshorten }} | {% if c in schemaview.get_classes_modifying_slot(element) %} yes {% else %} no {% endif %} |
|
56
|
+
{% for c in classes_by_slot -%}
|
57
|
+
| {{ gen.link(c) }} | {{ schemaview.get_class(c).description|enshorten }} | {% if c in schemaview.get_classes_modifying_slot(element) %} yes {% else %} no {% endif %} |
|
57
58
|
{% endfor %}
|
58
59
|
|
59
60
|
{% endif %}
|
@@ -6,7 +6,7 @@ Generate JSON-LD contexts
|
|
6
6
|
import os
|
7
7
|
import re
|
8
8
|
from dataclasses import dataclass, field
|
9
|
-
from typing import Dict, Optional, Set, Union
|
9
|
+
from typing import Any, Dict, Optional, Set, Union
|
10
10
|
|
11
11
|
import click
|
12
12
|
from jsonasobj2 import JsonObj, as_json
|
@@ -133,16 +133,13 @@ class ContextGenerator(Generator):
|
|
133
133
|
class_def = {}
|
134
134
|
cn = camelcase(cls.name)
|
135
135
|
self.add_mappings(cls)
|
136
|
-
|
137
|
-
|
138
|
-
class_def["@id"] = (cls_uri_prefix + ":" + cls_uri_suffix) if cls_uri_prefix else cls.class_uri
|
139
|
-
if cls_uri_prefix:
|
140
|
-
self.add_prefix(cls_uri_prefix)
|
136
|
+
|
137
|
+
self._build_element_id(class_def, cls.class_uri)
|
141
138
|
if class_def:
|
142
139
|
self.slot_class_maps[cn] = class_def
|
143
140
|
|
144
141
|
# We don't bother to visit class slots - just all slots
|
145
|
-
return
|
142
|
+
return True
|
146
143
|
|
147
144
|
def visit_slot(self, aliased_slot_name: str, slot: SlotDefinition) -> None:
|
148
145
|
if slot.identifier:
|
@@ -169,15 +166,38 @@ class ContextGenerator(Generator):
|
|
169
166
|
slot_def["@type"] = "@id"
|
170
167
|
else:
|
171
168
|
slot_def["@type"] = range_type.uri
|
172
|
-
|
173
|
-
|
174
|
-
slot_def["@id"] = slot.slot_uri
|
175
|
-
if slot_prefix:
|
176
|
-
self.add_prefix(slot_prefix)
|
169
|
+
|
170
|
+
self._build_element_id(slot_def, slot.slot_uri)
|
177
171
|
self.add_mappings(slot)
|
178
172
|
if slot_def:
|
179
173
|
self.context_body[underscore(aliased_slot_name)] = slot_def
|
180
174
|
|
175
|
+
def _build_element_id(self, definition: Any, uri: str) -> None:
|
176
|
+
"""
|
177
|
+
Defines the elements @id attribute according to the default namespace prefix of the schema.
|
178
|
+
|
179
|
+
The @id namespace prefix is added only if it doesn't correspond to the default schema namespace prefix
|
180
|
+
whether it is in URI format or as an alias.
|
181
|
+
|
182
|
+
@param definition: the element (class or slot) definition
|
183
|
+
@param uri: the uri of the element (class or slot)
|
184
|
+
@return: None
|
185
|
+
"""
|
186
|
+
uri_prefix, uri_suffix = self.namespaces.prefix_suffix(uri)
|
187
|
+
is_default_namespace = uri_prefix == self.context_body["@vocab"] or uri_prefix == self.namespaces.prefix_for(
|
188
|
+
self.context_body["@vocab"]
|
189
|
+
)
|
190
|
+
|
191
|
+
if not uri_prefix and not uri_suffix:
|
192
|
+
definition["@id"] = uri
|
193
|
+
elif not uri_prefix or is_default_namespace:
|
194
|
+
definition["@id"] = uri_suffix
|
195
|
+
else:
|
196
|
+
definition["@id"] = (uri_prefix + ":" + uri_suffix) if uri_prefix else uri
|
197
|
+
|
198
|
+
if uri_prefix and not is_default_namespace:
|
199
|
+
self.add_prefix(uri_prefix)
|
200
|
+
|
181
201
|
def serialize(self, base: Optional[Union[str, Namespace]] = None, **kwargs) -> str:
|
182
202
|
return super().serialize(base=base, **kwargs)
|
183
203
|
|
linkml/generators/owlgen.py
CHANGED
@@ -274,6 +274,8 @@ class OwlSchemaGenerator(Generator):
|
|
274
274
|
self.graph.add((uri, metaslot_uri, obj))
|
275
275
|
|
276
276
|
for k, v in e.annotations.items():
|
277
|
+
if isinstance(v, dict) or isinstance(v, list):
|
278
|
+
continue
|
277
279
|
if ":" not in k:
|
278
280
|
default_prefix = this_sv.schema.default_prefix
|
279
281
|
if default_prefix in this_sv.schema.prefixes:
|
@@ -0,0 +1,457 @@
|
|
1
|
+
import sys
|
2
|
+
from abc import ABC, abstractmethod
|
3
|
+
from enum import Enum
|
4
|
+
from typing import (
|
5
|
+
Any,
|
6
|
+
ClassVar,
|
7
|
+
Generic,
|
8
|
+
Iterable,
|
9
|
+
List,
|
10
|
+
Optional,
|
11
|
+
Type,
|
12
|
+
TypeVar,
|
13
|
+
Union,
|
14
|
+
get_args,
|
15
|
+
)
|
16
|
+
|
17
|
+
from linkml_runtime.linkml_model import Element
|
18
|
+
from linkml_runtime.linkml_model.meta import ArrayExpression, DimensionExpression
|
19
|
+
from pydantic import VERSION as PYDANTIC_VERSION
|
20
|
+
|
21
|
+
if int(PYDANTIC_VERSION[0]) < 2:
|
22
|
+
pass
|
23
|
+
else:
|
24
|
+
from pydantic import GetCoreSchemaHandler
|
25
|
+
from pydantic_core import CoreSchema, core_schema
|
26
|
+
|
27
|
+
if sys.version_info.minor <= 8:
|
28
|
+
from typing_extensions import Annotated
|
29
|
+
else:
|
30
|
+
from typing import Annotated
|
31
|
+
|
32
|
+
from linkml.generators.pydanticgen.build import SlotResult
|
33
|
+
from linkml.generators.pydanticgen.template import Import, Imports, ObjectImport
|
34
|
+
|
35
|
+
|
36
|
+
class ArrayRepresentation(Enum):
|
37
|
+
LIST = "list"
|
38
|
+
NPARRAY = "nparray" # numpy and nptyping must be installed to use this
|
39
|
+
|
40
|
+
|
41
|
+
_BOUNDED_ARRAY_FIELDS = ("exact_number_dimensions", "minimum_number_dimensions", "maximum_number_dimensions")
|
42
|
+
|
43
|
+
_T = TypeVar("_T")
|
44
|
+
_RecursiveListType = Iterable[Union[_T, Iterable["_RecursiveListType"]]]
|
45
|
+
if int(PYDANTIC_VERSION[0]) >= 2:
|
46
|
+
|
47
|
+
class AnyShapeArrayType(Generic[_T]):
|
48
|
+
@classmethod
|
49
|
+
def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler) -> CoreSchema:
|
50
|
+
# double-nested parameterized types here
|
51
|
+
# source_type: List[Union[T,List[...]]]
|
52
|
+
item_type = Any if get_args(get_args(source_type)[0])[0] is _T else get_args(get_args(source_type)[0])[0]
|
53
|
+
|
54
|
+
item_schema = handler.generate_schema(item_type)
|
55
|
+
if item_schema.get("type", "any") != "any":
|
56
|
+
item_schema["strict"] = True
|
57
|
+
array_ref = f"any-shape-array-{item_type.__name__}"
|
58
|
+
|
59
|
+
schema = core_schema.definitions_schema(
|
60
|
+
core_schema.list_schema(core_schema.definition_reference_schema(array_ref)),
|
61
|
+
[
|
62
|
+
core_schema.union_schema(
|
63
|
+
[
|
64
|
+
core_schema.list_schema(core_schema.definition_reference_schema(array_ref)),
|
65
|
+
item_schema,
|
66
|
+
],
|
67
|
+
ref=array_ref,
|
68
|
+
)
|
69
|
+
],
|
70
|
+
)
|
71
|
+
|
72
|
+
return schema
|
73
|
+
|
74
|
+
AnyShapeArray = Annotated[_RecursiveListType, AnyShapeArrayType]
|
75
|
+
|
76
|
+
_AnyShapeArrayImports = (
|
77
|
+
Imports()
|
78
|
+
+ Import(
|
79
|
+
module="typing",
|
80
|
+
objects=[
|
81
|
+
ObjectImport(name="Annotated"),
|
82
|
+
ObjectImport(name="Generic"),
|
83
|
+
ObjectImport(name="Iterable"),
|
84
|
+
ObjectImport(name="TypeVar"),
|
85
|
+
ObjectImport(name="Union"),
|
86
|
+
ObjectImport(name="get_args"),
|
87
|
+
],
|
88
|
+
)
|
89
|
+
+ Import(module="pydantic", objects=[ObjectImport(name="GetCoreSchemaHandler")])
|
90
|
+
+ Import(module="pydantic_core", objects=[ObjectImport(name="CoreSchema"), ObjectImport(name="core_schema")])
|
91
|
+
)
|
92
|
+
|
93
|
+
# annotated types are special and inspect.getsource() can't stringify them
|
94
|
+
_AnyShapeArrayInjects = [
|
95
|
+
'_T = TypeVar("_T")',
|
96
|
+
'_RecursiveListType = Iterable[Union[_T, Iterable["_RecursiveListType"]]]',
|
97
|
+
AnyShapeArrayType,
|
98
|
+
"AnyShapeArray = Annotated[_RecursiveListType, AnyShapeArrayType]",
|
99
|
+
]
|
100
|
+
|
101
|
+
else:
|
102
|
+
|
103
|
+
class AnyShapeArray(Generic[_T]):
|
104
|
+
type_: Type[Any] = Any
|
105
|
+
|
106
|
+
def __class_getitem__(cls, item):
|
107
|
+
alias = type(f"AnyShape_{str(item.__name__)}", (AnyShapeArray,), {"type_": item})
|
108
|
+
alias.type_ = item
|
109
|
+
return alias
|
110
|
+
|
111
|
+
@classmethod
|
112
|
+
def __get_validators__(cls):
|
113
|
+
yield cls.validate
|
114
|
+
|
115
|
+
@classmethod
|
116
|
+
def __modify_schema__(cls, field_schema):
|
117
|
+
try:
|
118
|
+
item_type = field_schema["allOf"][0]["type"]
|
119
|
+
type_schema = {"type": item_type}
|
120
|
+
del field_schema["allOf"]
|
121
|
+
except KeyError as e:
|
122
|
+
if "allOf" in str(e):
|
123
|
+
item_type = "Any"
|
124
|
+
type_schema = {}
|
125
|
+
else:
|
126
|
+
raise e
|
127
|
+
|
128
|
+
array_id = f"#any-shape-array-{item_type}"
|
129
|
+
field_schema["anyOf"] = [
|
130
|
+
type_schema,
|
131
|
+
{"type": "array", "items": {"$ref": array_id}},
|
132
|
+
]
|
133
|
+
field_schema["$id"] = array_id
|
134
|
+
|
135
|
+
@classmethod
|
136
|
+
def validate(cls, v: Union[List[_T], list]):
|
137
|
+
if str(type(v)) == "<class 'numpy.ndarray'>":
|
138
|
+
v = v.tolist()
|
139
|
+
|
140
|
+
if not isinstance(v, list):
|
141
|
+
raise TypeError(f"Must be a list of lists! got {v}")
|
142
|
+
|
143
|
+
def _validate(_v: Union[List[_T], list]):
|
144
|
+
for item in _v:
|
145
|
+
if isinstance(item, list):
|
146
|
+
_validate(item)
|
147
|
+
else:
|
148
|
+
try:
|
149
|
+
anytype = cls.type_.__name__ in ("AnyType", "Any")
|
150
|
+
except AttributeError:
|
151
|
+
# in python 3.8 and 3.9, `typing.Any` has no __name__
|
152
|
+
anytype = str(cls.type_).split(".")[-1] in ("AnyType", "Any")
|
153
|
+
|
154
|
+
if not anytype and not isinstance(item, cls.type_):
|
155
|
+
raise TypeError(
|
156
|
+
(
|
157
|
+
f"List items must be list of lists, or the type used in "
|
158
|
+
f"the subscript ({cls.type_}. Got item {item} and outer value {v}"
|
159
|
+
)
|
160
|
+
)
|
161
|
+
return _v
|
162
|
+
|
163
|
+
return _validate(v)
|
164
|
+
|
165
|
+
_AnyShapeArrayImports = Imports() + Import(
|
166
|
+
module="typing",
|
167
|
+
objects=[ObjectImport(name="Generic"), ObjectImport(name="TypeVar"), ObjectImport(name="_GenericAlias")],
|
168
|
+
)
|
169
|
+
_AnyShapeArrayInjects = [
|
170
|
+
'_T = TypeVar("_T")',
|
171
|
+
AnyShapeArray,
|
172
|
+
]
|
173
|
+
|
174
|
+
_ConListImports = Imports() + Import(module="pydantic", objects=[ObjectImport(name="conlist")])
|
175
|
+
|
176
|
+
|
177
|
+
class ArrayRangeGenerator(ABC):
|
178
|
+
"""
|
179
|
+
Metaclass for generating a given format of array annotation.
|
180
|
+
|
181
|
+
See :ref:`array-forms` for more details on array range forms.
|
182
|
+
|
183
|
+
These classes do only enough validation of the array specification to decide
|
184
|
+
which kind of representation to generate. Proper value validation should
|
185
|
+
happen elsewhere (ie. in the metamodel and generated :class:`.ArrayExpression` class.)
|
186
|
+
|
187
|
+
Each of the array representation generation methods should be able to handle
|
188
|
+
the supported pydantic versions (currently still 1 and 2).
|
189
|
+
|
190
|
+
Notes:
|
191
|
+
|
192
|
+
When checking for array specification, recall that there is a semantic difference between
|
193
|
+
``None`` and ``False`` , particularly for :attr:`.ArrayExpression.max_number_dimensions` -
|
194
|
+
check for absence of specification with ``is None`` rather than checking for truthiness/falsiness
|
195
|
+
(unless that's what you intend to do ofc ;)
|
196
|
+
|
197
|
+
Attributes:
|
198
|
+
array (:class:`.ArrayExpression` ): Array to create an annotation for
|
199
|
+
dtype (Union[str, :class:`.Element` ): dtype of the entire array as a string
|
200
|
+
pydantic_ver (str): Pydantic version to generate array form for -
|
201
|
+
currently only pydantic 1 and 2 are differentiated, and pydantic 1 will be deprecated soon.
|
202
|
+
|
203
|
+
"""
|
204
|
+
|
205
|
+
REPR: ClassVar[ArrayRepresentation]
|
206
|
+
|
207
|
+
def __init__(
|
208
|
+
self, array: Optional[ArrayExpression], dtype: Union[str, Element], pydantic_ver: str = PYDANTIC_VERSION
|
209
|
+
):
|
210
|
+
self.array = array
|
211
|
+
self.dtype = dtype
|
212
|
+
self.pydantic_ver = pydantic_ver
|
213
|
+
|
214
|
+
def make(self) -> SlotResult:
|
215
|
+
"""Create the string form of the array representation"""
|
216
|
+
if not self.array.dimensions and not self.has_bounded_dimensions:
|
217
|
+
# any-shaped array
|
218
|
+
return self.any_shape(self.array)
|
219
|
+
elif not self.array.dimensions and self.has_bounded_dimensions:
|
220
|
+
return self.bounded_dimensions(self.array)
|
221
|
+
elif self.array.dimensions and not self.has_bounded_dimensions:
|
222
|
+
return self.parameterized_dimensions(self.array)
|
223
|
+
else:
|
224
|
+
return self.complex_dimensions(self.array)
|
225
|
+
|
226
|
+
@property
|
227
|
+
def has_bounded_dimensions(self) -> bool:
|
228
|
+
"""Whether the :class:`.ArrayExpression` has some shape specification aside from ``dimensions``"""
|
229
|
+
return any([getattr(self.array, arr_field, None) is not None for arr_field in _BOUNDED_ARRAY_FIELDS])
|
230
|
+
|
231
|
+
@classmethod
|
232
|
+
def get_generator(cls, repr: ArrayRepresentation) -> Type["ArrayRangeGenerator"]:
|
233
|
+
"""Get the generator class for a given array representation"""
|
234
|
+
for subclass in cls.__subclasses__():
|
235
|
+
if repr in (subclass.REPR, subclass.REPR.value):
|
236
|
+
return subclass
|
237
|
+
raise ValueError(f"Generator for array representation {repr} not found!")
|
238
|
+
|
239
|
+
@abstractmethod
|
240
|
+
def any_shape(self, array: Optional[ArrayRepresentation] = None) -> SlotResult:
|
241
|
+
"""Any shaped array!"""
|
242
|
+
pass
|
243
|
+
|
244
|
+
@abstractmethod
|
245
|
+
def bounded_dimensions(self, array: ArrayExpression) -> SlotResult:
|
246
|
+
"""Array shape specified numerically, without axis parameterization"""
|
247
|
+
pass
|
248
|
+
|
249
|
+
@abstractmethod
|
250
|
+
def parameterized_dimensions(self, array: ArrayExpression) -> SlotResult:
|
251
|
+
"""Array shape specified with ``dimensions`` without additional parameterized dimensions"""
|
252
|
+
pass
|
253
|
+
|
254
|
+
@abstractmethod
|
255
|
+
def complex_dimensions(self, array: ArrayExpression) -> SlotResult:
|
256
|
+
"""Array shape with both ``parameterized`` and ``bounded`` dimensions"""
|
257
|
+
pass
|
258
|
+
|
259
|
+
|
260
|
+
class ListOfListsArray(ArrayRangeGenerator):
|
261
|
+
"""
|
262
|
+
Represent arrays as lists of lists!
|
263
|
+
|
264
|
+
TODO: Move all validation of values (eg. anywhere we raise a ValueError) to the ArrayExpression
|
265
|
+
dataclass and out of the generator class
|
266
|
+
"""
|
267
|
+
|
268
|
+
REPR = ArrayRepresentation.LIST
|
269
|
+
|
270
|
+
@staticmethod
|
271
|
+
def _list_of_lists(dimensions: int, dtype: str) -> str:
|
272
|
+
return ("List[" * dimensions) + dtype + ("]" * dimensions)
|
273
|
+
|
274
|
+
@staticmethod
|
275
|
+
def _parameterized_dimension(dimension: DimensionExpression, dtype: str) -> SlotResult:
|
276
|
+
# TODO: Preserve label representation in some readable way! doing the MVP now of using conlist
|
277
|
+
if dimension.exact_cardinality and (dimension.minimum_cardinality or dimension.maximum_cardinality):
|
278
|
+
raise ValueError("Can only specify EITHER exact_cardinality OR minimum/maximum cardinality")
|
279
|
+
elif dimension.exact_cardinality:
|
280
|
+
dmin = dimension.exact_cardinality
|
281
|
+
dmax = dimension.exact_cardinality
|
282
|
+
elif dimension.minimum_cardinality or dimension.maximum_cardinality:
|
283
|
+
dmin = dimension.minimum_cardinality
|
284
|
+
dmax = dimension.maximum_cardinality
|
285
|
+
else:
|
286
|
+
# TODO: handle labels for labeled but unshaped arrays
|
287
|
+
return SlotResult(annotation="List[" + dtype + "]")
|
288
|
+
|
289
|
+
items = []
|
290
|
+
if int(PYDANTIC_VERSION[0]) >= 2:
|
291
|
+
if dmin is not None:
|
292
|
+
items.append(f"min_length={dmin}")
|
293
|
+
if dmax is not None:
|
294
|
+
items.append(f"max_length={dmax}")
|
295
|
+
else:
|
296
|
+
if dmin is not None:
|
297
|
+
items.append(f"min_items={dmin}")
|
298
|
+
if dmax is not None:
|
299
|
+
items.append(f"max_items={dmax}")
|
300
|
+
items.append(f"item_type={dtype}")
|
301
|
+
items = ", ".join(items)
|
302
|
+
annotation = f"conlist({items})"
|
303
|
+
|
304
|
+
return SlotResult(annotation=annotation, imports=_ConListImports)
|
305
|
+
|
306
|
+
def any_shape(self, array: Optional[ArrayExpression] = None, with_inner_union: bool = False) -> SlotResult:
|
307
|
+
"""
|
308
|
+
An AnyShaped array (using :class:`.AnyShapeArray` )
|
309
|
+
|
310
|
+
Args:
|
311
|
+
array (:class:`.ArrayExpression`): The array expression (not used)
|
312
|
+
with_inner_union (bool): If ``True`` , the innermost type is a ``Union`` of the ``AnyShapeArray`` class
|
313
|
+
and ``dtype`` (default: ``False`` )
|
314
|
+
|
315
|
+
"""
|
316
|
+
if self.dtype in ("Any", "AnyType"):
|
317
|
+
annotation = "AnyShapeArray"
|
318
|
+
else:
|
319
|
+
annotation = f"AnyShapeArray[{self.dtype}]"
|
320
|
+
|
321
|
+
if with_inner_union:
|
322
|
+
annotation = f"Union[{annotation}, {self.dtype}]"
|
323
|
+
return SlotResult(annotation=annotation, injected_classes=_AnyShapeArrayInjects, imports=_AnyShapeArrayImports)
|
324
|
+
|
325
|
+
def bounded_dimensions(self, array: ArrayExpression) -> SlotResult:
|
326
|
+
"""
|
327
|
+
A nested series of ``List[]`` annotations with :attr:`.dtype` at the center.
|
328
|
+
|
329
|
+
When an array expression allows for a range of dimensions, each set of ``List`` s is joined by a ``Union`` .
|
330
|
+
"""
|
331
|
+
if array.exact_number_dimensions or (
|
332
|
+
array.minimum_number_dimensions
|
333
|
+
and array.maximum_number_dimensions
|
334
|
+
and array.minimum_number_dimensions == array.maximum_number_dimensions
|
335
|
+
):
|
336
|
+
exact_dims = array.exact_number_dimensions or array.minimum_number_dimensions
|
337
|
+
return SlotResult(annotation=self._list_of_lists(exact_dims, self.dtype))
|
338
|
+
elif not array.maximum_number_dimensions and (
|
339
|
+
array.minimum_number_dimensions is None or array.minimum_number_dimensions == 1
|
340
|
+
):
|
341
|
+
return self.any_shape()
|
342
|
+
elif array.maximum_number_dimensions:
|
343
|
+
# e.g., if min = 2, max = 3, annotation = Union[List[List[dtype]], List[List[List[dtype]]]]
|
344
|
+
min_dims = array.minimum_number_dimensions if array.minimum_number_dimensions is not None else 1
|
345
|
+
annotations = [
|
346
|
+
self._list_of_lists(i, self.dtype) for i in range(min_dims, array.maximum_number_dimensions + 1)
|
347
|
+
]
|
348
|
+
# TODO: Format this nicely!
|
349
|
+
return SlotResult(annotation="Union[" + ", ".join(annotations) + "]")
|
350
|
+
else:
|
351
|
+
# min specified with no max
|
352
|
+
# e.g., if min = 3, annotation = List[List[AnyShapeArray[dtype]]]
|
353
|
+
return SlotResult(
|
354
|
+
annotation=self._list_of_lists(array.minimum_number_dimensions - 1, self.any_shape().annotation),
|
355
|
+
injected_classes=_AnyShapeArrayInjects,
|
356
|
+
imports=_AnyShapeArrayImports,
|
357
|
+
)
|
358
|
+
|
359
|
+
def parameterized_dimensions(self, array: ArrayExpression) -> SlotResult:
|
360
|
+
"""
|
361
|
+
Constrained shapes using :func:`pydantic.conlist`
|
362
|
+
|
363
|
+
TODO:
|
364
|
+
- preservation of aliases
|
365
|
+
- (what other metadata is allowable on labeled dimensions?)
|
366
|
+
"""
|
367
|
+
# generate dimensions from inside out and then format
|
368
|
+
# e.g., if dimensions = [{min_card: 3}, {min_card: 2}],
|
369
|
+
# annotation = conlist(min_length=3, item_type=conlist(min_length=2, item_type=dtype))
|
370
|
+
range = self.dtype
|
371
|
+
for dimension in reversed(array.dimensions):
|
372
|
+
range = self._parameterized_dimension(dimension, range).annotation
|
373
|
+
|
374
|
+
return SlotResult(annotation=range, imports=_ConListImports)
|
375
|
+
|
376
|
+
def complex_dimensions(self, array: ArrayExpression) -> SlotResult:
|
377
|
+
"""
|
378
|
+
Mixture of parameterized dimensions with a max or min (or both) shape for anonymous dimensions.
|
379
|
+
|
380
|
+
A mixture of ``List`` , :class:`.conlist` , and :class:`.AnyShapeArray` .
|
381
|
+
"""
|
382
|
+
# first process any unlabeled dimensions which must be the innermost level of the annotation,
|
383
|
+
# then wrap that with labeled dimensions
|
384
|
+
if array.exact_number_dimensions or (
|
385
|
+
array.minimum_number_dimensions
|
386
|
+
and array.maximum_number_dimensions
|
387
|
+
and array.minimum_number_dimensions == array.maximum_number_dimensions
|
388
|
+
):
|
389
|
+
exact_dims = array.exact_number_dimensions or array.minimum_number_dimensions
|
390
|
+
if exact_dims > len(array.dimensions):
|
391
|
+
res = SlotResult(annotation=self._list_of_lists(exact_dims - len(array.dimensions), self.dtype))
|
392
|
+
elif exact_dims == len(array.dimensions):
|
393
|
+
# equivalent to labeled shape
|
394
|
+
return self.parameterized_dimensions(array)
|
395
|
+
else:
|
396
|
+
raise ValueError(
|
397
|
+
"if exact_number_dimensions is provided, it must be greater than the parameterized dimensions"
|
398
|
+
)
|
399
|
+
|
400
|
+
elif array.maximum_number_dimensions is not None and not array.maximum_number_dimensions:
|
401
|
+
# unlimited n dimensions, so innermost is AnyShape with dtype
|
402
|
+
res = self.any_shape(with_inner_union=True)
|
403
|
+
|
404
|
+
if array.minimum_number_dimensions and array.minimum_number_dimensions > len(array.dimensions):
|
405
|
+
# some minimum anonymous dimensions but unlimited max dimensions
|
406
|
+
# e.g., if min = 3, len(dim) = 2, then res.annotation = List[Union[AnyShapeArray[dtype], dtype]]
|
407
|
+
# res.annotation will be wrapped with the 2 labeled dimensions later
|
408
|
+
res.annotation = self._list_of_lists(
|
409
|
+
array.minimum_number_dimensions - len(array.dimensions), res.annotation
|
410
|
+
)
|
411
|
+
|
412
|
+
elif array.minimum_number_dimensions and array.maximum_number_dimensions is None:
|
413
|
+
raise ValueError(
|
414
|
+
(
|
415
|
+
"Cannot specify a minimum_number_dimensions while maximum is None while using labeled dimensions - "
|
416
|
+
"either use exact_number_dimensions > len(dimensions) for extra parameterized dimensions or set "
|
417
|
+
"maximum_number_dimensions explicitly to False for unbounded dimensions"
|
418
|
+
)
|
419
|
+
)
|
420
|
+
elif array.maximum_number_dimensions:
|
421
|
+
initial_min = array.minimum_number_dimensions if array.minimum_number_dimensions is not None else 0
|
422
|
+
dmin = max(len(array.dimensions), initial_min) - len(array.dimensions)
|
423
|
+
dmax = array.maximum_number_dimensions - len(array.dimensions)
|
424
|
+
|
425
|
+
res = self.bounded_dimensions(
|
426
|
+
ArrayExpression(minimum_number_dimensions=dmin, maximum_number_dimensions=dmax)
|
427
|
+
)
|
428
|
+
else:
|
429
|
+
raise ValueError("Unsupported array specification! this is almost certainly a bug!") # pragma: no cover
|
430
|
+
|
431
|
+
# Wrap inner dimension with labeled dimension
|
432
|
+
# e.g., if dimensions = [{min_card: 3}, {min_card: 2}]
|
433
|
+
# and res.annotation = List[Union[AnyShapeArray[dtype], dtype]]
|
434
|
+
# (min 3 dims, no max dims)
|
435
|
+
# then the final annotation = conlist(
|
436
|
+
# min_length=3,
|
437
|
+
# item_type=conlist(
|
438
|
+
# min_length=2,
|
439
|
+
# item_type=List[Union[AnyShapeArray[dtype], dtype]]
|
440
|
+
# )
|
441
|
+
# )
|
442
|
+
for dim in reversed(array.dimensions):
|
443
|
+
res = res.merge(self._parameterized_dimension(dim, dtype=res.annotation))
|
444
|
+
|
445
|
+
return res
|
446
|
+
|
447
|
+
|
448
|
+
class NPTypingArray(ArrayRangeGenerator):
|
449
|
+
"""
|
450
|
+
Represent array range with nptyping, and serialization/loading with an ArrayProxy
|
451
|
+
"""
|
452
|
+
|
453
|
+
REPR = ArrayRepresentation.NPARRAY
|
454
|
+
|
455
|
+
def __init__(self, **kwargs):
|
456
|
+
super(self).__init__(**kwargs)
|
457
|
+
raise NotImplementedError("NPTyping array ranges are not implemented yet :(")
|
@@ -0,0 +1,29 @@
|
|
1
|
+
"""
|
2
|
+
Utilities for formatting pydanticgen outputs
|
3
|
+
|
4
|
+
Kept separate in case we decide we don't want it/we want it to be moved to a place
|
5
|
+
that's better for other generators to use.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from typing import Optional
|
9
|
+
|
10
|
+
try:
|
11
|
+
from black import Mode, TargetVersion, format_str
|
12
|
+
except ImportError:
|
13
|
+
import warnings
|
14
|
+
|
15
|
+
warnings.warn("Black is an optional dependency, install it with the extra 'black' like `pip install linkml[black]`")
|
16
|
+
|
17
|
+
|
18
|
+
def _default_mode() -> "Mode":
|
19
|
+
return Mode(
|
20
|
+
target_versions={TargetVersion.PY311},
|
21
|
+
)
|
22
|
+
|
23
|
+
|
24
|
+
def format_black(code: str, mode: Optional["Mode"] = None) -> str:
|
25
|
+
if mode is None:
|
26
|
+
mode = _default_mode()
|
27
|
+
|
28
|
+
formatted = format_str(code, mode=mode)
|
29
|
+
return formatted
|
@@ -0,0 +1,79 @@
|
|
1
|
+
from typing import List, Optional, Type, TypeVar, Union
|
2
|
+
|
3
|
+
from pydantic import BaseModel
|
4
|
+
|
5
|
+
from linkml.generators.pydanticgen.template import Import, Imports
|
6
|
+
|
7
|
+
T = TypeVar("T", bound="BuildResult", covariant=True)
|
8
|
+
|
9
|
+
|
10
|
+
class BuildResult(BaseModel):
|
11
|
+
"""
|
12
|
+
The result of any build phase for any linkML object
|
13
|
+
|
14
|
+
BuildResults are merged in the serialization process, and are used
|
15
|
+
to keep track of not only the particular representation
|
16
|
+
of the thing in question, but any "side effects" that need to happen
|
17
|
+
elsewhere in the generation process (like adding imports, injecting classes, etc.)
|
18
|
+
"""
|
19
|
+
|
20
|
+
imports: Optional[Union[List[Import], Imports]] = None
|
21
|
+
injected_classes: Optional[List[Union[str, Type]]] = None
|
22
|
+
|
23
|
+
def merge(self, other: T) -> T:
|
24
|
+
"""
|
25
|
+
Merge a build result with another.
|
26
|
+
|
27
|
+
- Merges imports with :meth:`.Imports.__add__`
|
28
|
+
- Extends (with simple deduplication) injected classes
|
29
|
+
|
30
|
+
.. note::
|
31
|
+
|
32
|
+
This returns a (shallow) copy of ``self``, so subclasses don't need to make additional copies.
|
33
|
+
|
34
|
+
Args:
|
35
|
+
other (:class:`.BuildResult`): A subclass of BuildResult, generic over whatever we have passed.
|
36
|
+
|
37
|
+
Returns:
|
38
|
+
:class:`.BuildResult`
|
39
|
+
"""
|
40
|
+
self_copy = self.copy()
|
41
|
+
if other.imports:
|
42
|
+
if self.imports is not None:
|
43
|
+
self_copy.imports += other.imports
|
44
|
+
else:
|
45
|
+
self_copy.imports = other.imports
|
46
|
+
if other.injected_classes:
|
47
|
+
self_copy.injected_classes.extend(other.injected_classes)
|
48
|
+
self_copy.injected_classes = list(dict.fromkeys(self_copy.injected_classes))
|
49
|
+
return self_copy
|
50
|
+
|
51
|
+
|
52
|
+
class SlotResult(BuildResult):
|
53
|
+
annotation: str
|
54
|
+
"""The type annotation used in the generated model"""
|
55
|
+
field_extras: Optional[dict] = None
|
56
|
+
"""Additional metadata for this slot to be held in the Field object"""
|
57
|
+
|
58
|
+
def merge(self, other: "SlotResult") -> "SlotResult":
|
59
|
+
"""
|
60
|
+
Merge two SlotResults...
|
61
|
+
|
62
|
+
- calling :meth:`.BuildResult.merge`
|
63
|
+
- replacing the existing annotation with that given by ``other`` .
|
64
|
+
- updating any ``field_extras`` with the other
|
65
|
+
|
66
|
+
Args:
|
67
|
+
other (:class:`.SlotResult`): The other slot result to merge!
|
68
|
+
|
69
|
+
Returns:
|
70
|
+
:class:`.SlotResult`
|
71
|
+
"""
|
72
|
+
res = super(SlotResult, self).merge(other)
|
73
|
+
# Replace with other's annotation
|
74
|
+
res.annotation = other.annotation
|
75
|
+
if other.field_extras is not None:
|
76
|
+
if res.field_extras is None:
|
77
|
+
res.field_extras = {}
|
78
|
+
res.field_extras.update(other.field_extras)
|
79
|
+
return res
|
@@ -1,17 +1,24 @@
|
|
1
1
|
import inspect
|
2
2
|
import logging
|
3
3
|
import os
|
4
|
+
import textwrap
|
4
5
|
from collections import defaultdict
|
5
6
|
from copy import deepcopy
|
6
|
-
from dataclasses import dataclass
|
7
|
+
from dataclasses import dataclass, field
|
7
8
|
from pathlib import Path
|
8
9
|
from types import ModuleType
|
9
|
-
from typing import
|
10
|
+
from typing import (
|
11
|
+
Dict,
|
12
|
+
List,
|
13
|
+
Literal,
|
14
|
+
Optional,
|
15
|
+
Set,
|
16
|
+
Type,
|
17
|
+
Union,
|
18
|
+
)
|
10
19
|
|
11
20
|
import click
|
12
21
|
from jinja2 import ChoiceLoader, Environment, FileSystemLoader
|
13
|
-
|
14
|
-
# from linkml.generators import pydantic_GEN_VERSION
|
15
22
|
from linkml_runtime.linkml_model.meta import (
|
16
23
|
Annotation,
|
17
24
|
ClassDefinition,
|
@@ -30,6 +37,8 @@ from linkml.generators.common.type_designators import (
|
|
30
37
|
get_type_designator_value,
|
31
38
|
)
|
32
39
|
from linkml.generators.oocodegen import OOCodeGenerator
|
40
|
+
from linkml.generators.pydanticgen.array import ArrayRangeGenerator, ArrayRepresentation
|
41
|
+
from linkml.generators.pydanticgen.build import SlotResult
|
33
42
|
from linkml.generators.pydanticgen.template import (
|
34
43
|
ConditionalImport,
|
35
44
|
Import,
|
@@ -111,6 +120,11 @@ class PydanticGenerator(OOCodeGenerator):
|
|
111
120
|
file_extension = "py"
|
112
121
|
|
113
122
|
# ObjectVars
|
123
|
+
array_representations: List[ArrayRepresentation] = field(default_factory=lambda: [ArrayRepresentation.LIST])
|
124
|
+
black: bool = False
|
125
|
+
"""
|
126
|
+
If black is present in the environment, format the serialized code with it
|
127
|
+
"""
|
114
128
|
pydantic_version: int = int(PYDANTIC_VERSION[0])
|
115
129
|
template_dir: Optional[Union[str, Path]] = None
|
116
130
|
"""
|
@@ -491,7 +505,22 @@ class PydanticGenerator(OOCodeGenerator):
|
|
491
505
|
env.loader = loader
|
492
506
|
return env
|
493
507
|
|
494
|
-
def
|
508
|
+
def get_array_representations_range(self, slot: SlotDefinition, range: str) -> List[SlotResult]:
|
509
|
+
"""
|
510
|
+
Generate the python range for array representations
|
511
|
+
"""
|
512
|
+
array_reps = []
|
513
|
+
for repr in self.array_representations:
|
514
|
+
generator = ArrayRangeGenerator.get_generator(repr)
|
515
|
+
result = generator(slot.array, range, self.pydantic_version).make()
|
516
|
+
array_reps.append(result)
|
517
|
+
|
518
|
+
if len(array_reps) == 0:
|
519
|
+
raise ValueError("No array representation generated, but one was requested!")
|
520
|
+
|
521
|
+
return array_reps
|
522
|
+
|
523
|
+
def render(self) -> PydanticModule:
|
495
524
|
sv: SchemaView
|
496
525
|
sv = self.schemaview
|
497
526
|
schema = sv.schema
|
@@ -501,6 +530,9 @@ class PydanticGenerator(OOCodeGenerator):
|
|
501
530
|
description=schema.description.replace('"', '\\"') if schema.description else None,
|
502
531
|
)
|
503
532
|
enums = self.generate_enums(sv.all_enums())
|
533
|
+
injected_classes = []
|
534
|
+
if self.injected_classes is not None:
|
535
|
+
injected_classes += self.injected_classes
|
504
536
|
|
505
537
|
imports = DEFAULT_IMPORTS
|
506
538
|
if self.imports is not None:
|
@@ -559,10 +591,20 @@ class PydanticGenerator(OOCodeGenerator):
|
|
559
591
|
else:
|
560
592
|
raise Exception(f"Could not generate python range for {class_name}.{s.name}")
|
561
593
|
|
562
|
-
if
|
594
|
+
if s.array is not None:
|
563
595
|
# TODO add support for xarray
|
564
|
-
|
565
|
-
|
596
|
+
results = self.get_array_representations_range(s, pyrange)
|
597
|
+
# TODO: Move results unpacking to own function that is used after each slot build stage :)
|
598
|
+
for res in results:
|
599
|
+
if res.injected_classes:
|
600
|
+
injected_classes += res.injected_classes
|
601
|
+
if res.imports:
|
602
|
+
imports += res.imports
|
603
|
+
if len(results) == 1:
|
604
|
+
pyrange = results[0].annotation
|
605
|
+
else:
|
606
|
+
pyrange = f"Union[{', '.join([res.annotation for res in results])}]"
|
607
|
+
|
566
608
|
if "linkml:ColumnOrderedArray" in class_def.implements:
|
567
609
|
raise NotImplementedError("Cannot generate Pydantic code for ColumnOrderedArrays.")
|
568
610
|
elif s.multivalued:
|
@@ -586,10 +628,11 @@ class PydanticGenerator(OOCodeGenerator):
|
|
586
628
|
ann = Annotation("python_range", pyrange)
|
587
629
|
s.annotations[ann.tag] = ann
|
588
630
|
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
631
|
+
# TODO: Make cleaning injected classes its own method
|
632
|
+
injected_classes = list(
|
633
|
+
dict.fromkeys([c if isinstance(c, str) else inspect.getsource(c) for c in injected_classes])
|
634
|
+
)
|
635
|
+
injected_classes = [textwrap.dedent(c) for c in injected_classes]
|
593
636
|
|
594
637
|
base_model = PydanticBaseModel(
|
595
638
|
pydantic_ver=self.pydantic_version, extra_fields=self.extra_fields, fields=self.injected_fields
|
@@ -630,8 +673,11 @@ class PydanticGenerator(OOCodeGenerator):
|
|
630
673
|
enums=enums,
|
631
674
|
classes=classes,
|
632
675
|
)
|
633
|
-
|
634
|
-
|
676
|
+
return module
|
677
|
+
|
678
|
+
def serialize(self) -> str:
|
679
|
+
module = self.render()
|
680
|
+
return module.render(self._template_environment(), self.black)
|
635
681
|
|
636
682
|
def default_value_for_type(self, typ: str) -> str:
|
637
683
|
return "None"
|
@@ -669,6 +715,13 @@ Available templates to override:
|
|
669
715
|
default=1,
|
670
716
|
help="Pydantic version to use (1 or 2)",
|
671
717
|
)
|
718
|
+
@click.option(
|
719
|
+
"--array-representations",
|
720
|
+
type=click.Choice([k.value for k in ArrayRepresentation]),
|
721
|
+
multiple=True,
|
722
|
+
default=["list"],
|
723
|
+
help="List of array representations to accept for array slots. Default is list of lists.",
|
724
|
+
)
|
672
725
|
@click.option(
|
673
726
|
"--extra-fields",
|
674
727
|
type=click.Choice(["allow", "ignore", "forbid"], case_sensitive=False),
|
@@ -685,8 +738,9 @@ def cli(
|
|
685
738
|
genmeta=False,
|
686
739
|
classvars=True,
|
687
740
|
slots=True,
|
741
|
+
array_representations=list("list"),
|
688
742
|
pydantic_version=1,
|
689
|
-
extra_fields="forbid",
|
743
|
+
extra_fields: Literal["allow", "forbid", "ignore"] = "forbid",
|
690
744
|
**args,
|
691
745
|
):
|
692
746
|
"""Generate pydantic classes to represent a LinkML model"""
|
@@ -705,6 +759,7 @@ def cli(
|
|
705
759
|
gen = PydanticGenerator(
|
706
760
|
yamlfile,
|
707
761
|
pydantic_version=pydantic_version,
|
762
|
+
array_representations=[ArrayRepresentation(x) for x in array_representations],
|
708
763
|
extra_fields=extra_fields,
|
709
764
|
emit_metadata=head,
|
710
765
|
genmeta=genmeta,
|
@@ -1,14 +1,24 @@
|
|
1
|
-
from
|
1
|
+
from importlib.util import find_spec
|
2
|
+
from typing import Any, ClassVar, Dict, Generator, List, Literal, Optional, Union, overload
|
2
3
|
|
3
4
|
from jinja2 import Environment, PackageLoader
|
4
5
|
from pydantic import BaseModel, Field
|
5
6
|
from pydantic.version import VERSION as PYDANTIC_VERSION
|
6
7
|
|
8
|
+
try:
|
9
|
+
if find_spec("black") is not None:
|
10
|
+
from linkml.generators.pydanticgen.black import format_black
|
11
|
+
else:
|
12
|
+
# no warning, having black is optional, we only warn when someone tries to import it explicitly
|
13
|
+
format_black = None
|
14
|
+
except ImportError:
|
15
|
+
# we can also get an import error from find_spec during testing because that's how we mock not having it installed
|
16
|
+
format_black = None
|
17
|
+
|
7
18
|
if int(PYDANTIC_VERSION[0]) >= 2:
|
8
19
|
from pydantic import computed_field
|
9
20
|
else:
|
10
|
-
|
11
|
-
from pydantic.fields import ModelField
|
21
|
+
from pydantic.fields import ModelField
|
12
22
|
|
13
23
|
|
14
24
|
class TemplateModel(BaseModel):
|
@@ -22,12 +32,31 @@ class TemplateModel(BaseModel):
|
|
22
32
|
to already be rendered to strings - ie. rather than the ``class.py.jinja``
|
23
33
|
template receiving a full :class:`.PydanticAttribute` object or dictionary,
|
24
34
|
it receives it having already been rendered to a string. See the :meth:`.render` method.
|
35
|
+
|
36
|
+
.. admonition:: Black Formatting
|
37
|
+
|
38
|
+
Template models will try to use ``black`` to format results when it is available in the
|
39
|
+
environment when render is called with ``black = True`` . If it isn't, then the string is
|
40
|
+
returned without any formatting beyond the template.
|
41
|
+
This is mostly important for complex annotations like those produced for arrays,
|
42
|
+
as otherwise the templates are acceptable looking.
|
43
|
+
|
44
|
+
To install linkml with black, use the extra ``black`` dependency.
|
45
|
+
|
46
|
+
e.g. with pip::
|
47
|
+
|
48
|
+
pip install linkml[black]
|
49
|
+
|
50
|
+
or with poetry::
|
51
|
+
|
52
|
+
poetry install -E black
|
53
|
+
|
25
54
|
"""
|
26
55
|
|
27
56
|
template: ClassVar[str]
|
28
57
|
pydantic_ver: int = int(PYDANTIC_VERSION[0])
|
29
58
|
|
30
|
-
def render(self, environment: Optional[Environment] = None) -> str:
|
59
|
+
def render(self, environment: Optional[Environment] = None, black: bool = False) -> str:
|
31
60
|
"""
|
32
61
|
Recursively render a template model to a string.
|
33
62
|
|
@@ -35,6 +64,10 @@ class TemplateModel(BaseModel):
|
|
35
64
|
using the template set in :attr:`.TemplateModel.template` , but preserving the structure
|
36
65
|
of lists and dictionaries. Regular :class:`.BaseModel` s are rendered to dictionaries.
|
37
66
|
Any other value is passed through unchanged.
|
67
|
+
|
68
|
+
Args:
|
69
|
+
environment (:class:`jinja2.Environment`): Template environment - see :meth:`.environment`
|
70
|
+
black (bool): if ``True`` , format template with black. (default False)
|
38
71
|
"""
|
39
72
|
if environment is None:
|
40
73
|
environment = TemplateModel.environment()
|
@@ -46,7 +79,17 @@ class TemplateModel(BaseModel):
|
|
46
79
|
|
47
80
|
data = {k: _render(getattr(self, k, None), environment) for k in fields}
|
48
81
|
template = environment.get_template(self.template)
|
49
|
-
|
82
|
+
rendered = template.render(**data)
|
83
|
+
if format_black is not None and black:
|
84
|
+
try:
|
85
|
+
return format_black(rendered)
|
86
|
+
except Exception:
|
87
|
+
# TODO: it would nice to have a standard logging module here ;)
|
88
|
+
return rendered
|
89
|
+
elif black and format_black is None:
|
90
|
+
raise ValueError("black formatting was requested, but black is not installed in this environment")
|
91
|
+
else:
|
92
|
+
return rendered
|
50
93
|
|
51
94
|
@classmethod
|
52
95
|
def environment(cls) -> Environment:
|
@@ -148,6 +191,17 @@ class PydanticBaseModel(TemplateModel):
|
|
148
191
|
Extra fields that are typically injected into the base model via
|
149
192
|
:attr:`~linkml.generators.pydanticgen.PydanticGenerator.injected_fields`
|
150
193
|
"""
|
194
|
+
strict: bool = False
|
195
|
+
"""
|
196
|
+
Enable strict mode in the base model.
|
197
|
+
|
198
|
+
.. note::
|
199
|
+
|
200
|
+
Pydantic 2 only! Pydantic 1 only has strict types, not strict mode. See: https://github.com/linkml/linkml/issues/1955
|
201
|
+
|
202
|
+
References:
|
203
|
+
https://docs.pydantic.dev/latest/concepts/strict_mode
|
204
|
+
"""
|
151
205
|
|
152
206
|
|
153
207
|
class PydanticAttribute(TemplateModel):
|
@@ -252,11 +306,11 @@ class PydanticClass(TemplateModel):
|
|
252
306
|
super(PydanticClass, self).__init__(**kwargs)
|
253
307
|
self.validators = self._validators()
|
254
308
|
|
255
|
-
def render(self, environment: Optional[Environment] = None) -> str:
|
309
|
+
def render(self, environment: Optional[Environment] = None, black: bool = False) -> str:
|
256
310
|
"""Overridden in pydantic 1 to ensure that validators are regenerated at rendering time"""
|
257
311
|
# refresh in case attributes have changed since init
|
258
312
|
self.validators = self._validators()
|
259
|
-
return super(PydanticClass, self).render(environment)
|
313
|
+
return super(PydanticClass, self).render(environment, black)
|
260
314
|
|
261
315
|
|
262
316
|
class ObjectImport(BaseModel):
|
@@ -320,6 +374,10 @@ class Import(TemplateModel):
|
|
320
374
|
return [self, other]
|
321
375
|
|
322
376
|
# handle conditionals
|
377
|
+
if isinstance(self, ConditionalImport) and isinstance(other, ConditionalImport):
|
378
|
+
# If our condition is the same, return the newer version
|
379
|
+
if self.condition == other.condition:
|
380
|
+
return [other]
|
323
381
|
if isinstance(self, ConditionalImport) or isinstance(other, ConditionalImport):
|
324
382
|
# we don't have a good way of combining conditionals, just return both
|
325
383
|
return [self, other]
|
@@ -430,9 +488,20 @@ class Imports(TemplateModel):
|
|
430
488
|
|
431
489
|
imports: List[Union[Import, ConditionalImport]] = Field(default_factory=list)
|
432
490
|
|
433
|
-
def __add__(self, other: Import) -> "Imports":
|
491
|
+
def __add__(self, other: Union[Import, "Imports", List[Import]]) -> "Imports":
|
492
|
+
if isinstance(other, Imports) or (isinstance(other, list) and all([isinstance(i, Import) for i in other])):
|
493
|
+
if hasattr(self, "model_copy"):
|
494
|
+
self_copy = self.model_copy(deep=True)
|
495
|
+
else:
|
496
|
+
self_copy = self.copy()
|
497
|
+
|
498
|
+
for i in other:
|
499
|
+
self_copy += i
|
500
|
+
return self_copy
|
501
|
+
|
434
502
|
# check if we have one of these already
|
435
503
|
imports = self.imports.copy()
|
504
|
+
|
436
505
|
existing = [i for i in imports if i.module == other.module]
|
437
506
|
|
438
507
|
# if we have nothing importing from this module yet, add it!
|
@@ -454,6 +523,10 @@ class Imports(TemplateModel):
|
|
454
523
|
merged = e.merge(other)
|
455
524
|
imports.extend(merged)
|
456
525
|
break
|
526
|
+
|
527
|
+
# SPECIAL CASE - __future__ annotations must happen at the top of a file
|
528
|
+
imports = sorted(imports, key=lambda i: i.module == "__future__", reverse=True)
|
529
|
+
|
457
530
|
return Imports(imports=imports)
|
458
531
|
|
459
532
|
def __len__(self) -> int:
|
@@ -495,10 +568,10 @@ class PydanticModule(TemplateModel):
|
|
495
568
|
super(PydanticModule, self).__init__(**kwargs)
|
496
569
|
self.class_names = [c.name for c in self.classes.values()]
|
497
570
|
|
498
|
-
def render(self, environment: Optional[Environment] = None) -> str:
|
571
|
+
def render(self, environment: Optional[Environment] = None, black: bool = False) -> str:
|
499
572
|
"""
|
500
573
|
Trivial override of parent method for pydantic 1 to ensure that
|
501
574
|
:attr:`.class_names` are correct at render time
|
502
575
|
"""
|
503
576
|
self.class_names = [c.name for c in self.classes.values()]
|
504
|
-
return super(PydanticModule, self).render(environment)
|
577
|
+
return super(PydanticModule, self).render(environment, black)
|
@@ -16,7 +16,9 @@ class {{ name }}(BaseModel):
|
|
16
16
|
validate_default = True,
|
17
17
|
extra = "{{ extra_fields }}",
|
18
18
|
arbitrary_types_allowed = True,
|
19
|
-
use_enum_values = True
|
19
|
+
use_enum_values = True,
|
20
|
+
strict = {{ strict }},
|
21
|
+
)
|
20
22
|
{% endif %}
|
21
23
|
{% if fields is not none %}
|
22
24
|
{% for field in fields %}
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import re
|
2
|
-
from typing import Any, Callable
|
2
|
+
from typing import Any, Callable, Optional
|
3
3
|
|
4
4
|
from linkml_runtime import SchemaView
|
5
5
|
from linkml_runtime.linkml_model import SlotDefinition
|
@@ -10,21 +10,34 @@ from linkml.generators.shacl.shacl_data_type import ShaclDataType
|
|
10
10
|
|
11
11
|
|
12
12
|
class IfAbsentProcessor:
|
13
|
+
"""
|
14
|
+
Processes value of ifabsent slot.
|
15
|
+
|
16
|
+
See `<https://w3id.org/linkml/ifabsent>`_.
|
17
|
+
|
18
|
+
TODO: unify this with ifabsent_functions
|
19
|
+
"""
|
13
20
|
|
14
21
|
ifabsent_regex = re.compile("""(?:(?P<type>\w+)\()?[\"\']?(?P<default_value>[^\(\)\"\')]*)[\"\']?\)?""")
|
15
22
|
|
16
23
|
def __init__(self, schema_view: SchemaView):
|
17
24
|
self.schema_view = schema_view
|
18
25
|
|
19
|
-
def process_slot(
|
26
|
+
def process_slot(
|
27
|
+
self, add_prop: Callable[[URIRef, Identifier], None], slot: SlotDefinition, class_uri: Optional[URIRef] = None
|
28
|
+
) -> None:
|
20
29
|
if slot.ifabsent:
|
21
30
|
ifabsent_match = self.ifabsent_regex.search(slot.ifabsent)
|
22
31
|
ifabsent_default_value = ifabsent_match.group("default_value")
|
23
32
|
|
24
|
-
self._map_to_default_value(slot, add_prop, ifabsent_default_value)
|
33
|
+
self._map_to_default_value(slot, add_prop, ifabsent_default_value, class_uri)
|
25
34
|
|
26
35
|
def _map_to_default_value(
|
27
|
-
self,
|
36
|
+
self,
|
37
|
+
slot: SlotDefinition,
|
38
|
+
add_prop: Callable[[URIRef, Identifier], None],
|
39
|
+
ifabsent_default_value: Any,
|
40
|
+
class_uri: Optional[URIRef] = None,
|
28
41
|
) -> None:
|
29
42
|
for datatype in list(ShaclDataType):
|
30
43
|
if datatype.linkml_type == slot.range:
|
@@ -38,4 +51,9 @@ class IfAbsentProcessor:
|
|
38
51
|
add_prop(SH.defaultValue, Literal(ifabsent_default_value))
|
39
52
|
return
|
40
53
|
|
54
|
+
if ifabsent_default_value == "class_curie":
|
55
|
+
if class_uri:
|
56
|
+
add_prop(SH.defaultValue, class_uri)
|
57
|
+
return
|
58
|
+
|
41
59
|
raise ValueError(f"The ifabsent value `{slot.ifabsent}` of the `{slot.name}` slot could not be processed")
|
linkml/generators/shaclgen.py
CHANGED
@@ -162,7 +162,7 @@ class ShaclGenerator(Generator):
|
|
162
162
|
if sv.get_identifier_slot(r) is not None:
|
163
163
|
prop_pv(SH.nodeKind, SH.IRI)
|
164
164
|
else:
|
165
|
-
prop_pv(SH.nodeKind, SH.
|
165
|
+
prop_pv(SH.nodeKind, SH.BlankNodeOrIRI)
|
166
166
|
elif r in sv.all_types().values():
|
167
167
|
self._add_type(prop_pv, r)
|
168
168
|
elif r in sv.all_enums():
|
@@ -172,7 +172,7 @@ class ShaclGenerator(Generator):
|
|
172
172
|
if s.pattern:
|
173
173
|
prop_pv(SH.pattern, Literal(s.pattern))
|
174
174
|
|
175
|
-
ifabsent_processor.process_slot(prop_pv, s)
|
175
|
+
ifabsent_processor.process_slot(prop_pv, s, class_uri)
|
176
176
|
|
177
177
|
return g
|
178
178
|
|
linkml/utils/generator.py
CHANGED
@@ -684,7 +684,7 @@ class Generator(metaclass=abc.ABCMeta):
|
|
684
684
|
|
685
685
|
def slot_name(self, name: str) -> str:
|
686
686
|
"""
|
687
|
-
Return the underscored version of the aliased slot name if name is a slot. Prepend
|
687
|
+
Return the underscored version of the aliased slot name if name is a slot. Prepend ``unknown_`` if the name
|
688
688
|
isn't valid.
|
689
689
|
"""
|
690
690
|
slot = self.slot_for(name)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: linkml
|
3
|
-
Version: 1.7.
|
3
|
+
Version: 1.7.7
|
4
4
|
Summary: Linked Open Data Modeling Language
|
5
5
|
Home-page: https://linkml.io/linkml/
|
6
6
|
Keywords: schema,linked data,data modeling,rdf,owl,biolink
|
@@ -19,12 +19,15 @@ Classifier: Programming Language :: Python :: 3.10
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.11
|
20
20
|
Classifier: Programming Language :: Python :: 3.10
|
21
21
|
Classifier: Programming Language :: Python :: 3.11
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
22
23
|
Classifier: Programming Language :: Python :: 3.8
|
23
24
|
Classifier: Programming Language :: Python :: 3.9
|
24
25
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
26
|
+
Provides-Extra: black
|
25
27
|
Provides-Extra: shacl
|
26
28
|
Provides-Extra: tests
|
27
29
|
Requires-Dist: antlr4-python3-runtime (>=4.9.0,<4.10)
|
30
|
+
Requires-Dist: black (>=24.0.0) ; extra == "black" or extra == "tests"
|
28
31
|
Requires-Dist: click (>=7.0)
|
29
32
|
Requires-Dist: graphviz (>=0.10.1)
|
30
33
|
Requires-Dist: hbreader
|
@@ -33,11 +36,11 @@ Requires-Dist: jinja2 (>=3.1.0)
|
|
33
36
|
Requires-Dist: jsonasobj2 (>=1.0.3,<2.0.0)
|
34
37
|
Requires-Dist: jsonschema[format] (>=4.0.0)
|
35
38
|
Requires-Dist: linkml-dataops
|
36
|
-
Requires-Dist: linkml-runtime (>=1.7.
|
39
|
+
Requires-Dist: linkml-runtime (>=1.7.4)
|
37
40
|
Requires-Dist: openpyxl
|
38
41
|
Requires-Dist: parse
|
39
42
|
Requires-Dist: prefixcommons (>=0.1.7)
|
40
|
-
Requires-Dist: prefixmaps (>=0.
|
43
|
+
Requires-Dist: prefixmaps (>=0.2.2)
|
41
44
|
Requires-Dist: pydantic (>=1.0.0,<3.0.0)
|
42
45
|
Requires-Dist: pyjsg (>=0.11.6)
|
43
46
|
Requires-Dist: pyshacl (>=0.25.0,<0.26.0) ; extra == "shacl" or extra == "tests"
|
@@ -48,7 +51,6 @@ Requires-Dist: pyyaml
|
|
48
51
|
Requires-Dist: rdflib (>=6.0.0)
|
49
52
|
Requires-Dist: requests (>=2.22)
|
50
53
|
Requires-Dist: sqlalchemy (>=1.4.31)
|
51
|
-
Requires-Dist: typing-extensions (>=4.5.0,<5.0.0) ; python_version == "3.7"
|
52
54
|
Requires-Dist: watchdog (>=0.9.0)
|
53
55
|
Project-URL: Documentation, https://linkml.io/linkml/
|
54
56
|
Project-URL: Repository, https://github.com/linkml/linkml
|
@@ -13,7 +13,7 @@ linkml/generators/docgen/enum.md.jinja2,sha256=mXnUrRkleY2bOTEyAZ5c4pcUnqhs6BNa8
|
|
13
13
|
linkml/generators/docgen/index.md.jinja2,sha256=wXUYTmayPLFltC0vbGE_Mf6m3GkkWav7FOEjCvEpHp4,1466
|
14
14
|
linkml/generators/docgen/index.tex.jinja2,sha256=Go_EA-_N4JUpbOYbk3OY11mz5yV70VF2l2sMtgIPWw4,501
|
15
15
|
linkml/generators/docgen/schema.md.jinja2,sha256=xlENfnzNRYgPT_0tdqNFxgklVM4Qf5BuzhFVvSMDuxs,70
|
16
|
-
linkml/generators/docgen/slot.md.jinja2,sha256=
|
16
|
+
linkml/generators/docgen/slot.md.jinja2,sha256=ZGa-kaNvi5LknvFRIjKQyRpSynr1SlkLrpEmWDNPwDA,3226
|
17
17
|
linkml/generators/docgen/subset.md.jinja2,sha256=fTNIpAkml5RKFbbtLore3IAzFN1cISVsyL1ru2-Z4oA,2665
|
18
18
|
linkml/generators/docgen/type.md.jinja2,sha256=QmCMJZrFwP33eHkggBVtypbyrxTb-XZn9vHOYojVaYk,635
|
19
19
|
linkml/generators/docgen.py,sha256=dz7OWxclX306EweVIgC765h3NKtJTRg0TPnqmltWygs,34347
|
@@ -26,7 +26,7 @@ linkml/generators/graphqlgen.py,sha256=6qZpI0rwg3ypsv_KrLVzXgdsJfR8LNPqgMwaRwzwn
|
|
26
26
|
linkml/generators/javagen/example_template.java.jinja2,sha256=ec4CVTv_0zS7V5Y-1E6H4lRraya10gfX7BEMBlu38X4,444
|
27
27
|
linkml/generators/javagen/java_record_template.jinja2,sha256=OQZffLSy_xR3FIhQMltvrYyVeut7l2Q-tzK7AOiVmWs,1729
|
28
28
|
linkml/generators/javagen.py,sha256=KxwupMztyCRHcSsbtTnOovuj1WamsAny0mxbYWvTiDs,5324
|
29
|
-
linkml/generators/jsonldcontextgen.py,sha256=
|
29
|
+
linkml/generators/jsonldcontextgen.py,sha256=N_XWCx_1eS5KcWlzlyl79C-dW0gfe32v886UskD1iTU,8644
|
30
30
|
linkml/generators/jsonldgen.py,sha256=pk8Gmh2gSvXt_o7MTuJ71thNfIJuHUGkNl3yl3RIdsE,7728
|
31
31
|
linkml/generators/jsonschemagen.py,sha256=XOsYIrpA6BwtAcfh8GNGduIjpuPAsF2f_PQ1mghj_WU,27455
|
32
32
|
linkml/generators/legacy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -34,16 +34,19 @@ linkml/generators/linkmlgen.py,sha256=1_Kt_7rD42WvCTjq0yaq1Of7jEDZR_uusyzAT-qWMH
|
|
34
34
|
linkml/generators/markdowngen.py,sha256=dDMyusNXLvM2raIW-_vd2AQBhDHn0P_rMeDq3QFuacs,32926
|
35
35
|
linkml/generators/namespacegen.py,sha256=vVcIyM0zlKd7XRvtdzwTwHjG4Pg49801gy4FUmjJlqQ,6450
|
36
36
|
linkml/generators/oocodegen.py,sha256=r73QI08ajbTZTobc9OIG6BMWZft3zdu76vKVR33pyYg,7774
|
37
|
-
linkml/generators/owlgen.py,sha256=
|
37
|
+
linkml/generators/owlgen.py,sha256=NqEvuJOUh4ZEe5tVeVol3SHy7zpCagEhso7Q1GLyIqY,54458
|
38
38
|
linkml/generators/plantumlgen.py,sha256=tk-_XJtBA5EqYJSUTc3bdMdCwqlC-rc9VYO9A2V_HpM,14895
|
39
39
|
linkml/generators/prefixmapgen.py,sha256=L9TccwKNHEguW0ox5qgf_GhIuqauYTI8d4jSjeqdkWo,4720
|
40
40
|
linkml/generators/projectgen.py,sha256=OuT_AneoZFNMCn50GllfZafadUHt50u61JcftM2eed4,9750
|
41
41
|
linkml/generators/protogen.py,sha256=5UxThsFDVyDTzzTDh77Z0anx4-tLgz8kQQ-e7ywIqwI,2290
|
42
42
|
linkml/generators/pydanticgen/__init__.py,sha256=uKGaaQSaeKqinHImXGFE448i-t8Oi0No8suIO76sep8,629
|
43
|
-
linkml/generators/pydanticgen/
|
44
|
-
linkml/generators/pydanticgen/
|
43
|
+
linkml/generators/pydanticgen/array.py,sha256=imbHtwgQcz_uloFWDBDojIMydZw9eysQyQl6ppT5eL8,19045
|
44
|
+
linkml/generators/pydanticgen/black.py,sha256=c-Hgaao9hd4nPMU9w0Hmg2j4Wc81IcY0zAfppQPr1cM,721
|
45
|
+
linkml/generators/pydanticgen/build.py,sha256=Ia8qy4C16b-KqO_fw4tGQW_Eo4grCVyX7RsuJ3uRTtk,2681
|
46
|
+
linkml/generators/pydanticgen/pydanticgen.py,sha256=psEdNy_oJqHnTBZkQp4H8pNrFXv_y92j5oEoFcp_AEU,29809
|
47
|
+
linkml/generators/pydanticgen/template.py,sha256=7I-qch_vr1elXwJ1kxRcZEl3j6-WTLYRh8_b2DjxjyE,19911
|
45
48
|
linkml/generators/pydanticgen/templates/attribute.py.jinja,sha256=AlH_QFJJkONpzXQRGqnW4ufmjp9s9E7Q9W5r8ykNGeQ,443
|
46
|
-
linkml/generators/pydanticgen/templates/base_model.py.jinja,sha256=
|
49
|
+
linkml/generators/pydanticgen/templates/base_model.py.jinja,sha256=48y64MnC9rjNSV-nKLMeDuHN4gm15UsInhnKxh65zoM,834
|
47
50
|
linkml/generators/pydanticgen/templates/class.py.jinja,sha256=RIdkqdZS9rDILUuVqDIAWK_vATGkirLbPhdHSyHDAbY,565
|
48
51
|
linkml/generators/pydanticgen/templates/conditional_import.py.jinja,sha256=YheknDrxvepiJUzeItSL5aSbAkCdR1k0a6m6aTA4qNM,240
|
49
52
|
linkml/generators/pydanticgen/templates/enum.py.jinja,sha256=572XFQyEMZfL-G_Cj68T-NI_mUnDoFOAVJOGIKu2Hb8,338
|
@@ -54,9 +57,9 @@ linkml/generators/pydanticgen/templates/validator.py.jinja,sha256=Yo4dubQal-HwEo
|
|
54
57
|
linkml/generators/pythongen.py,sha256=AvOQZJ-OT8lMYeaT2bdexHuRPRfTK7wpbmgAdfc2lW4,52805
|
55
58
|
linkml/generators/rdfgen.py,sha256=L6F08iDUqVemXXrRbJmcOxvJTt14hR0oo8WLoqf4npw,2656
|
56
59
|
linkml/generators/shacl/__init__.py,sha256=O-M-wndKw8rMW-U8X3QCNHal-ErXP6uXZqxiQSa77l4,108
|
57
|
-
linkml/generators/shacl/ifabsent_processor.py,sha256=
|
60
|
+
linkml/generators/shacl/ifabsent_processor.py,sha256=kV9BGA2ZPXLRfaFuW0o4jpkATvGggvrqpAo9c1UqWNE,2193
|
58
61
|
linkml/generators/shacl/shacl_data_type.py,sha256=BT3C9tdFyBQnuucPN7YQiFAKEa9yuzy-Q26X6dmOXgo,1827
|
59
|
-
linkml/generators/shaclgen.py,sha256=
|
62
|
+
linkml/generators/shaclgen.py,sha256=vCNAX15wg0h86ZplRjo721_58UHUSVHkQULTYV_1HZI,8324
|
60
63
|
linkml/generators/shexgen.py,sha256=KzhaL-A4R4peSdhY6nlDWmS-DPfnZMxMzrXhHGnA_Ag,9874
|
61
64
|
linkml/generators/sparqlgen.py,sha256=c7x8GFChKWprBx4rToSnu9qN8OleWTCenVUdZ8XSTWM,6138
|
62
65
|
linkml/generators/sqlalchemy/__init__.py,sha256=mb9AC1rIFkSiNZhhG0TAk45ol9PjS1XvsrvCjgfVUpQ,249
|
@@ -100,7 +103,7 @@ linkml/utils/converter.py,sha256=snAF-pgZBSi4ANiaKXxqtZk5w3qonJjTfI4X5OVLmUE,628
|
|
100
103
|
linkml/utils/datautils.py,sha256=QlbzwXykh5Fphfe5KxPo6_ekXfniLbHiEGJtLWjUrvY,3742
|
101
104
|
linkml/utils/datavalidator.py,sha256=kBdWaVi8IZT1bOwEJgJYx-wZAb_PTBObB9nHpYORfKA,472
|
102
105
|
linkml/utils/execute_tutorial.py,sha256=T4kHTSyz3ItJGEUZxVjR-3yLVKnOr5Ix4NMGE47-IuE,6912
|
103
|
-
linkml/utils/generator.py,sha256=
|
106
|
+
linkml/utils/generator.py,sha256=3q_PpZQxBTwpoN8lHHMkUhHm1gg0qXhiHgLWV9FIVLM,38363
|
104
107
|
linkml/utils/helpers.py,sha256=yR8n4zFA5wPcYC7xzRuNF3wO16vG80v6j7DM3qTNmIc,447
|
105
108
|
linkml/utils/ifabsent_functions.py,sha256=IkcBcmRYu8sllx7_mTCqu4aHfTxX2AnQoccZ1KOODds,5843
|
106
109
|
linkml/utils/logictools.py,sha256=GSmBiobC49TcQjE08RtXEE3JwJEOV7eEREio25uJiFs,21184
|
@@ -138,8 +141,8 @@ linkml/workspaces/datamodel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMp
|
|
138
141
|
linkml/workspaces/datamodel/workspaces.py,sha256=4HdkqweGNfMPqnB1_Onc9DcTfkhoagTRcqruh08nRoI,14905
|
139
142
|
linkml/workspaces/datamodel/workspaces.yaml,sha256=EjVrwPpeRZqJRjuGyyDRxxFzuv55SiLIXPBRUG6HStU,4233
|
140
143
|
linkml/workspaces/example_runner.py,sha256=OmC_yZLIb4KXGQrstBVZL0UAQ9ZAaraguQF0RSf-snk,11611
|
141
|
-
linkml-1.7.
|
142
|
-
linkml-1.7.
|
143
|
-
linkml-1.7.
|
144
|
-
linkml-1.7.
|
145
|
-
linkml-1.7.
|
144
|
+
linkml-1.7.7.dist-info/entry_points.txt,sha256=7haDkIbyC7ZLhm5z-e3BhrLJpY2xoW1yuD8Y7QPNtVg,2093
|
145
|
+
linkml-1.7.7.dist-info/METADATA,sha256=f74yR9Byjm8tw_y28edi2dJtVwthbMwMUocjhXiy0TQ,3656
|
146
|
+
linkml-1.7.7.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88
|
147
|
+
linkml-1.7.7.dist-info/LICENSE,sha256=kORMoywK6j9_iy0UvLR-a80P1Rvc9AOM4gsKlUNZABg,535
|
148
|
+
linkml-1.7.7.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|