cognite-neat 0.76.2__py3-none-any.whl → 0.77.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.
Potentially problematic release.
This version of cognite-neat might be problematic. Click here for more details.
- cognite/neat/_version.py +1 -1
- cognite/neat/rules/exporters/_rules2dms.py +3 -5
- cognite/neat/rules/exporters/_rules2excel.py +62 -37
- cognite/neat/rules/importers/_dms2rules.py +64 -31
- cognite/neat/rules/importers/_spreadsheet2rules.py +77 -58
- cognite/neat/rules/issues/dms.py +65 -0
- cognite/neat/rules/models/dms/_exporter.py +1 -3
- cognite/neat/rules/models/dms/_rules.py +2 -4
- cognite/neat/rules/models/dms/_rules_input.py +9 -0
- cognite/neat/rules/models/dms/_schema.py +160 -39
- cognite/neat/rules/models/dms/_serializer.py +5 -5
- cognite/neat/rules/models/dms/_validation.py +34 -1
- cognite/neat/rules/models/domain.py +1 -0
- cognite/neat/rules/models/information/_converter.py +2 -0
- cognite/neat/workflows/steps/lib/current/rules_exporter.py +2 -1
- {cognite_neat-0.76.2.dist-info → cognite_neat-0.77.0.dist-info}/METADATA +1 -1
- {cognite_neat-0.76.2.dist-info → cognite_neat-0.77.0.dist-info}/RECORD +20 -20
- {cognite_neat-0.76.2.dist-info → cognite_neat-0.77.0.dist-info}/LICENSE +0 -0
- {cognite_neat-0.76.2.dist-info → cognite_neat-0.77.0.dist-info}/WHEEL +0 -0
- {cognite_neat-0.76.2.dist-info → cognite_neat-0.77.0.dist-info}/entry_points.txt +0 -0
cognite/neat/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.77.0"
|
|
@@ -5,7 +5,7 @@ from typing import Literal, TypeAlias, cast
|
|
|
5
5
|
|
|
6
6
|
from cognite.client import CogniteClient
|
|
7
7
|
from cognite.client.data_classes._base import CogniteResource, CogniteResourceList
|
|
8
|
-
from cognite.client.data_classes.data_modeling import DataModelApply, DataModelId
|
|
8
|
+
from cognite.client.data_classes.data_modeling import DataModelApply, DataModelApplyList, DataModelId
|
|
9
9
|
from cognite.client.exceptions import CogniteAPIError
|
|
10
10
|
|
|
11
11
|
from cognite.neat.rules import issues
|
|
@@ -114,9 +114,7 @@ class DMSExporter(CDFExporter[DMSSchema]):
|
|
|
114
114
|
dms_rules = rules.as_dms_architect_rules()
|
|
115
115
|
else:
|
|
116
116
|
raise ValueError(f"{type(rules).__name__} cannot be exported to DMS")
|
|
117
|
-
return dms_rules.as_schema(
|
|
118
|
-
include_ref=True, include_pipeline=self.export_pipeline, instance_space=self.instance_space
|
|
119
|
-
)
|
|
117
|
+
return dms_rules.as_schema(include_pipeline=self.export_pipeline, instance_space=self.instance_space)
|
|
120
118
|
|
|
121
119
|
def delete_from_cdf(self, rules: Rules, client: CogniteClient, dry_run: bool = False) -> Iterable[UploadResult]:
|
|
122
120
|
schema, to_export = self._prepare_schema_and_exporters(rules, client)
|
|
@@ -272,7 +270,7 @@ class DMSExporter(CDFExporter[DMSSchema]):
|
|
|
272
270
|
if self.export_components.intersection({"all", "views"}):
|
|
273
271
|
to_export.append((schema.views, ViewLoader(client, self.existing_handling)))
|
|
274
272
|
if self.export_components.intersection({"all", "data_models"}):
|
|
275
|
-
to_export.append((schema.
|
|
273
|
+
to_export.append((DataModelApplyList([schema.data_model]), DataModelLoader(client)))
|
|
276
274
|
if isinstance(schema, PipelineSchema):
|
|
277
275
|
to_export.append((schema.databases, RawDatabaseLoader(client)))
|
|
278
276
|
to_export.append((schema.raw_tables, RawTableLoader(client)))
|
|
@@ -13,8 +13,10 @@ from openpyxl.worksheet.worksheet import Worksheet
|
|
|
13
13
|
|
|
14
14
|
from cognite.neat.rules._shared import Rules
|
|
15
15
|
from cognite.neat.rules.models import (
|
|
16
|
+
DataModelType,
|
|
16
17
|
DMSRules,
|
|
17
18
|
DomainRules,
|
|
19
|
+
ExtensionCategory,
|
|
18
20
|
InformationRules,
|
|
19
21
|
RoleTypes,
|
|
20
22
|
SchemaCompleteness,
|
|
@@ -35,11 +37,18 @@ class ExcelExporter(BaseExporter[Workbook]):
|
|
|
35
37
|
new_model_id: The new model ID to use for the exported spreadsheet. This is only applicable if the input
|
|
36
38
|
rules have 'is_reference' set. If provided, the model ID will be used to automatically create the
|
|
37
39
|
new metadata sheet in the Excel file.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
dump_as: This determines how the rules are written to the Excel file. An Excel file has up to three sets of
|
|
41
|
+
sheets: user, last, and reference. The user sheets are used for inputting rules from a user. The last sheets
|
|
42
|
+
are used for the last version of the same model as the user, while the reference sheets are used for
|
|
43
|
+
the model the user is building on. The options are:
|
|
44
|
+
* "user": The rules are written to the user sheets. This is used when you want to modify the rules
|
|
45
|
+
directly and potentially change the model. This is useful when you have imported the data model
|
|
46
|
+
from outside CDF and you want to modify it before you write it to CDF.
|
|
47
|
+
* "last": The rules are written to the last sheets. This is used when you want to extend the rules,
|
|
48
|
+
but have validation that you are not breaking the existing model. This is used when you want to
|
|
49
|
+
change a model that has already been published to CDF and that model is in production.
|
|
50
|
+
* "reference": The rules are written to the reference sheets. This is typically used when you want to build
|
|
51
|
+
a new solution on top of an enterprise model.
|
|
43
52
|
|
|
44
53
|
The following styles are available:
|
|
45
54
|
|
|
@@ -51,7 +60,7 @@ class ExcelExporter(BaseExporter[Workbook]):
|
|
|
51
60
|
"""
|
|
52
61
|
|
|
53
62
|
Style = Literal["none", "minimal", "default", "maximal"]
|
|
54
|
-
|
|
63
|
+
DumpOptions = Literal["user", "last", "reference"]
|
|
55
64
|
_main_header_by_sheet_name: ClassVar[dict[str, str]] = {
|
|
56
65
|
"Properties": "Definition of Properties per Class",
|
|
57
66
|
"Classes": "Definition of Classes",
|
|
@@ -59,21 +68,24 @@ class ExcelExporter(BaseExporter[Workbook]):
|
|
|
59
68
|
"Containers": "Definition of Containers",
|
|
60
69
|
}
|
|
61
70
|
style_options = get_args(Style)
|
|
71
|
+
dump_options = get_args(DumpOptions)
|
|
62
72
|
|
|
63
73
|
def __init__(
|
|
64
74
|
self,
|
|
65
75
|
styling: Style = "default",
|
|
66
76
|
output_role: RoleTypes | None = None,
|
|
67
77
|
new_model_id: tuple[str, str, str] | None = None,
|
|
68
|
-
|
|
78
|
+
dump_as: DumpOptions = "user",
|
|
69
79
|
):
|
|
70
80
|
if styling not in self.style_options:
|
|
71
81
|
raise ValueError(f"Invalid styling: {styling}. Valid options are {self.style_options}")
|
|
82
|
+
if dump_as not in self.dump_options:
|
|
83
|
+
raise ValueError(f"Invalid dump_as: {dump_as}. Valid options are {self.dump_options}")
|
|
72
84
|
self.styling = styling
|
|
73
85
|
self._styling_level = self.style_options.index(styling)
|
|
74
86
|
self.output_role = output_role
|
|
75
87
|
self.new_model_id = new_model_id
|
|
76
|
-
self.
|
|
88
|
+
self.dump_as = dump_as
|
|
77
89
|
|
|
78
90
|
def export_to_file(self, rules: Rules, filepath: Path) -> None:
|
|
79
91
|
"""Exports transformation rules to excel file."""
|
|
@@ -90,41 +102,48 @@ class ExcelExporter(BaseExporter[Workbook]):
|
|
|
90
102
|
# Remove default sheet named "Sheet"
|
|
91
103
|
workbook.remove(workbook["Sheet"])
|
|
92
104
|
|
|
93
|
-
|
|
105
|
+
dumped_user_rules: dict[str, Any]
|
|
106
|
+
dumped_last_rules: dict[str, Any] | None = None
|
|
94
107
|
dumped_reference_rules: dict[str, Any] | None = None
|
|
95
|
-
if self.
|
|
108
|
+
if self.dump_as != "user":
|
|
96
109
|
# Writes empty reference sheets
|
|
97
|
-
|
|
110
|
+
dumped_user_rules = {
|
|
98
111
|
"Metadata": self._create_metadata_sheet_user_rules(rules),
|
|
99
112
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
113
|
+
|
|
114
|
+
if self.dump_as == "last":
|
|
115
|
+
dumped_last_rules = rules.model_dump(by_alias=True)
|
|
116
|
+
if rules.reference:
|
|
117
|
+
dumped_reference_rules = rules.reference.model_dump(by_alias=True)
|
|
118
|
+
elif self.dump_as == "reference":
|
|
119
|
+
dumped_reference_rules = rules.reference_self().model_dump(by_alias=True)
|
|
104
120
|
else:
|
|
105
|
-
|
|
121
|
+
dumped_user_rules = rules.model_dump(by_alias=True)
|
|
122
|
+
if rules.last:
|
|
123
|
+
dumped_last_rules = rules.last.model_dump(by_alias=True)
|
|
106
124
|
if rules.reference:
|
|
107
125
|
dumped_reference_rules = rules.reference.model_dump(by_alias=True)
|
|
108
126
|
|
|
109
|
-
self._write_metadata_sheet(workbook,
|
|
110
|
-
self._write_sheets(workbook,
|
|
127
|
+
self._write_metadata_sheet(workbook, dumped_user_rules["Metadata"])
|
|
128
|
+
self._write_sheets(workbook, dumped_user_rules, rules)
|
|
129
|
+
if dumped_last_rules:
|
|
130
|
+
self._write_sheets(workbook, dumped_last_rules, rules, sheet_prefix="Last")
|
|
131
|
+
|
|
111
132
|
if dumped_reference_rules:
|
|
112
|
-
|
|
113
|
-
self.
|
|
133
|
+
prefix = "Ref"
|
|
134
|
+
self._write_sheets(workbook, dumped_reference_rules, rules, sheet_prefix=prefix)
|
|
135
|
+
self._write_metadata_sheet(workbook, dumped_reference_rules["Metadata"], sheet_prefix=prefix)
|
|
114
136
|
|
|
115
137
|
if self._styling_level > 0:
|
|
116
138
|
self._adjust_column_widths(workbook)
|
|
117
139
|
|
|
118
140
|
return workbook
|
|
119
141
|
|
|
120
|
-
def _write_sheets(self, workbook: Workbook, dumped_rules: dict[str, Any], rules: Rules,
|
|
142
|
+
def _write_sheets(self, workbook: Workbook, dumped_rules: dict[str, Any], rules: Rules, sheet_prefix: str = ""):
|
|
121
143
|
for sheet_name, headers in rules.headers_by_sheet(by_alias=True).items():
|
|
122
144
|
if sheet_name in ("Metadata", "prefixes", "Reference", "Last"):
|
|
123
145
|
continue
|
|
124
|
-
|
|
125
|
-
sheet = workbook.create_sheet(f"Ref{sheet_name}")
|
|
126
|
-
else:
|
|
127
|
-
sheet = workbook.create_sheet(sheet_name)
|
|
146
|
+
sheet = workbook.create_sheet(f"{sheet_prefix}{sheet_name}")
|
|
128
147
|
|
|
129
148
|
main_header = self._main_header_by_sheet_name[sheet_name]
|
|
130
149
|
sheet.append([main_header] + [""] * (len(headers) - 1))
|
|
@@ -170,17 +189,14 @@ class ExcelExporter(BaseExporter[Workbook]):
|
|
|
170
189
|
for cell in sheet["2"]:
|
|
171
190
|
cell.font = Font(bold=True, size=14)
|
|
172
191
|
|
|
173
|
-
def _write_metadata_sheet(self, workbook: Workbook, metadata: dict[str, Any],
|
|
192
|
+
def _write_metadata_sheet(self, workbook: Workbook, metadata: dict[str, Any], sheet_prefix: str = "") -> None:
|
|
174
193
|
# Excel does not support timezone in datetime strings
|
|
175
194
|
if isinstance(metadata.get("created"), datetime):
|
|
176
195
|
metadata["created"] = metadata["created"].replace(tzinfo=None)
|
|
177
196
|
if isinstance(metadata.get("updated"), datetime):
|
|
178
197
|
metadata["updated"] = metadata["updated"].replace(tzinfo=None)
|
|
179
198
|
|
|
180
|
-
|
|
181
|
-
metadata_sheet = workbook.create_sheet("RefMetadata")
|
|
182
|
-
else:
|
|
183
|
-
metadata_sheet = workbook.create_sheet("Metadata")
|
|
199
|
+
metadata_sheet = workbook.create_sheet(f"{sheet_prefix}Metadata")
|
|
184
200
|
for key, value in metadata.items():
|
|
185
201
|
metadata_sheet.append([key, value])
|
|
186
202
|
|
|
@@ -241,25 +257,29 @@ class ExcelExporter(BaseExporter[Workbook]):
|
|
|
241
257
|
# Excel does not support timezone in datetime strings
|
|
242
258
|
now_iso = datetime.now().replace(tzinfo=None).isoformat()
|
|
243
259
|
is_info = isinstance(rules, InformationRules)
|
|
244
|
-
is_dms =
|
|
245
|
-
is_extension = self.new_model_id is not None
|
|
246
|
-
is_solution =
|
|
260
|
+
is_dms = isinstance(rules, DMSRules)
|
|
261
|
+
is_extension = self.new_model_id is not None or rules.reference is not None
|
|
262
|
+
is_solution = rules.metadata.data_model_type == DataModelType.solution
|
|
247
263
|
|
|
248
|
-
if is_solution:
|
|
264
|
+
if is_solution and self.new_model_id:
|
|
249
265
|
metadata["prefix" if is_info else "space"] = self.new_model_id[0] # type: ignore[index]
|
|
250
266
|
metadata["title" if is_info else "externalId"] = self.new_model_id[1] # type: ignore[index]
|
|
251
267
|
metadata["version"] = self.new_model_id[2] # type: ignore[index]
|
|
268
|
+
elif is_solution and self.dump_as == "reference" and rules.reference:
|
|
269
|
+
metadata["prefix" if is_info else "space"] = "YOUR_PREFIX"
|
|
270
|
+
metadata["title" if is_info else "externalId"] = "YOUR_TITLE"
|
|
271
|
+
metadata["version"] = "1"
|
|
252
272
|
else:
|
|
253
273
|
metadata["prefix" if is_info else "space"] = existing_model_id[0]
|
|
254
274
|
metadata["title" if is_info else "externalId"] = existing_model_id[1]
|
|
255
275
|
metadata["version"] = existing_model_id[2]
|
|
256
276
|
|
|
257
|
-
if is_solution and is_info:
|
|
277
|
+
if is_solution and is_info and self.new_model_id:
|
|
258
278
|
metadata["namespace"] = f"http://purl.org/{self.new_model_id[0]}/" # type: ignore[index]
|
|
259
279
|
elif is_info:
|
|
260
280
|
metadata["namespace"] = existing_metadata["namespace"]
|
|
261
281
|
|
|
262
|
-
if is_solution and is_dms:
|
|
282
|
+
if is_solution and is_dms and self.new_model_id:
|
|
263
283
|
metadata["name"] = self.new_model_id[1] # type: ignore[index]
|
|
264
284
|
|
|
265
285
|
if is_solution:
|
|
@@ -285,6 +305,11 @@ class ExcelExporter(BaseExporter[Workbook]):
|
|
|
285
305
|
else:
|
|
286
306
|
metadata["schema"] = SchemaCompleteness.complete.value
|
|
287
307
|
|
|
288
|
-
|
|
308
|
+
if is_solution:
|
|
309
|
+
metadata["dataModelType"] = DataModelType.solution.value
|
|
310
|
+
else:
|
|
311
|
+
metadata["dataModelType"] = DataModelType.enterprise.value
|
|
289
312
|
|
|
313
|
+
metadata["extension"] = ExtensionCategory.addition.value
|
|
314
|
+
metadata["role"] = (self.output_role and self.output_role.value) or rules.metadata.role.value
|
|
290
315
|
return metadata
|
|
@@ -6,7 +6,7 @@ from typing import Any, Literal, cast, overload
|
|
|
6
6
|
|
|
7
7
|
from cognite.client import CogniteClient
|
|
8
8
|
from cognite.client import data_modeling as dm
|
|
9
|
-
from cognite.client.data_classes.data_modeling import DataModelIdentifier
|
|
9
|
+
from cognite.client.data_classes.data_modeling import DataModelId, DataModelIdentifier
|
|
10
10
|
from cognite.client.data_classes.data_modeling.containers import BTreeIndex, InvertedIndex
|
|
11
11
|
from cognite.client.data_classes.data_modeling.views import (
|
|
12
12
|
MultiEdgeConnectionApply,
|
|
@@ -39,7 +39,6 @@ from cognite.neat.rules.models.dms import (
|
|
|
39
39
|
from cognite.neat.rules.models.entities import (
|
|
40
40
|
ClassEntity,
|
|
41
41
|
ContainerEntity,
|
|
42
|
-
DataModelEntity,
|
|
43
42
|
DMSUnknownEntity,
|
|
44
43
|
ViewEntity,
|
|
45
44
|
ViewPropertyEntity,
|
|
@@ -52,12 +51,14 @@ class DMSImporter(BaseImporter):
|
|
|
52
51
|
schema: DMSSchema,
|
|
53
52
|
read_issues: Sequence[ValidationIssue] | None = None,
|
|
54
53
|
metadata: DMSMetadata | None = None,
|
|
54
|
+
ref_metadata: DMSMetadata | None = None,
|
|
55
55
|
):
|
|
56
56
|
# Calling this root schema to distinguish it from
|
|
57
57
|
# * User Schema
|
|
58
58
|
# * Reference Schema
|
|
59
59
|
self.root_schema = schema
|
|
60
60
|
self.metadata = metadata
|
|
61
|
+
self.ref_metadata = ref_metadata
|
|
61
62
|
self.issue_list = IssueList(read_issues)
|
|
62
63
|
self._all_containers_by_id = {container.as_id(): container for container in schema.containers}
|
|
63
64
|
if self.root_schema.reference:
|
|
@@ -66,42 +67,78 @@ class DMSImporter(BaseImporter):
|
|
|
66
67
|
)
|
|
67
68
|
|
|
68
69
|
@classmethod
|
|
69
|
-
def from_data_model_id(
|
|
70
|
+
def from_data_model_id(
|
|
71
|
+
cls,
|
|
72
|
+
client: CogniteClient,
|
|
73
|
+
data_model_id: DataModelIdentifier,
|
|
74
|
+
reference_model_id: DataModelIdentifier | None = None,
|
|
75
|
+
) -> "DMSImporter":
|
|
70
76
|
"""Create a DMSImporter ready to convert the given data model to rules.
|
|
71
77
|
|
|
72
78
|
Args:
|
|
73
79
|
client: Instantiated CogniteClient to retrieve data model.
|
|
80
|
+
reference_model_id: The reference data model to retrieve. This is the data model that
|
|
81
|
+
the given data model is built on top of, typically, an enterprise data model.
|
|
74
82
|
data_model_id: Data Model to retrieve.
|
|
75
83
|
|
|
76
84
|
Returns:
|
|
77
85
|
DMSImporter: DMSImporter instance
|
|
78
86
|
"""
|
|
79
|
-
|
|
80
|
-
|
|
87
|
+
data_model_ids = [data_model_id, reference_model_id] if reference_model_id else [data_model_id]
|
|
88
|
+
data_models = client.data_modeling.data_models.retrieve(data_model_ids, inline_views=True)
|
|
89
|
+
|
|
90
|
+
user_models = cls._find_model_in_list(data_models, data_model_id)
|
|
91
|
+
if len(user_models) == 0:
|
|
81
92
|
return cls(DMSSchema(), [issues.importing.NoDataModelError(f"Data model {data_model_id} not found")])
|
|
82
|
-
|
|
93
|
+
user_model = user_models.latest_version()
|
|
94
|
+
|
|
95
|
+
if reference_model_id:
|
|
96
|
+
ref_models = cls._find_model_in_list(data_models, reference_model_id)
|
|
97
|
+
if len(ref_models) == 0:
|
|
98
|
+
return cls(
|
|
99
|
+
DMSSchema(), [issues.importing.NoDataModelError(f"Data model {reference_model_id} not found")]
|
|
100
|
+
)
|
|
101
|
+
ref_model: dm.DataModel[dm.View] | None = ref_models.latest_version()
|
|
102
|
+
else:
|
|
103
|
+
ref_model = None
|
|
83
104
|
|
|
84
105
|
try:
|
|
85
|
-
schema = DMSSchema.from_data_model(client,
|
|
106
|
+
schema = DMSSchema.from_data_model(client, user_model, ref_model)
|
|
86
107
|
except Exception as e:
|
|
87
108
|
return cls(DMSSchema(), [issues.importing.APIError(str(e))])
|
|
88
109
|
|
|
89
|
-
|
|
90
|
-
|
|
110
|
+
metadata = cls._create_metadata_from_model(user_model)
|
|
111
|
+
ref_metadata = cls._create_metadata_from_model(ref_model) if ref_model else None
|
|
91
112
|
|
|
92
|
-
|
|
113
|
+
return cls(schema, [], metadata, ref_metadata)
|
|
93
114
|
|
|
94
|
-
|
|
115
|
+
@classmethod
|
|
116
|
+
def _find_model_in_list(
|
|
117
|
+
cls, data_models: dm.DataModelList[dm.View], model_id: DataModelIdentifier
|
|
118
|
+
) -> dm.DataModelList[dm.View]:
|
|
119
|
+
identifier = DataModelId.load(model_id)
|
|
120
|
+
return dm.DataModelList[dm.View](
|
|
121
|
+
[
|
|
122
|
+
model
|
|
123
|
+
for model in data_models
|
|
124
|
+
if (model.space, model.external_id) == (identifier.space, identifier.external_id)
|
|
125
|
+
]
|
|
126
|
+
)
|
|
95
127
|
|
|
96
128
|
@classmethod
|
|
97
129
|
def _create_metadata_from_model(
|
|
98
130
|
cls,
|
|
99
131
|
model: dm.DataModel[dm.View] | dm.DataModelApply,
|
|
100
|
-
created: datetime | None = None,
|
|
101
|
-
updated: datetime | None = None,
|
|
102
132
|
) -> DMSMetadata:
|
|
103
133
|
description, creator = DMSMetadata._get_description_and_creator(model.description)
|
|
104
|
-
|
|
134
|
+
|
|
135
|
+
if isinstance(model, dm.DataModel):
|
|
136
|
+
created = ms_to_datetime(model.created_time)
|
|
137
|
+
updated = ms_to_datetime(model.last_updated_time)
|
|
138
|
+
else:
|
|
139
|
+
now = datetime.now().replace(microsecond=0)
|
|
140
|
+
created = now
|
|
141
|
+
updated = now
|
|
105
142
|
return DMSMetadata(
|
|
106
143
|
schema_=SchemaCompleteness.complete,
|
|
107
144
|
extension=ExtensionCategory.addition,
|
|
@@ -109,8 +146,8 @@ class DMSImporter(BaseImporter):
|
|
|
109
146
|
external_id=model.external_id,
|
|
110
147
|
name=model.name or model.external_id,
|
|
111
148
|
version=model.version or "0.1.0",
|
|
112
|
-
updated=updated
|
|
113
|
-
created=created
|
|
149
|
+
updated=updated,
|
|
150
|
+
created=created,
|
|
114
151
|
creator=creator,
|
|
115
152
|
description=description,
|
|
116
153
|
)
|
|
@@ -147,28 +184,33 @@ class DMSImporter(BaseImporter):
|
|
|
147
184
|
# In case there were errors during the import, the to_rules method will return None
|
|
148
185
|
return self._return_or_raise(self.issue_list, errors)
|
|
149
186
|
|
|
150
|
-
if
|
|
187
|
+
if not self.root_schema.data_model:
|
|
151
188
|
self.issue_list.append(issues.importing.NoDataModelError("No data model found."))
|
|
152
189
|
return self._return_or_raise(self.issue_list, errors)
|
|
153
|
-
|
|
190
|
+
model = self.root_schema.data_model
|
|
154
191
|
with _handle_issues(
|
|
155
192
|
self.issue_list,
|
|
156
193
|
) as future:
|
|
157
194
|
schema_completeness = SchemaCompleteness.complete
|
|
158
195
|
data_model_type = DataModelType.enterprise
|
|
159
196
|
reference: DMSRules | None = None
|
|
160
|
-
if ref_schema := self.root_schema.reference:
|
|
197
|
+
if (ref_schema := self.root_schema.reference) and (ref_model := ref_schema.data_model):
|
|
161
198
|
# Reference should always be an enterprise model.
|
|
162
199
|
reference = DMSRules(
|
|
163
200
|
**self._create_rule_components(
|
|
164
|
-
|
|
201
|
+
ref_model,
|
|
202
|
+
ref_schema,
|
|
203
|
+
self.ref_metadata or self._create_default_metadata(ref_schema.views),
|
|
204
|
+
DataModelType.enterprise,
|
|
165
205
|
)
|
|
166
206
|
)
|
|
167
207
|
schema_completeness = SchemaCompleteness.extended
|
|
168
208
|
data_model_type = DataModelType.solution
|
|
169
209
|
|
|
170
210
|
user_rules = DMSRules(
|
|
171
|
-
**self._create_rule_components(
|
|
211
|
+
**self._create_rule_components(
|
|
212
|
+
model, self.root_schema, self.metadata, data_model_type, schema_completeness
|
|
213
|
+
),
|
|
172
214
|
reference=reference,
|
|
173
215
|
)
|
|
174
216
|
|
|
@@ -179,21 +221,12 @@ class DMSImporter(BaseImporter):
|
|
|
179
221
|
|
|
180
222
|
def _create_rule_components(
|
|
181
223
|
self,
|
|
224
|
+
data_model: dm.DataModelApply,
|
|
182
225
|
schema: DMSSchema,
|
|
183
226
|
metadata: DMSMetadata | None = None,
|
|
184
227
|
data_model_type: DataModelType | None = None,
|
|
185
228
|
schema_completeness: SchemaCompleteness | None = None,
|
|
186
229
|
) -> dict[str, Any]:
|
|
187
|
-
if len(schema.data_models) > 2:
|
|
188
|
-
# Creating a DataModelEntity to convert the data model id to a string.
|
|
189
|
-
self.issue_list.append(
|
|
190
|
-
issues.importing.MultipleDataModelsWarning(
|
|
191
|
-
[str(DataModelEntity.from_id(model.as_id())) for model in schema.data_models]
|
|
192
|
-
)
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
data_model = schema.data_models[0]
|
|
196
|
-
|
|
197
230
|
properties = SheetList[DMSProperty]()
|
|
198
231
|
for view in schema.views:
|
|
199
232
|
view_id = view.as_id()
|
|
@@ -87,71 +87,95 @@ class MetadataRaw(UserDict):
|
|
|
87
87
|
class ReadResult:
|
|
88
88
|
sheets: dict[str, dict | list]
|
|
89
89
|
read_info_by_sheet: dict[str, SpreadsheetRead]
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
metadata: MetadataRaw
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def role(self) -> RoleTypes:
|
|
94
|
+
return self.metadata.role
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def schema(self) -> SchemaCompleteness | None:
|
|
98
|
+
return self.metadata.schema
|
|
92
99
|
|
|
93
100
|
|
|
94
101
|
class SpreadsheetReader:
|
|
95
|
-
def __init__(
|
|
102
|
+
def __init__(
|
|
103
|
+
self,
|
|
104
|
+
issue_list: IssueList,
|
|
105
|
+
required: bool = True,
|
|
106
|
+
metadata: MetadataRaw | None = None,
|
|
107
|
+
sheet_prefix: Literal["", "Last", "Ref"] = "",
|
|
108
|
+
):
|
|
96
109
|
self.issue_list = issue_list
|
|
97
|
-
self.
|
|
110
|
+
self.required = required
|
|
111
|
+
self.metadata = metadata
|
|
112
|
+
self._sheet_prefix = sheet_prefix
|
|
98
113
|
|
|
99
114
|
@property
|
|
100
115
|
def metadata_sheet_name(self) -> str:
|
|
101
|
-
|
|
102
|
-
return self.to_reference_sheet(metadata_name) if self._is_reference else metadata_name
|
|
116
|
+
return f"{self._sheet_prefix}Metadata"
|
|
103
117
|
|
|
104
118
|
def sheet_names(self, role: RoleTypes) -> set[str]:
|
|
105
119
|
names = MANDATORY_SHEETS_BY_ROLE[role]
|
|
106
|
-
return {self.
|
|
107
|
-
|
|
108
|
-
@classmethod
|
|
109
|
-
def to_reference_sheet(cls, sheet_name: str) -> str:
|
|
110
|
-
return f"Ref{sheet_name}"
|
|
120
|
+
return {f"{self._sheet_prefix}{sheet_name}" for sheet_name in names if sheet_name != "Metadata"}
|
|
111
121
|
|
|
112
122
|
def read(self, filepath: Path) -> None | ReadResult:
|
|
113
123
|
with pd.ExcelFile(filepath) as excel_file:
|
|
114
|
-
|
|
124
|
+
metadata: MetadataRaw | None
|
|
125
|
+
if self.metadata is not None:
|
|
126
|
+
metadata = self.metadata
|
|
127
|
+
else:
|
|
128
|
+
metadata = self._read_metadata(excel_file, filepath)
|
|
129
|
+
if metadata is None:
|
|
130
|
+
# The reading of metadata failed, so we can't continue
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
sheets, read_info_by_sheet = self._read_sheets(excel_file, metadata.role)
|
|
134
|
+
if sheets is None or self.issue_list.has_errors:
|
|
135
|
+
return None
|
|
136
|
+
sheets["Metadata"] = dict(metadata)
|
|
137
|
+
|
|
138
|
+
return ReadResult(sheets, read_info_by_sheet, metadata)
|
|
139
|
+
|
|
140
|
+
def _read_metadata(self, excel_file: ExcelFile, filepath: Path) -> MetadataRaw | None:
|
|
141
|
+
if self.metadata_sheet_name not in excel_file.sheet_names:
|
|
142
|
+
if self.required:
|
|
115
143
|
self.issue_list.append(
|
|
116
144
|
issues.spreadsheet_file.MetadataSheetMissingOrFailedError(
|
|
117
145
|
filepath, sheet_name=self.metadata_sheet_name
|
|
118
146
|
)
|
|
119
147
|
)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
metadata = MetadataRaw.from_excel(excel_file, self.metadata_sheet_name)
|
|
148
|
+
return None
|
|
123
149
|
|
|
124
|
-
|
|
125
|
-
return None
|
|
150
|
+
metadata = MetadataRaw.from_excel(excel_file, self.metadata_sheet_name)
|
|
126
151
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return ReadResult(sheets, read_info_by_sheet, metadata.role, metadata.schema)
|
|
152
|
+
if not metadata.is_valid(self.issue_list, filepath):
|
|
153
|
+
return None
|
|
154
|
+
return metadata
|
|
132
155
|
|
|
133
156
|
def _read_sheets(
|
|
134
|
-
self,
|
|
157
|
+
self, excel_file: ExcelFile, read_role: RoleTypes
|
|
135
158
|
) -> tuple[dict[str, dict | list] | None, dict[str, SpreadsheetRead]]:
|
|
136
159
|
read_info_by_sheet: dict[str, SpreadsheetRead] = defaultdict(SpreadsheetRead)
|
|
137
160
|
|
|
138
|
-
sheets: dict[str, dict | list] = {
|
|
161
|
+
sheets: dict[str, dict | list] = {}
|
|
139
162
|
|
|
140
|
-
expected_sheet_names = self.sheet_names(
|
|
163
|
+
expected_sheet_names = self.sheet_names(read_role)
|
|
141
164
|
|
|
142
165
|
if missing_sheets := expected_sheet_names.difference(set(excel_file.sheet_names)):
|
|
143
|
-
self.
|
|
144
|
-
|
|
145
|
-
|
|
166
|
+
if self.required:
|
|
167
|
+
self.issue_list.append(
|
|
168
|
+
issues.spreadsheet_file.SheetMissingError(cast(Path, excel_file.io), list(missing_sheets))
|
|
169
|
+
)
|
|
146
170
|
return None, read_info_by_sheet
|
|
147
171
|
|
|
148
172
|
for source_sheet_name, target_sheet_name, headers_input in SOURCE_SHEET__TARGET_FIELD__HEADERS:
|
|
149
|
-
source_sheet_name = self.
|
|
173
|
+
source_sheet_name = f"{self._sheet_prefix}{source_sheet_name}"
|
|
150
174
|
|
|
151
175
|
if source_sheet_name not in excel_file.sheet_names:
|
|
152
176
|
continue
|
|
153
177
|
if isinstance(headers_input, dict):
|
|
154
|
-
headers = headers_input[
|
|
178
|
+
headers = headers_input[read_role]
|
|
155
179
|
else:
|
|
156
180
|
headers = headers_input
|
|
157
181
|
|
|
@@ -188,42 +212,37 @@ class ExcelImporter(BaseImporter):
|
|
|
188
212
|
issue_list.append(issues.spreadsheet_file.SpreadsheetNotFoundError(self.filepath))
|
|
189
213
|
return self._return_or_raise(issue_list, errors)
|
|
190
214
|
|
|
191
|
-
|
|
192
|
-
if
|
|
215
|
+
user_read = SpreadsheetReader(issue_list).read(self.filepath)
|
|
216
|
+
if user_read is None or issue_list.has_errors:
|
|
193
217
|
return self._return_or_raise(issue_list, errors)
|
|
194
218
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
219
|
+
last_read: ReadResult | None = None
|
|
220
|
+
reference_read: ReadResult | None = None
|
|
221
|
+
if user_read.schema == SchemaCompleteness.extended:
|
|
222
|
+
# Last does not have its own metadata sheet. It is the same as the user's metadata sheet.
|
|
223
|
+
last_read = SpreadsheetReader(
|
|
224
|
+
issue_list, required=False, metadata=user_read.metadata, sheet_prefix="Last"
|
|
225
|
+
).read(self.filepath)
|
|
226
|
+
reference_read = SpreadsheetReader(issue_list, sheet_prefix="Ref").read(self.filepath)
|
|
202
227
|
if issue_list.has_errors:
|
|
203
228
|
return self._return_or_raise(issue_list, errors)
|
|
204
229
|
|
|
205
|
-
if
|
|
230
|
+
if reference_read and user_read.role != reference_read.role:
|
|
206
231
|
issue_list.append(issues.spreadsheet_file.RoleMismatchError(self.filepath))
|
|
207
232
|
return self._return_or_raise(issue_list, errors)
|
|
208
233
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
read_info_by_sheet.update(
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
original_role = reference_result.role
|
|
222
|
-
read_info_by_sheet = reference_result.read_info_by_sheet
|
|
223
|
-
else:
|
|
224
|
-
raise ValueError(
|
|
225
|
-
"No rules were generated. This should have been caught earlier. " f"Bug in {type(self).__name__}."
|
|
226
|
-
)
|
|
234
|
+
sheets = user_read.sheets
|
|
235
|
+
original_role = user_read.role
|
|
236
|
+
read_info_by_sheet = user_read.read_info_by_sheet
|
|
237
|
+
if last_read:
|
|
238
|
+
sheets["last"] = last_read.sheets
|
|
239
|
+
read_info_by_sheet.update(last_read.read_info_by_sheet)
|
|
240
|
+
if reference_read:
|
|
241
|
+
# The last rules will also be validated against the reference rules
|
|
242
|
+
sheets["last"]["reference"] = reference_read.sheets # type: ignore[call-overload]
|
|
243
|
+
if reference_read:
|
|
244
|
+
sheets["reference"] = reference_read.sheets
|
|
245
|
+
read_info_by_sheet.update(reference_read.read_info_by_sheet)
|
|
227
246
|
|
|
228
247
|
rules_cls = RULES_PER_ROLE[original_role]
|
|
229
248
|
with _handle_issues(
|