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.
@@ -46,14 +46,15 @@ URI: {{ gen.uri_link(element) }}
46
46
  <!-- no inheritance hierarchy -->
47
47
  {% endif %}
48
48
 
49
- {% if schemaview.get_classes_by_slot(element, include_induced=True) %}
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 schemaview.get_classes_by_slot(element, include_induced=True) -%}
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
- cls_uri_prefix, cls_uri_suffix = self.namespaces.prefix_suffix(cls.class_uri)
137
- if not self.default_ns or not cls_uri_prefix or cls_uri_prefix != self.default_ns:
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 False
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
- slot_prefix = self.namespaces.prefix_for(slot.slot_uri)
173
- if not self.default_ns or not slot_prefix or slot_prefix != self.default_ns:
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
 
@@ -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 Dict, List, Literal, Optional, Set, Type, Union
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 serialize(self) -> str:
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 "linkml:elements" in s.implements:
594
+ if s.array is not None:
563
595
  # TODO add support for xarray
564
- pyrange = "np.ndarray"
565
- imports += Import(module="numpy", alias="np")
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
- if self.injected_classes is not None:
590
- injected_classes = [c if isinstance(c, str) else inspect.getsource(c) for c in self.injected_classes]
591
- else:
592
- injected_classes = None
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
- code = module.render(self._template_environment())
634
- return code
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 typing import TYPE_CHECKING, Any, ClassVar, Dict, Generator, List, Literal, Optional, Union, overload
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
- if TYPE_CHECKING: # pragma: no cover
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
- return template.render(**data)
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(self, add_prop: Callable[[URIRef, Identifier], None], slot: SlotDefinition):
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, slot: SlotDefinition, add_prop: Callable[[URIRef, Identifier], None], ifabsent_default_value: Any
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")
@@ -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.BlankNode)
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 "unknown\_" if the name
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.6
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.0)
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.1.3)
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=HcLu32FiyIRy0AaV-GwBPUwEbTCY-KkPDhOZk3OKfqg,3226
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=048kzs28xooik86A8OQ0NIbXxyJZGkqH1ZMxg17mcf4,8073
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=FMzdiK85CND8wnLiXPmylpayUTGpuydgD_g4hFjyez4,54374
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/pydanticgen.py,sha256=ZUi9fE5ZiQbW5V4Ckth8hysMghHh9H2RpqLZa3m8PfQ,27598
44
- linkml/generators/pydanticgen/template.py,sha256=e2PYQFRlDR1Nva_xy-NlsttpW79L8n6e63QKHZzoXH8,17016
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=eYKNI-4itB6lYW5yhfIWngtMcHZnuEaic9Dv56XKX4M,797
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=I7zvSjZzB7tZjcXRE0UE3DUmF5sY7QrKbXblYvpRwkc,1746
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=mTkHziEIKoMTr-Vl6nwOdqxk-97cJzTsFG7ViMiFhMs,8308
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=8-5JpMmvkWyLMeK5qSRv3XuOpfAmi2quNQ4Mo6nNCA0,38362
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.6.dist-info/entry_points.txt,sha256=7haDkIbyC7ZLhm5z-e3BhrLJpY2xoW1yuD8Y7QPNtVg,2093
142
- linkml-1.7.6.dist-info/METADATA,sha256=PaI8ylnY-nUYjDrS-44yWh-r6UsiLNLQ_cYYDBNZHX8,3588
143
- linkml-1.7.6.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88
144
- linkml-1.7.6.dist-info/LICENSE,sha256=kORMoywK6j9_iy0UvLR-a80P1Rvc9AOM4gsKlUNZABg,535
145
- linkml-1.7.6.dist-info/RECORD,,
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