mdmodels 0.1.0__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.
- mdmodels/__init__.py +35 -0
- mdmodels/adder_method.py +130 -0
- mdmodels/create.py +498 -0
- mdmodels/datamodel.py +301 -0
- mdmodels/git_utils.py +50 -0
- mdmodels/graph/__init__.py +45 -0
- mdmodels/graph/basenode.py +196 -0
- mdmodels/graph/create.py +151 -0
- mdmodels/graph/relation.py +176 -0
- mdmodels/library.py +226 -0
- mdmodels/llm/__init__.py +34 -0
- mdmodels/llm/embed.py +81 -0
- mdmodels/llm/fetcher.py +141 -0
- mdmodels/llm/handler.py +137 -0
- mdmodels/llm/prompts.py +157 -0
- mdmodels/llm/response.py +87 -0
- mdmodels/llm/templates/__init__.py +23 -0
- mdmodels/llm/templates/dataset_query.py +231 -0
- mdmodels/meta.py +96 -0
- mdmodels/path.py +306 -0
- mdmodels/reference.py +131 -0
- mdmodels/sql/__init__.py +27 -0
- mdmodels/sql/base.py +43 -0
- mdmodels/sql/connector.py +215 -0
- mdmodels/sql/create.py +425 -0
- mdmodels/sql/insert.py +240 -0
- mdmodels/sql/linked_type.py +123 -0
- mdmodels/sql/utils.py +197 -0
- mdmodels/units/__init__.py +21 -0
- mdmodels/units/annotation.py +41 -0
- mdmodels/units/converter.py +210 -0
- mdmodels/units/mappings.py +59 -0
- mdmodels/units/unit_definition.py +168 -0
- mdmodels/utils.py +77 -0
- mdmodels-0.1.0.dist-info/METADATA +75 -0
- mdmodels-0.1.0.dist-info/RECORD +37 -0
- mdmodels-0.1.0.dist-info/WHEEL +4 -0
mdmodels/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# -----------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) 2024 Jan Range
|
|
3
|
+
#
|
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
# in the Software without restriction, including without limitation the rights
|
|
7
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
# furnished to do so, subject to the following conditions:
|
|
10
|
+
# #
|
|
11
|
+
# The above copyright notice and this permission notice shall be included in
|
|
12
|
+
# all copies or substantial portions of the Software.
|
|
13
|
+
# #
|
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
20
|
+
# THE SOFTWARE.
|
|
21
|
+
# -----------------------------------------------------------------------------
|
|
22
|
+
import nest_asyncio
|
|
23
|
+
from mdmodels_core import Templates # noqa
|
|
24
|
+
|
|
25
|
+
from .datamodel import DataModel
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def patch_nest_asyncio():
|
|
29
|
+
nest_asyncio.apply()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"DataModel",
|
|
34
|
+
"Templates",
|
|
35
|
+
]
|
mdmodels/adder_method.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# -----------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) 2024 Jan Range
|
|
3
|
+
#
|
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
# in the Software without restriction, including without limitation the rights
|
|
7
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
# furnished to do so, subject to the following conditions:
|
|
10
|
+
# #
|
|
11
|
+
# The above copyright notice and this permission notice shall be included in
|
|
12
|
+
# all copies or substantial portions of the Software.
|
|
13
|
+
# #
|
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
20
|
+
# THE SOFTWARE.
|
|
21
|
+
# -----------------------------------------------------------------------------
|
|
22
|
+
import builtins
|
|
23
|
+
from typing import Union, get_origin, get_args, Annotated
|
|
24
|
+
import warnings
|
|
25
|
+
|
|
26
|
+
import forge
|
|
27
|
+
|
|
28
|
+
from forge import FParameter, sign, FSignature
|
|
29
|
+
|
|
30
|
+
from mdmodels import DataModel
|
|
31
|
+
from mdmodels.units.unit_definition import UnitDefinition, BaseUnit
|
|
32
|
+
from mdmodels.utils import extract_dtype
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def apply_adder_methods(cls: type[DataModel]):
|
|
36
|
+
"""
|
|
37
|
+
Apply adder methods to the given DataModel class.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
cls (type[DataModel]): The DataModel class to which adder methods will be applied.
|
|
41
|
+
"""
|
|
42
|
+
for name, field in cls.model_fields.items():
|
|
43
|
+
if get_origin(field.annotation) is not list:
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
method_name = f"add_to_{name}"
|
|
47
|
+
underlying_type = get_args(field.annotation)
|
|
48
|
+
|
|
49
|
+
if len(underlying_type) != 1 or get_origin(underlying_type[0]) is Union:
|
|
50
|
+
warnings.warn(
|
|
51
|
+
f"Only one type is supported for adder methods. {cls.__name__}.{name} has multiple types. Skipping.",
|
|
52
|
+
)
|
|
53
|
+
continue
|
|
54
|
+
else:
|
|
55
|
+
underlying_type = underlying_type[0]
|
|
56
|
+
|
|
57
|
+
if underlying_type in [UnitDefinition, BaseUnit] or is_builtin_type(
|
|
58
|
+
underlying_type
|
|
59
|
+
):
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
add_method = _create_add_method(underlying_type, name)
|
|
63
|
+
setattr(cls, method_name, add_method)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def is_builtin_type(obj):
|
|
67
|
+
"""
|
|
68
|
+
Check if the given object is a built-in type.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
obj: The object to check.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
bool: True if the object is a built-in type, False otherwise.
|
|
75
|
+
"""
|
|
76
|
+
return obj.__name__ in dir(builtins)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _create_add_method(
|
|
80
|
+
coll_cls: type[DataModel],
|
|
81
|
+
field: str,
|
|
82
|
+
):
|
|
83
|
+
"""
|
|
84
|
+
Create an adder method for the given collection class and field.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
coll_cls (type[DataModel]): The collection class for which the adder method is created.
|
|
88
|
+
field (str): The field to which the adder method will be applied.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
function: The created adder method.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
@sign(*_create_signature(coll_cls))
|
|
95
|
+
def add_method(self, **kwargs):
|
|
96
|
+
coll = getattr(self, field)
|
|
97
|
+
coll.append(coll_cls(**kwargs))
|
|
98
|
+
return coll[-1]
|
|
99
|
+
|
|
100
|
+
return add_method
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _create_signature(coll_cls: type[DataModel]):
|
|
104
|
+
"""
|
|
105
|
+
Create a signature for the adder method.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
coll_cls (type[DataModel]): The collection class for which the signature is created.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
FSignature: The created signature.
|
|
112
|
+
"""
|
|
113
|
+
if get_origin(coll_cls) is Annotated:
|
|
114
|
+
coll_cls = extract_dtype(coll_cls)
|
|
115
|
+
|
|
116
|
+
annotations = coll_cls.__annotations__
|
|
117
|
+
parameters = [
|
|
118
|
+
FParameter(
|
|
119
|
+
kind=FParameter.KEYWORD_ONLY,
|
|
120
|
+
name=name,
|
|
121
|
+
type=annotations[name],
|
|
122
|
+
default=None if get_origin(annotations[name]) is not list else [],
|
|
123
|
+
)
|
|
124
|
+
for name in annotations
|
|
125
|
+
if name != "return"
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
return FSignature(
|
|
129
|
+
[forge.self] + parameters,
|
|
130
|
+
)
|
mdmodels/create.py
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
# -----------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) 2024 Jan Range
|
|
3
|
+
#
|
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
# in the Software without restriction, including without limitation the rights
|
|
7
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
# furnished to do so, subject to the following conditions:
|
|
10
|
+
# #
|
|
11
|
+
# The above copyright notice and this permission notice shall be included in
|
|
12
|
+
# all copies or substantial portions of the Software.
|
|
13
|
+
# #
|
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
20
|
+
# THE SOFTWARE.
|
|
21
|
+
# -----------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
import copy
|
|
24
|
+
import pathlib
|
|
25
|
+
from enum import Enum
|
|
26
|
+
from functools import partial
|
|
27
|
+
from typing import Any, Annotated, ForwardRef, Union
|
|
28
|
+
|
|
29
|
+
import httpx
|
|
30
|
+
import validators
|
|
31
|
+
from mdmodels_core import DataModel as RSDataModel # type: ignore
|
|
32
|
+
from pydantic import BeforeValidator
|
|
33
|
+
from pydantic_core.core_schema import ValidationInfo
|
|
34
|
+
from pydantic_xml import RootXmlModel, create_model, attr, element, wrapped
|
|
35
|
+
|
|
36
|
+
from mdmodels.adder_method import apply_adder_methods
|
|
37
|
+
from mdmodels.datamodel import DataModel
|
|
38
|
+
from mdmodels.library import Library
|
|
39
|
+
from mdmodels.path import PathFactory
|
|
40
|
+
from mdmodels.reference import ReferenceContext
|
|
41
|
+
from mdmodels.units.annotation import UnitDefinitionAnnot
|
|
42
|
+
from mdmodels.utils import extract_option
|
|
43
|
+
|
|
44
|
+
# Mapping of string type names to Python units
|
|
45
|
+
TYPE_MAPPING = {
|
|
46
|
+
"string": str,
|
|
47
|
+
"integer": int,
|
|
48
|
+
"float": float,
|
|
49
|
+
"boolean": bool,
|
|
50
|
+
"number": float,
|
|
51
|
+
"date": str,
|
|
52
|
+
"bytes": bytes,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class StringElement(RootXmlModel):
|
|
57
|
+
root: str
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class FloatElement(RootXmlModel):
|
|
61
|
+
root: float
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class BooleanElement(RootXmlModel):
|
|
65
|
+
root: bool
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class IntegerElement(RootXmlModel):
|
|
69
|
+
root: int
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class BytesElement(RootXmlModel):
|
|
73
|
+
root: bytes
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
BASIC_TYPE_ELEMENTS = {
|
|
77
|
+
str: StringElement,
|
|
78
|
+
float: FloatElement,
|
|
79
|
+
bool: BooleanElement,
|
|
80
|
+
int: IntegerElement,
|
|
81
|
+
bytes: BytesElement,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def build_module(
|
|
86
|
+
path: pathlib.Path | str | None = None,
|
|
87
|
+
data_model: RSDataModel | None = None,
|
|
88
|
+
) -> Library:
|
|
89
|
+
"""
|
|
90
|
+
Create a data model module from a markdown file.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
path (pathlib.Path | str): Path to the markdown file.
|
|
94
|
+
data_model (RSDataModel | None): The data model. If None, it will be initialized from the path.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Library: A module containing the generated data model.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
if data_model and path:
|
|
101
|
+
raise ValueError("Only one of 'path' or 'data_model' should be provided")
|
|
102
|
+
|
|
103
|
+
if data_model:
|
|
104
|
+
assert isinstance(data_model, RSDataModel), "data_model must be an RSDataModel"
|
|
105
|
+
dm = data_model
|
|
106
|
+
elif path:
|
|
107
|
+
dm = init_data_model(path)
|
|
108
|
+
else:
|
|
109
|
+
raise ValueError("Either 'path' or 'data_model' must be provided")
|
|
110
|
+
|
|
111
|
+
global path_factory
|
|
112
|
+
global references
|
|
113
|
+
global module
|
|
114
|
+
|
|
115
|
+
references = {}
|
|
116
|
+
path_factory = PathFactory(model=dm)
|
|
117
|
+
module = Library(rust_model=dm, path_factory=path_factory)
|
|
118
|
+
|
|
119
|
+
for rs_type in dm.model.objects:
|
|
120
|
+
if rs_type.name in module:
|
|
121
|
+
module[rs_type.name].__mdmodels__.path_factory = path_factory
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
py_type = build_type(dm, rs_type, module)
|
|
125
|
+
py_type.__mdmodels__.path_factory = path_factory # type: ignore
|
|
126
|
+
|
|
127
|
+
module[rs_type.name] = py_type
|
|
128
|
+
|
|
129
|
+
for obj, reference in references.items():
|
|
130
|
+
module[obj].__mdmodels__.reference_paths += reference
|
|
131
|
+
|
|
132
|
+
module.resolve_target_primary_keys()
|
|
133
|
+
|
|
134
|
+
return module
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def init_data_model(path):
|
|
138
|
+
"""
|
|
139
|
+
Initialize the data model from a path or URL.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
path (str | pathlib.Path): Path or URL to the markdown file.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
RSDataModel: The initialized data model.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
if validators.url(path):
|
|
149
|
+
content = httpx.get(path).text
|
|
150
|
+
return RSDataModel.from_markdown_string(content)
|
|
151
|
+
else:
|
|
152
|
+
if isinstance(path, str):
|
|
153
|
+
path = pathlib.Path(path)
|
|
154
|
+
|
|
155
|
+
assert path.exists(), f"Path '{path}' does not exist"
|
|
156
|
+
return RSDataModel.from_markdown(str(path))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def build_type(
|
|
160
|
+
dm: RSDataModel,
|
|
161
|
+
rs_type,
|
|
162
|
+
py_types: dict,
|
|
163
|
+
):
|
|
164
|
+
"""
|
|
165
|
+
Build a Python type from a data model type.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
dm (RSDataModel): The data model.
|
|
169
|
+
rs_type: The data model type.
|
|
170
|
+
py_types (dict): Dictionary of Python units.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
forward_refs = []
|
|
174
|
+
attrs = {}
|
|
175
|
+
|
|
176
|
+
for attribute in rs_type.attributes:
|
|
177
|
+
params = {}
|
|
178
|
+
dtypes = []
|
|
179
|
+
|
|
180
|
+
for dtype in attribute.dtypes:
|
|
181
|
+
dtype = get_dtype(dtype, dm, py_types, rs_type.name)
|
|
182
|
+
|
|
183
|
+
if dtype.__name__ in py_types or hasattr(dtype, "__recursive__"):
|
|
184
|
+
module.add_cross_connection(
|
|
185
|
+
source_type=rs_type.name,
|
|
186
|
+
source_attr=attribute.name,
|
|
187
|
+
target_type=dtype.__name__,
|
|
188
|
+
is_array=attribute.is_array,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if hasattr(dtype, "__recursive__"):
|
|
192
|
+
dtype = ForwardRef(dtype.__name__)
|
|
193
|
+
forward_refs.append(dtype)
|
|
194
|
+
|
|
195
|
+
before_validator = partial(
|
|
196
|
+
_check_type_compliance,
|
|
197
|
+
cls=dtype, # type: ignore
|
|
198
|
+
py_types=py_types, # type: ignore
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
dtype = Annotated[dtype, BeforeValidator(before_validator)] # type: ignore
|
|
202
|
+
|
|
203
|
+
dtypes.append(dtype)
|
|
204
|
+
|
|
205
|
+
dtypes = _set_custom_tags(attribute, dtypes)
|
|
206
|
+
|
|
207
|
+
if len(dtypes) > 1:
|
|
208
|
+
dtype = Union[tuple(dtypes)] # type: ignore
|
|
209
|
+
elif len(dtypes) == 0:
|
|
210
|
+
raise ValueError(f"No data type found for attribute {attribute.name}")
|
|
211
|
+
else:
|
|
212
|
+
dtype = dtypes[0]
|
|
213
|
+
|
|
214
|
+
if attribute.is_array:
|
|
215
|
+
dtype = list[dtype]
|
|
216
|
+
|
|
217
|
+
if description := attribute.docstring:
|
|
218
|
+
params["description"] = description
|
|
219
|
+
|
|
220
|
+
params["default"] = _get_default(attribute.default)
|
|
221
|
+
|
|
222
|
+
if not attribute.required and not attribute.is_array:
|
|
223
|
+
dtype = dtype | None # type: ignore
|
|
224
|
+
elif not attribute.required and attribute.is_array:
|
|
225
|
+
params["default_factory"] = list
|
|
226
|
+
del params["default"]
|
|
227
|
+
|
|
228
|
+
attrs[attribute.name] = _process_xml_attribute(attribute, dtype, params)
|
|
229
|
+
|
|
230
|
+
model = create_model(
|
|
231
|
+
rs_type.name,
|
|
232
|
+
__base__=DataModel,
|
|
233
|
+
**attrs,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
for ref in forward_refs:
|
|
237
|
+
ref._evaluate(py_types, py_types, set())
|
|
238
|
+
|
|
239
|
+
_extract_references(rs_type)
|
|
240
|
+
apply_adder_methods(model)
|
|
241
|
+
|
|
242
|
+
return model
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _process_xml_attribute(attribute, dtype, params: dict) -> tuple[type, Any]:
|
|
246
|
+
"""
|
|
247
|
+
Process an XML attribute and update the attrs dictionary.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
attribute: The attribute to process.
|
|
251
|
+
dtype: The data type of the attribute.
|
|
252
|
+
params: Additional parameters for the attribute.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Tuple[type, Any]: The processed attribute.
|
|
256
|
+
"""
|
|
257
|
+
if attribute.xml.is_attr:
|
|
258
|
+
assert not _is_wrapped_xml(
|
|
259
|
+
attribute.xml.name
|
|
260
|
+
), "Wrapped XML is not allowed to be an attribute"
|
|
261
|
+
return (
|
|
262
|
+
dtype,
|
|
263
|
+
attr(
|
|
264
|
+
name=attribute.xml.name,
|
|
265
|
+
**params,
|
|
266
|
+
),
|
|
267
|
+
)
|
|
268
|
+
elif _is_wrapped_xml(attribute.xml.name):
|
|
269
|
+
*path, name = attribute.xml.name.split("/")
|
|
270
|
+
return (dtype, wrapped("/".join(path), element(tag=name, **params)))
|
|
271
|
+
elif _is_multiple_xml(attribute.xml.name):
|
|
272
|
+
return (dtype, element(**params))
|
|
273
|
+
else:
|
|
274
|
+
return (dtype, element(tag=attribute.xml.name, **params))
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _set_custom_tags(attribute, dtypes):
|
|
278
|
+
"""
|
|
279
|
+
Set custom XML tags for the given attribute and data types.
|
|
280
|
+
|
|
281
|
+
This function processes the XML name of the attribute to determine if it
|
|
282
|
+
contains multiple or wrapped XML elements. It then assigns custom XML tags
|
|
283
|
+
to the data types based on the parsed names.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
attribute: The attribute containing the XML name to process.
|
|
287
|
+
dtypes: A list of data types to which custom XML tags will be assigned.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
list: A list of data types with assigned custom XML tags.
|
|
291
|
+
"""
|
|
292
|
+
new_dtypes = []
|
|
293
|
+
if _is_multiple_xml(attribute.xml.name):
|
|
294
|
+
paths = [p.strip() for p in attribute.xml.name.split(",")]
|
|
295
|
+
|
|
296
|
+
if _is_wrapped_xml(attribute.xml.name):
|
|
297
|
+
names = [p.split("/")[-1] for p in paths]
|
|
298
|
+
else:
|
|
299
|
+
names = paths
|
|
300
|
+
|
|
301
|
+
for dtype, name in zip(dtypes, names):
|
|
302
|
+
if dtype in BASIC_TYPE_ELEMENTS:
|
|
303
|
+
dtype = copy.copy(BASIC_TYPE_ELEMENTS[dtype])
|
|
304
|
+
dtype.__xml_tag__ = name
|
|
305
|
+
new_dtypes.append(dtype)
|
|
306
|
+
else:
|
|
307
|
+
dtype = copy.copy(dtype)
|
|
308
|
+
dtype.__xml_tag__ = name
|
|
309
|
+
new_dtypes.append(dtype)
|
|
310
|
+
else:
|
|
311
|
+
return dtypes
|
|
312
|
+
|
|
313
|
+
return new_dtypes
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _is_multiple_xml(name: str):
|
|
317
|
+
"""
|
|
318
|
+
Check if the XML name contains multiple elements.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
name (str): The XML name to check.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
bool: True if the name contains multiple elements, False otherwise.
|
|
325
|
+
"""
|
|
326
|
+
return len(name.split(",")) > 1
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _is_wrapped_xml(name: str):
|
|
330
|
+
"""
|
|
331
|
+
Check if the XML name is wrapped, indicating a nested structure.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
name (str): The XML name to check.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
bool: True if the name is wrapped, False otherwise.
|
|
338
|
+
"""
|
|
339
|
+
return len(name.split("/")) > 1
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _extract_references(obj):
|
|
343
|
+
"""Extract attribute references from an object.
|
|
344
|
+
|
|
345
|
+
References are used for cross-referencing objects in the data model.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
obj: The object to extract references from.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
List[str]: A list of references.
|
|
352
|
+
"""
|
|
353
|
+
|
|
354
|
+
for attribute in obj.attributes:
|
|
355
|
+
if ref := extract_option(attribute, "references"):
|
|
356
|
+
_create_ref_context(attribute, obj, ref)
|
|
357
|
+
|
|
358
|
+
# Add cross connection for DB schemes
|
|
359
|
+
tbl, col = path_factory.get_attr_type_by_dot(ref)
|
|
360
|
+
module.add_cross_connection(
|
|
361
|
+
source_type=obj.name,
|
|
362
|
+
source_attr=attribute.name,
|
|
363
|
+
target_type=tbl,
|
|
364
|
+
target_attr=col,
|
|
365
|
+
is_identifier=True,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
return references
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _create_ref_context(attr, obj, ref: str):
|
|
372
|
+
"""
|
|
373
|
+
Process a reference attribute and update the references dictionary.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
attr: The attribute containing the reference.
|
|
377
|
+
obj: The object to which the attribute belongs.
|
|
378
|
+
ref (str): The reference string in dot notation.
|
|
379
|
+
|
|
380
|
+
"""
|
|
381
|
+
root = ref.split(".")[0]
|
|
382
|
+
target_path = path_factory.dot_to_json_path(ref)
|
|
383
|
+
source_paths = path_factory.get_type_paths(root, obj.name, attr.name)
|
|
384
|
+
for source_path in source_paths:
|
|
385
|
+
ctx = ReferenceContext(
|
|
386
|
+
source_path=source_path,
|
|
387
|
+
target_path=target_path,
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
if root not in references:
|
|
391
|
+
references[root] = []
|
|
392
|
+
|
|
393
|
+
references[root].append(ctx)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _get_default(default):
|
|
397
|
+
"""
|
|
398
|
+
Get the default value from a given default object.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
default: The default object to extract the value from.
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
The extracted default value, which can be a string, boolean, integer, float, or None.
|
|
405
|
+
"""
|
|
406
|
+
if default is None:
|
|
407
|
+
return None
|
|
408
|
+
|
|
409
|
+
if default.is_string():
|
|
410
|
+
return default.as_string().replace('"', "")
|
|
411
|
+
elif default.is_boolean():
|
|
412
|
+
return default.as_boolean()
|
|
413
|
+
elif default.is_integer():
|
|
414
|
+
return default.as_integer()
|
|
415
|
+
elif default.is_float():
|
|
416
|
+
return default.as_float()
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def get_dtype(
|
|
420
|
+
dtype: str,
|
|
421
|
+
dm: RSDataModel,
|
|
422
|
+
py_types: dict,
|
|
423
|
+
rs_type_name: str,
|
|
424
|
+
):
|
|
425
|
+
"""
|
|
426
|
+
Get the Python data type for an attribute.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
dtype: The data type.
|
|
430
|
+
dm (RSDataModel): The data model.
|
|
431
|
+
py_types (dict): Dictionary of Python units.
|
|
432
|
+
rs_type_name (str): The name of the data model type.
|
|
433
|
+
Returns:
|
|
434
|
+
type: The Python data type.
|
|
435
|
+
"""
|
|
436
|
+
|
|
437
|
+
if dtype == rs_type_name:
|
|
438
|
+
return type(rs_type_name, (DataModel,), {"__recursive__": True})
|
|
439
|
+
|
|
440
|
+
if dtype in TYPE_MAPPING:
|
|
441
|
+
return TYPE_MAPPING[dtype]
|
|
442
|
+
elif dtype == "UnitDefinition":
|
|
443
|
+
return UnitDefinitionAnnot
|
|
444
|
+
elif dtype in py_types:
|
|
445
|
+
return py_types[dtype]
|
|
446
|
+
elif sub_obj := next((o for o in dm.model.objects if o.name == dtype), None):
|
|
447
|
+
py_types[dtype] = build_type(dm, sub_obj, py_types)
|
|
448
|
+
return py_types[dtype]
|
|
449
|
+
elif enum_obj := next((o for o in dm.model.enums if o.name == dtype), None):
|
|
450
|
+
py_types[dtype] = build_enum(enum_obj, py_types)
|
|
451
|
+
return py_types[dtype]
|
|
452
|
+
else:
|
|
453
|
+
raise ValueError(f"Unknown type {dtype}")
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def build_enum(enum_obj, py_types: dict):
|
|
457
|
+
"""
|
|
458
|
+
Create a Python Enum from a data model Enum object.
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
enum_obj: The Enum object.
|
|
462
|
+
py_types (dict): Dictionary of Python types/enums.
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
Enum: The created Python Enum.
|
|
466
|
+
"""
|
|
467
|
+
|
|
468
|
+
if enum_obj.name in py_types:
|
|
469
|
+
return py_types[enum_obj.name]
|
|
470
|
+
|
|
471
|
+
return Enum(enum_obj.name, enum_obj.mappings)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _check_type_compliance(
|
|
475
|
+
value: Any,
|
|
476
|
+
info: ValidationInfo,
|
|
477
|
+
cls: type[DataModel] | ForwardRef,
|
|
478
|
+
py_types: Library,
|
|
479
|
+
):
|
|
480
|
+
"""
|
|
481
|
+
Check if the value complies with the expected data model type.
|
|
482
|
+
|
|
483
|
+
Args:
|
|
484
|
+
value (Any): The value to check.
|
|
485
|
+
info (ValidationInfo): Validation information.
|
|
486
|
+
cls (type[DataModel]): The expected data model class.
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
Any: The validated value.
|
|
490
|
+
"""
|
|
491
|
+
if not hasattr(value, "model_fields"):
|
|
492
|
+
return value
|
|
493
|
+
if isinstance(cls, ForwardRef):
|
|
494
|
+
cls = cls._evaluate(py_types, py_types, set()) # type: ignore
|
|
495
|
+
if type(value).__name__ == cls.__name__: # type: ignore
|
|
496
|
+
return cls(**value.model_dump()) # type: ignore
|
|
497
|
+
|
|
498
|
+
return value
|