linkml 1.7.4__py3-none-any.whl → 1.7.6__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/__init__.py +2 -0
- linkml/generators/docgen/class.md.jinja2 +15 -2
- linkml/generators/docgen/slot.md.jinja2 +14 -1
- linkml/generators/docgen.py +32 -11
- linkml/generators/excelgen.py +5 -3
- linkml/generators/golanggen.py +6 -3
- linkml/generators/jsonldcontextgen.py +9 -6
- linkml/generators/jsonldgen.py +3 -1
- linkml/generators/oocodegen.py +6 -3
- linkml/generators/owlgen.py +30 -3
- linkml/generators/prefixmapgen.py +5 -4
- linkml/generators/projectgen.py +18 -5
- linkml/generators/pydanticgen/__init__.py +29 -0
- linkml/generators/{pydanticgen.py → pydanticgen/pydanticgen.py} +196 -301
- linkml/generators/pydanticgen/template.py +504 -0
- linkml/generators/pydanticgen/templates/attribute.py.jinja +10 -0
- linkml/generators/pydanticgen/templates/base_model.py.jinja +27 -0
- linkml/generators/pydanticgen/templates/class.py.jinja +21 -0
- linkml/generators/pydanticgen/templates/conditional_import.py.jinja +9 -0
- linkml/generators/pydanticgen/templates/enum.py.jinja +16 -0
- linkml/generators/pydanticgen/templates/footer.py.jinja +13 -0
- linkml/generators/pydanticgen/templates/imports.py.jinja +31 -0
- linkml/generators/pydanticgen/templates/module.py.jinja +27 -0
- linkml/generators/pydanticgen/templates/validator.py.jinja +15 -0
- linkml/generators/pythongen.py +19 -10
- linkml/generators/shacl/__init__.py +3 -0
- linkml/generators/shacl/ifabsent_processor.py +41 -0
- linkml/generators/shacl/shacl_data_type.py +40 -0
- linkml/generators/shaclgen.py +105 -82
- linkml/generators/shexgen.py +1 -1
- linkml/generators/sparqlgen.py +5 -4
- linkml/generators/sqlalchemygen.py +15 -14
- linkml/generators/sqltablegen.py +37 -25
- linkml/generators/terminusdbgen.py +7 -1
- linkml/generators/typescriptgen.py +2 -1
- linkml/linter/config/datamodel/config.py +8 -0
- linkml/linter/linter.py +2 -1
- linkml/linter/rules.py +11 -2
- linkml/transformers/logical_model_transformer.py +2 -1
- linkml/transformers/relmodel_transformer.py +4 -2
- linkml/transformers/schema_renamer.py +1 -1
- linkml/utils/generator.py +11 -8
- linkml/utils/ifabsent_functions.py +7 -9
- linkml/utils/schema_builder.py +1 -0
- linkml/utils/schema_fixer.py +3 -2
- linkml/utils/schemaloader.py +1 -9
- linkml/utils/sqlutils.py +39 -25
- linkml/validator/validation_context.py +2 -1
- {linkml-1.7.4.dist-info → linkml-1.7.6.dist-info}/METADATA +4 -1
- {linkml-1.7.4.dist-info → linkml-1.7.6.dist-info}/RECORD +53 -40
- {linkml-1.7.4.dist-info → linkml-1.7.6.dist-info}/entry_points.txt +0 -1
- linkml/generators/sqlddlgen.py +0 -559
- {linkml-1.7.4.dist-info → linkml-1.7.6.dist-info}/LICENSE +0 -0
- {linkml-1.7.4.dist-info → linkml-1.7.6.dist-info}/WHEEL +0 -0
@@ -0,0 +1,504 @@
|
|
1
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Dict, Generator, List, Literal, Optional, Union, overload
|
2
|
+
|
3
|
+
from jinja2 import Environment, PackageLoader
|
4
|
+
from pydantic import BaseModel, Field
|
5
|
+
from pydantic.version import VERSION as PYDANTIC_VERSION
|
6
|
+
|
7
|
+
if int(PYDANTIC_VERSION[0]) >= 2:
|
8
|
+
from pydantic import computed_field
|
9
|
+
else:
|
10
|
+
if TYPE_CHECKING: # pragma: no cover
|
11
|
+
from pydantic.fields import ModelField
|
12
|
+
|
13
|
+
|
14
|
+
class TemplateModel(BaseModel):
|
15
|
+
"""
|
16
|
+
Metaclass to render pydantic models with jinja templates.
|
17
|
+
|
18
|
+
Each subclass needs to declare a :class:`typing.ClassVar` for a
|
19
|
+
jinja template within the `templates` directory.
|
20
|
+
|
21
|
+
Templates are written expecting each of the other TemplateModels
|
22
|
+
to already be rendered to strings - ie. rather than the ``class.py.jinja``
|
23
|
+
template receiving a full :class:`.PydanticAttribute` object or dictionary,
|
24
|
+
it receives it having already been rendered to a string. See the :meth:`.render` method.
|
25
|
+
"""
|
26
|
+
|
27
|
+
template: ClassVar[str]
|
28
|
+
pydantic_ver: int = int(PYDANTIC_VERSION[0])
|
29
|
+
|
30
|
+
def render(self, environment: Optional[Environment] = None) -> str:
|
31
|
+
"""
|
32
|
+
Recursively render a template model to a string.
|
33
|
+
|
34
|
+
For each field in the model, recurse through, rendering each :class:`.TemplateModel`
|
35
|
+
using the template set in :attr:`.TemplateModel.template` , but preserving the structure
|
36
|
+
of lists and dictionaries. Regular :class:`.BaseModel` s are rendered to dictionaries.
|
37
|
+
Any other value is passed through unchanged.
|
38
|
+
"""
|
39
|
+
if environment is None:
|
40
|
+
environment = TemplateModel.environment()
|
41
|
+
|
42
|
+
if int(PYDANTIC_VERSION[0]) >= 2:
|
43
|
+
fields = {**self.model_fields, **self.model_computed_fields}
|
44
|
+
else:
|
45
|
+
fields = self.model_fields
|
46
|
+
|
47
|
+
data = {k: _render(getattr(self, k, None), environment) for k in fields}
|
48
|
+
template = environment.get_template(self.template)
|
49
|
+
return template.render(**data)
|
50
|
+
|
51
|
+
@classmethod
|
52
|
+
def environment(cls) -> Environment:
|
53
|
+
"""
|
54
|
+
Default environment for Template models.
|
55
|
+
|
56
|
+
uses a :class:`jinja2.PackageLoader` for the templates directory within this module
|
57
|
+
with the ``trim_blocks`` and ``lstrip_blocks`` parameters set to ``True`` so that the
|
58
|
+
default templates could be written in a more readable way.
|
59
|
+
"""
|
60
|
+
return Environment(
|
61
|
+
loader=PackageLoader("linkml.generators.pydanticgen", "templates"), trim_blocks=True, lstrip_blocks=True
|
62
|
+
)
|
63
|
+
|
64
|
+
if int(PYDANTIC_VERSION[0]) < 2:
|
65
|
+
# simulate pydantic 2's model_fields behavior
|
66
|
+
# without using classmethod + property decorators
|
67
|
+
# see:
|
68
|
+
# - https://docs.python.org/3/whatsnew/3.11.html#language-builtins
|
69
|
+
# - https://github.com/python/cpython/issues/89519
|
70
|
+
# and:
|
71
|
+
# - https://docs.python.org/3/reference/datamodel.html#customizing-class-creation
|
72
|
+
# for this version.
|
73
|
+
model_fields: ClassVar[Dict[str, "ModelField"]]
|
74
|
+
|
75
|
+
def __init_subclass__(cls, **kwargs):
|
76
|
+
super().__init_subclass__(**kwargs)
|
77
|
+
cls.model_fields = cls.__fields__
|
78
|
+
|
79
|
+
@overload
|
80
|
+
def model_dump(self, mode: Literal["python"] = "python") -> dict: ...
|
81
|
+
|
82
|
+
@overload
|
83
|
+
def model_dump(self, mode: Literal["json"] = "json") -> str: ...
|
84
|
+
|
85
|
+
def model_dump(self, mode: Literal["python", "json"] = "python", **kwargs) -> Union[dict, str]:
|
86
|
+
if mode == "json":
|
87
|
+
return self.json(**kwargs)
|
88
|
+
return self.dict(**kwargs)
|
89
|
+
|
90
|
+
|
91
|
+
def _render(
|
92
|
+
item: Union[TemplateModel, Any, List[Union[Any, TemplateModel]], Dict[str, Union[Any, TemplateModel]]],
|
93
|
+
environment: Environment,
|
94
|
+
) -> Union[str, List[str], Dict[str, str]]:
|
95
|
+
if isinstance(item, TemplateModel):
|
96
|
+
return item.render(environment)
|
97
|
+
elif isinstance(item, list):
|
98
|
+
return [_render(i, environment) for i in item]
|
99
|
+
elif isinstance(item, dict):
|
100
|
+
return {k: _render(v, environment) for k, v in item.items()}
|
101
|
+
elif isinstance(item, BaseModel):
|
102
|
+
if int(PYDANTIC_VERSION[0]) >= 2:
|
103
|
+
fields = item.model_fields
|
104
|
+
else:
|
105
|
+
fields = item.__fields__
|
106
|
+
return {k: _render(getattr(item, k, None), environment) for k in fields.keys()}
|
107
|
+
else:
|
108
|
+
return item
|
109
|
+
|
110
|
+
|
111
|
+
class EnumValue(BaseModel):
|
112
|
+
"""
|
113
|
+
A single value within an :class:`.Enum`
|
114
|
+
"""
|
115
|
+
|
116
|
+
label: str
|
117
|
+
value: str
|
118
|
+
description: Optional[str] = None
|
119
|
+
|
120
|
+
|
121
|
+
class PydanticEnum(TemplateModel):
|
122
|
+
"""
|
123
|
+
Model used to render a :class:`enum.Enum`
|
124
|
+
"""
|
125
|
+
|
126
|
+
template: ClassVar[str] = "enum.py.jinja"
|
127
|
+
|
128
|
+
name: str
|
129
|
+
description: Optional[str] = None
|
130
|
+
values: Dict[str, EnumValue] = Field(default_factory=dict)
|
131
|
+
|
132
|
+
|
133
|
+
class PydanticBaseModel(TemplateModel):
|
134
|
+
"""
|
135
|
+
Parameterization of the base model that generated pydantic classes inherit from
|
136
|
+
"""
|
137
|
+
|
138
|
+
template: ClassVar[str] = "base_model.py.jinja"
|
139
|
+
|
140
|
+
default_name: ClassVar[str] = "ConfiguredBaseModel"
|
141
|
+
name: str = Field(default_factory=lambda: PydanticBaseModel.default_name)
|
142
|
+
extra_fields: Literal["allow", "forbid", "ignore"] = "forbid"
|
143
|
+
"""
|
144
|
+
Sets the ``extra`` model for pydantic models
|
145
|
+
"""
|
146
|
+
fields: Optional[List[str]] = None
|
147
|
+
"""
|
148
|
+
Extra fields that are typically injected into the base model via
|
149
|
+
:attr:`~linkml.generators.pydanticgen.PydanticGenerator.injected_fields`
|
150
|
+
"""
|
151
|
+
|
152
|
+
|
153
|
+
class PydanticAttribute(TemplateModel):
|
154
|
+
"""
|
155
|
+
Reduced version of SlotDefinition that carries all and only the information
|
156
|
+
needed by the template
|
157
|
+
"""
|
158
|
+
|
159
|
+
template: ClassVar[str] = "attribute.py.jinja"
|
160
|
+
|
161
|
+
name: str
|
162
|
+
required: bool = False
|
163
|
+
identifier: bool = False
|
164
|
+
key: bool = False
|
165
|
+
predefined: Optional[str] = None
|
166
|
+
"""Fixed string to use in body of field"""
|
167
|
+
annotations: Optional[dict] = None
|
168
|
+
"""
|
169
|
+
Of the form::
|
170
|
+
|
171
|
+
annotations = {'python_range': {'value': 'int'}}
|
172
|
+
|
173
|
+
.. todo::
|
174
|
+
|
175
|
+
simplify when refactoring pydanticgen, should just be a string or a model
|
176
|
+
|
177
|
+
"""
|
178
|
+
title: Optional[str] = None
|
179
|
+
description: Optional[str] = None
|
180
|
+
equals_number: Optional[Union[int, float]] = None
|
181
|
+
minimum_value: Optional[Union[int, float]] = None
|
182
|
+
maximum_value: Optional[Union[int, float]] = None
|
183
|
+
pattern: Optional[str] = None
|
184
|
+
|
185
|
+
if int(PYDANTIC_VERSION[0]) >= 2:
|
186
|
+
|
187
|
+
@computed_field
|
188
|
+
def field(self) -> str:
|
189
|
+
"""Computed value to use inside of the generated Field"""
|
190
|
+
if self.predefined:
|
191
|
+
return self.predefined
|
192
|
+
elif self.required or self.identifier or self.key:
|
193
|
+
return "..."
|
194
|
+
else:
|
195
|
+
return "None"
|
196
|
+
|
197
|
+
else:
|
198
|
+
field: Optional[str] = None
|
199
|
+
|
200
|
+
def __init__(self, **kwargs):
|
201
|
+
super(PydanticAttribute, self).__init__(**kwargs)
|
202
|
+
if self.predefined:
|
203
|
+
self.field = self.predefined
|
204
|
+
elif self.required or self.identifier or self.key:
|
205
|
+
self.field = "..."
|
206
|
+
else:
|
207
|
+
self.field = "None"
|
208
|
+
|
209
|
+
|
210
|
+
class PydanticValidator(PydanticAttribute):
|
211
|
+
"""
|
212
|
+
Trivial subclass of :class:`.PydanticAttribute` that uses the ``validator.py.jinja`` template instead
|
213
|
+
"""
|
214
|
+
|
215
|
+
template: ClassVar[str] = "validator.py.jinja"
|
216
|
+
|
217
|
+
|
218
|
+
class PydanticClass(TemplateModel):
|
219
|
+
"""
|
220
|
+
Reduced version of ClassDefinition that carries all and only the information
|
221
|
+
needed by the template.
|
222
|
+
|
223
|
+
On instantiation and rendering, will create any additional :attr:`.validators`
|
224
|
+
that are implied by the given :attr:`.attributes`. Currently the only kind of
|
225
|
+
slot-level validators that are created are for those slots that have a ``pattern``
|
226
|
+
property.
|
227
|
+
"""
|
228
|
+
|
229
|
+
template: ClassVar[str] = "class.py.jinja"
|
230
|
+
|
231
|
+
name: str
|
232
|
+
bases: Union[List[str], str] = PydanticBaseModel.default_name
|
233
|
+
description: Optional[str] = None
|
234
|
+
attributes: Optional[Dict[str, PydanticAttribute]] = None
|
235
|
+
|
236
|
+
def _validators(self) -> Optional[Dict[str, PydanticValidator]]:
|
237
|
+
if self.attributes is None:
|
238
|
+
return None
|
239
|
+
|
240
|
+
return {k: PydanticValidator(**v.model_dump()) for k, v in self.attributes.items() if v.pattern is not None}
|
241
|
+
|
242
|
+
if int(PYDANTIC_VERSION[0]) >= 2:
|
243
|
+
|
244
|
+
@computed_field
|
245
|
+
def validators(self) -> Optional[Dict[str, PydanticValidator]]:
|
246
|
+
return self._validators()
|
247
|
+
|
248
|
+
else:
|
249
|
+
validators: Optional[Dict[str, PydanticValidator]]
|
250
|
+
|
251
|
+
def __init__(self, **kwargs):
|
252
|
+
super(PydanticClass, self).__init__(**kwargs)
|
253
|
+
self.validators = self._validators()
|
254
|
+
|
255
|
+
def render(self, environment: Optional[Environment] = None) -> str:
|
256
|
+
"""Overridden in pydantic 1 to ensure that validators are regenerated at rendering time"""
|
257
|
+
# refresh in case attributes have changed since init
|
258
|
+
self.validators = self._validators()
|
259
|
+
return super(PydanticClass, self).render(environment)
|
260
|
+
|
261
|
+
|
262
|
+
class ObjectImport(BaseModel):
|
263
|
+
"""
|
264
|
+
An object to be imported from within a module.
|
265
|
+
|
266
|
+
See :class:`.Import` for examples
|
267
|
+
"""
|
268
|
+
|
269
|
+
name: str
|
270
|
+
alias: Optional[str] = None
|
271
|
+
|
272
|
+
|
273
|
+
class Import(TemplateModel):
|
274
|
+
"""
|
275
|
+
A python module, or module and classes to be imported.
|
276
|
+
|
277
|
+
Examples:
|
278
|
+
|
279
|
+
Module import:
|
280
|
+
|
281
|
+
.. code-block:: python
|
282
|
+
|
283
|
+
>>> Import(module='sys').render()
|
284
|
+
import sys
|
285
|
+
>>> Import(module='numpy', alias='np').render()
|
286
|
+
import numpy as np
|
287
|
+
|
288
|
+
Class import:
|
289
|
+
|
290
|
+
.. code-block:: python
|
291
|
+
|
292
|
+
>>> Import(module='pathlib', objects=[
|
293
|
+
>>> ObjectImport(name="Path"),
|
294
|
+
>>> ObjectImport(name="PurePath", alias="RenamedPurePath")
|
295
|
+
>>> ]).render()
|
296
|
+
from pathlib import (
|
297
|
+
Path,
|
298
|
+
PurePath as RenamedPurePath
|
299
|
+
)
|
300
|
+
|
301
|
+
"""
|
302
|
+
|
303
|
+
template: ClassVar[str] = "imports.py.jinja"
|
304
|
+
module: str
|
305
|
+
alias: Optional[str] = None
|
306
|
+
objects: Optional[List[ObjectImport]] = None
|
307
|
+
|
308
|
+
def merge(self, other: "Import") -> List["Import"]:
|
309
|
+
"""
|
310
|
+
Merge one import with another, see :meth:`.Imports` for an example.
|
311
|
+
|
312
|
+
* If module don't match, return both
|
313
|
+
* If one or the other are a :class:`.ConditionalImport`, return both
|
314
|
+
* If modules match, neither contain objects, but the other has an alias, return the other
|
315
|
+
* If modules match, one contains objects but the other doesn't, return both
|
316
|
+
* If modules match, both contain objects, merge the object lists, preferring objects with aliases
|
317
|
+
"""
|
318
|
+
# return both if we are orthogonal
|
319
|
+
if self.module != other.module:
|
320
|
+
return [self, other]
|
321
|
+
|
322
|
+
# handle conditionals
|
323
|
+
if isinstance(self, ConditionalImport) or isinstance(other, ConditionalImport):
|
324
|
+
# we don't have a good way of combining conditionals, just return both
|
325
|
+
return [self, other]
|
326
|
+
|
327
|
+
# handle module vs. object imports
|
328
|
+
elif other.objects is None and self.objects is None:
|
329
|
+
# both are modules, return the other only if it updates the alias
|
330
|
+
if other.alias:
|
331
|
+
return [other]
|
332
|
+
else:
|
333
|
+
return [self]
|
334
|
+
elif other.objects is not None and self.objects is not None:
|
335
|
+
# both are object imports, merge and return
|
336
|
+
alias = self.alias if other.alias is None else other.alias
|
337
|
+
# FIXME: super awkward implementation
|
338
|
+
# keep ours if it has an alias and the other doesn't,
|
339
|
+
# otherwise take the other's version
|
340
|
+
self_objs = {obj.name: obj for obj in self.objects}
|
341
|
+
other_objs = {
|
342
|
+
obj.name: obj for obj in other.objects if obj.name not in self_objs or self_objs[obj.name].alias is None
|
343
|
+
}
|
344
|
+
self_objs.update(other_objs)
|
345
|
+
|
346
|
+
return [Import(module=self.module, alias=alias, objects=list(self_objs.values()))]
|
347
|
+
else:
|
348
|
+
# one is a module, the other imports objects, keep both
|
349
|
+
return [self, other]
|
350
|
+
|
351
|
+
|
352
|
+
class ConditionalImport(Import):
|
353
|
+
"""
|
354
|
+
Import that depends on some condition in the environment, common when
|
355
|
+
using backported features or straddling dependency versions.
|
356
|
+
|
357
|
+
Make sure that everything that is needed to evaluate the condition is imported
|
358
|
+
before this is added to the injected imports!
|
359
|
+
|
360
|
+
Examples:
|
361
|
+
|
362
|
+
conditionally import Literal from ``typing_extensions`` if on python <= 3.8
|
363
|
+
|
364
|
+
.. code-block:: python
|
365
|
+
:force:
|
366
|
+
|
367
|
+
imports = (Imports() +
|
368
|
+
Import(module='sys') +
|
369
|
+
ConditionalImport(
|
370
|
+
module="typing",
|
371
|
+
objects=[ObjectImport(name="Literal")],
|
372
|
+
condition="sys.version_info >= (3, 8)",
|
373
|
+
alternative=Import(
|
374
|
+
module="typing_extensions",
|
375
|
+
objects=[ObjectImport(name="Literal")]
|
376
|
+
)
|
377
|
+
)
|
378
|
+
|
379
|
+
Renders to:
|
380
|
+
|
381
|
+
.. code-block:: python
|
382
|
+
:force:
|
383
|
+
|
384
|
+
import sys
|
385
|
+
if sys.version_info >= (3, 8):
|
386
|
+
from typing import Literal
|
387
|
+
else:
|
388
|
+
from typing_extensions import Literal
|
389
|
+
|
390
|
+
"""
|
391
|
+
|
392
|
+
template: ClassVar[str] = "conditional_import.py.jinja"
|
393
|
+
condition: str
|
394
|
+
alternative: Import
|
395
|
+
|
396
|
+
|
397
|
+
class Imports(TemplateModel):
|
398
|
+
"""
|
399
|
+
Container class for imports that can handle merging!
|
400
|
+
|
401
|
+
See :class:`.Import` and :class:`.ConditionalImport` for examples of declaring individual imports
|
402
|
+
|
403
|
+
Useful for generation, because each build stage will potentially generate
|
404
|
+
overlapping imports. This ensures that we can keep a collection of imports
|
405
|
+
without having many duplicates.
|
406
|
+
|
407
|
+
Defines methods for adding, iterating, and indexing from within the :attr:`Imports.imports` list.
|
408
|
+
|
409
|
+
Examples:
|
410
|
+
|
411
|
+
.. code-block:: python
|
412
|
+
:force:
|
413
|
+
|
414
|
+
imports = (Imports() +
|
415
|
+
Import(module="sys") +
|
416
|
+
Import(module="pathlib", objects=[ObjectImport(name="Path")]) +
|
417
|
+
Import(module="sys")
|
418
|
+
)
|
419
|
+
|
420
|
+
Renders to:
|
421
|
+
|
422
|
+
.. code-block:: python
|
423
|
+
|
424
|
+
from pathlib import Path
|
425
|
+
import sys
|
426
|
+
|
427
|
+
"""
|
428
|
+
|
429
|
+
template: ClassVar[str] = "imports.py.jinja"
|
430
|
+
|
431
|
+
imports: List[Union[Import, ConditionalImport]] = Field(default_factory=list)
|
432
|
+
|
433
|
+
def __add__(self, other: Import) -> "Imports":
|
434
|
+
# check if we have one of these already
|
435
|
+
imports = self.imports.copy()
|
436
|
+
existing = [i for i in imports if i.module == other.module]
|
437
|
+
|
438
|
+
# if we have nothing importing from this module yet, add it!
|
439
|
+
if len(existing) == 0:
|
440
|
+
imports.append(other)
|
441
|
+
elif len(existing) == 1:
|
442
|
+
imports.remove(existing[0])
|
443
|
+
imports.extend(existing[0].merge(other))
|
444
|
+
else:
|
445
|
+
# we have both a conditional and at least one nonconditional already.
|
446
|
+
# If this is another conditional, we just add it, otherwise, we merge it
|
447
|
+
# with the single nonconditional
|
448
|
+
if isinstance(other, ConditionalImport):
|
449
|
+
imports.append(other)
|
450
|
+
else:
|
451
|
+
for e in existing:
|
452
|
+
if isinstance(e, Import):
|
453
|
+
imports.remove(e)
|
454
|
+
merged = e.merge(other)
|
455
|
+
imports.extend(merged)
|
456
|
+
break
|
457
|
+
return Imports(imports=imports)
|
458
|
+
|
459
|
+
def __len__(self) -> int:
|
460
|
+
return len(self.imports)
|
461
|
+
|
462
|
+
def __iter__(self) -> Generator[Import, None, None]:
|
463
|
+
for i in self.imports:
|
464
|
+
yield i
|
465
|
+
|
466
|
+
def __getitem__(self, item: int) -> Import:
|
467
|
+
return self.imports[item]
|
468
|
+
|
469
|
+
|
470
|
+
class PydanticModule(TemplateModel):
|
471
|
+
"""
|
472
|
+
Top-level container model for generating a pydantic module :)
|
473
|
+
"""
|
474
|
+
|
475
|
+
template: ClassVar[str] = "module.py.jinja"
|
476
|
+
|
477
|
+
metamodel_version: Optional[str] = None
|
478
|
+
version: Optional[str] = None
|
479
|
+
base_model: PydanticBaseModel = PydanticBaseModel()
|
480
|
+
injected_classes: Optional[List[str]] = None
|
481
|
+
imports: List[Union[Import, ConditionalImport]] = Field(default_factory=list)
|
482
|
+
enums: Dict[str, PydanticEnum] = Field(default_factory=dict)
|
483
|
+
classes: Dict[str, PydanticClass] = Field(default_factory=dict)
|
484
|
+
|
485
|
+
if int(PYDANTIC_VERSION[0]) >= 2:
|
486
|
+
|
487
|
+
@computed_field
|
488
|
+
def class_names(self) -> List[str]:
|
489
|
+
return [c.name for c in self.classes.values()]
|
490
|
+
|
491
|
+
else:
|
492
|
+
class_names: List[str] = Field(default_factory=list)
|
493
|
+
|
494
|
+
def __init__(self, **kwargs):
|
495
|
+
super(PydanticModule, self).__init__(**kwargs)
|
496
|
+
self.class_names = [c.name for c in self.classes.values()]
|
497
|
+
|
498
|
+
def render(self, environment: Optional[Environment] = None) -> str:
|
499
|
+
"""
|
500
|
+
Trivial override of parent method for pydantic 1 to ensure that
|
501
|
+
:attr:`.class_names` are correct at render time
|
502
|
+
"""
|
503
|
+
self.class_names = [c.name for c in self.classes.values()]
|
504
|
+
return super(PydanticModule, self).render(environment)
|
@@ -0,0 +1,10 @@
|
|
1
|
+
{{name}}: {{ annotations['python_range'].value }} = Field({{ field }}
|
2
|
+
{%- if title != None %}, title="{{title}}"{% endif -%}
|
3
|
+
{%- if description %}, description="""{{description}}"""{% endif -%}
|
4
|
+
{%- if equals_number != None %}
|
5
|
+
, le={{equals_number}}, ge={{equals_number}}
|
6
|
+
{%- else -%}
|
7
|
+
{%- if minimum_value != None %}, ge={{minimum_value}}{% endif -%}
|
8
|
+
{%- if maximum_value != None %}, le={{maximum_value}}{% endif -%}
|
9
|
+
{%- endif -%}
|
10
|
+
)
|
@@ -0,0 +1,27 @@
|
|
1
|
+
{% if pydantic_ver == 1 %}
|
2
|
+
class WeakRefShimBaseModel(BaseModel):
|
3
|
+
__slots__ = '__weakref__'
|
4
|
+
|
5
|
+
class {{ name }}(WeakRefShimBaseModel,
|
6
|
+
validate_assignment = True,
|
7
|
+
validate_all = True,
|
8
|
+
underscore_attrs_are_private = True,
|
9
|
+
extra = "{{ extra_fields }}",
|
10
|
+
arbitrary_types_allowed = True,
|
11
|
+
use_enum_values = True):
|
12
|
+
{% else %}
|
13
|
+
class {{ name }}(BaseModel):
|
14
|
+
model_config = ConfigDict(
|
15
|
+
validate_assignment = True,
|
16
|
+
validate_default = True,
|
17
|
+
extra = "{{ extra_fields }}",
|
18
|
+
arbitrary_types_allowed = True,
|
19
|
+
use_enum_values = True)
|
20
|
+
{% endif %}
|
21
|
+
{% if fields is not none %}
|
22
|
+
{% for field in fields %}
|
23
|
+
{{ field }}
|
24
|
+
{% endfor %}
|
25
|
+
{% else %}
|
26
|
+
{{ "pass" }}
|
27
|
+
{% endif %}
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class {{ name }}({% if bases is string %}{{ bases }}{% else %}{{ bases | join(', ') }}{% endif %}):
|
2
|
+
{% if description %}
|
3
|
+
"""
|
4
|
+
{{ description | indent(width=4) }}
|
5
|
+
"""
|
6
|
+
{% endif -%}
|
7
|
+
{% if attributes or validators %}
|
8
|
+
{% if attributes %}
|
9
|
+
{% for attr in attributes.values() %}
|
10
|
+
{{ attr }}
|
11
|
+
{% endfor -%}
|
12
|
+
{% endif %}
|
13
|
+
{% if validators %}
|
14
|
+
{% for validator in validators.values() %}
|
15
|
+
|
16
|
+
{{ validator }}
|
17
|
+
{% endfor -%}
|
18
|
+
{% endif %}
|
19
|
+
{% else %}
|
20
|
+
pass
|
21
|
+
{% endif %}
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class {{ name }}(str{% if values %}, Enum{% endif %}):
|
2
|
+
{% if description %}
|
3
|
+
"""
|
4
|
+
{{ description }}
|
5
|
+
"""
|
6
|
+
{% endif %}
|
7
|
+
{% if values %}
|
8
|
+
{% for pv in values.values() -%}
|
9
|
+
{% if pv.description %}
|
10
|
+
# {{pv.description}}
|
11
|
+
{% endif %}
|
12
|
+
{{pv.label}} = "{{pv.value}}"
|
13
|
+
{% endfor %}
|
14
|
+
{% else %}
|
15
|
+
pass
|
16
|
+
{% endif %}
|
@@ -0,0 +1,13 @@
|
|
1
|
+
{% if pydantic_ver == 1 %}
|
2
|
+
# Update forward refs
|
3
|
+
# see https://pydantic-docs.helpmanual.io/usage/postponed_annotations/
|
4
|
+
{% for c in class_names -%}
|
5
|
+
{{ c }}.update_forward_refs()
|
6
|
+
{% endfor %}
|
7
|
+
{% else %}
|
8
|
+
# Model rebuild
|
9
|
+
# see https://pydantic-docs.helpmanual.io/usage/models/#rebuilding-a-model
|
10
|
+
{% for c in class_names -%}
|
11
|
+
{{ c }}.model_rebuild()
|
12
|
+
{% endfor %}
|
13
|
+
{% endif %}
|
@@ -0,0 +1,31 @@
|
|
1
|
+
{% macro import_(module, alias=None, objects = None) %}
|
2
|
+
{%- if objects is none and alias is none %}
|
3
|
+
import {{ module }}
|
4
|
+
{%- elif objects is none and alias is string %}
|
5
|
+
import {{ module }} as {{ alias }}
|
6
|
+
{%- else %}
|
7
|
+
{% if objects | length == 1 %}
|
8
|
+
from {{ module }} import {{ objects[0]['name'] }} {% if objects[0]['alias'] is not none %} as {{ objects[0]['alias'] }} {% endif %}
|
9
|
+
{%- else %}
|
10
|
+
from {{ module }} import (
|
11
|
+
{% for object in objects %}
|
12
|
+
{% if object['alias'] is string %}
|
13
|
+
{{ object['name'] }} as {{ object['alias'] }}
|
14
|
+
{%- else %}
|
15
|
+
{{ object['name'] }}
|
16
|
+
{%- endif %}
|
17
|
+
{% if not loop.last %},{{ '\n' }}{% else %}{{ '\n' }}{% endif %}
|
18
|
+
{% endfor %}
|
19
|
+
)
|
20
|
+
{%- endif %}
|
21
|
+
{%- endif %}
|
22
|
+
{% endmacro %}
|
23
|
+
{%- if module %}
|
24
|
+
{{ import_(module, alias, objects) }}
|
25
|
+
{% endif -%}
|
26
|
+
{#- For when used with Imports container -#}
|
27
|
+
{%- if imports -%}
|
28
|
+
{%- for i in imports -%}
|
29
|
+
{{ i }}
|
30
|
+
{%- endfor -%}
|
31
|
+
{% endif -%}
|
@@ -0,0 +1,27 @@
|
|
1
|
+
{% for import in imports %}
|
2
|
+
{{ import }}
|
3
|
+
{%- endfor -%}
|
4
|
+
|
5
|
+
metamodel_version = "{{metamodel_version}}"
|
6
|
+
version = "{{version if version else None}}"
|
7
|
+
|
8
|
+
|
9
|
+
{{ base_model }}
|
10
|
+
{% if enums %}
|
11
|
+
{% for e in enums.values() %}
|
12
|
+
|
13
|
+
{{ e }}
|
14
|
+
{% endfor %}
|
15
|
+
{% endif %}
|
16
|
+
{% if injected_classes %}
|
17
|
+
{% for c in injected_classes%}
|
18
|
+
|
19
|
+
{{ c }}
|
20
|
+
{% endfor %}
|
21
|
+
{% endif %}
|
22
|
+
{% for c in classes.values() %}
|
23
|
+
|
24
|
+
{{ c }}
|
25
|
+
{% endfor %}
|
26
|
+
|
27
|
+
{% include 'footer.py.jinja' %}
|
@@ -0,0 +1,15 @@
|
|
1
|
+
{% if pydantic_ver == 1 %}
|
2
|
+
@validator('{{name}}', allow_reuse=True)
|
3
|
+
{% else %}
|
4
|
+
@field_validator('{{name}}')
|
5
|
+
{% endif %}
|
6
|
+
def pattern_{{name}}(cls, v):
|
7
|
+
pattern=re.compile(r"{{pattern}}")
|
8
|
+
if isinstance(v,list):
|
9
|
+
for element in v:
|
10
|
+
if not pattern.match(element):
|
11
|
+
raise ValueError(f"Invalid {{name}} format: {element}")
|
12
|
+
elif isinstance(v,str):
|
13
|
+
if not pattern.match(v):
|
14
|
+
raise ValueError(f"Invalid {{name}} format: {v}")
|
15
|
+
return v
|