cognite-neat 0.99.0__py3-none-any.whl → 0.99.1__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.

Files changed (62) hide show
  1. cognite/neat/_client/_api/data_modeling_loaders.py +77 -4
  2. cognite/neat/_client/_api/schema.py +63 -2
  3. cognite/neat/_client/data_classes/schema.py +2 -348
  4. cognite/neat/_constants.py +27 -4
  5. cognite/neat/_graph/extractors/_classic_cdf/_classic.py +5 -5
  6. cognite/neat/_graph/loaders/_rdf2dms.py +2 -2
  7. cognite/neat/_graph/transformers/_classic_cdf.py +24 -13
  8. cognite/neat/_issues/_base.py +26 -17
  9. cognite/neat/_issues/errors/__init__.py +4 -2
  10. cognite/neat/_issues/errors/_external.py +7 -0
  11. cognite/neat/_issues/errors/_properties.py +2 -7
  12. cognite/neat/_issues/errors/_resources.py +1 -1
  13. cognite/neat/_issues/warnings/__init__.py +4 -2
  14. cognite/neat/_issues/warnings/_external.py +9 -1
  15. cognite/neat/_issues/warnings/_resources.py +26 -2
  16. cognite/neat/_issues/warnings/user_modeling.py +4 -4
  17. cognite/neat/_rules/_constants.py +2 -6
  18. cognite/neat/_rules/exporters/_rules2dms.py +4 -6
  19. cognite/neat/_rules/importers/__init__.py +1 -3
  20. cognite/neat/_rules/importers/_base.py +1 -1
  21. cognite/neat/_rules/importers/_dms2rules.py +3 -25
  22. cognite/neat/_rules/importers/_rdf/__init__.py +5 -0
  23. cognite/neat/_rules/importers/_rdf/_base.py +34 -11
  24. cognite/neat/_rules/importers/_rdf/_imf2rules.py +91 -0
  25. cognite/neat/_rules/importers/_rdf/_inference2rules.py +18 -2
  26. cognite/neat/_rules/importers/_rdf/_owl2rules.py +80 -0
  27. cognite/neat/_rules/importers/_rdf/_shared.py +138 -441
  28. cognite/neat/_rules/models/dms/__init__.py +2 -0
  29. cognite/neat/_rules/models/dms/_exporter.py +32 -30
  30. cognite/neat/_rules/models/dms/_rules.py +3 -45
  31. cognite/neat/_rules/models/dms/_validation.py +389 -122
  32. cognite/neat/_rules/models/information/__init__.py +2 -0
  33. cognite/neat/_rules/models/information/_rules.py +0 -59
  34. cognite/neat/_rules/models/information/_validation.py +9 -9
  35. cognite/neat/_rules/models/mapping/_classic2core.py +1 -1
  36. cognite/neat/_rules/models/mapping/_classic2core.yaml +8 -4
  37. cognite/neat/_rules/transformers/_pipelines.py +1 -1
  38. cognite/neat/_rules/transformers/_verification.py +29 -4
  39. cognite/neat/_session/_base.py +16 -41
  40. cognite/neat/_session/_prepare.py +6 -5
  41. cognite/neat/_session/_to.py +5 -2
  42. cognite/neat/_session/exceptions.py +4 -0
  43. cognite/neat/_utils/rdf_.py +6 -4
  44. cognite/neat/_version.py +1 -1
  45. cognite/neat/_workflows/steps/lib/current/rules_exporter.py +0 -88
  46. cognite/neat/_workflows/steps/lib/current/rules_importer.py +2 -16
  47. cognite/neat/_workflows/steps/lib/current/rules_validator.py +3 -5
  48. {cognite_neat-0.99.0.dist-info → cognite_neat-0.99.1.dist-info}/METADATA +1 -1
  49. {cognite_neat-0.99.0.dist-info → cognite_neat-0.99.1.dist-info}/RECORD +52 -60
  50. cognite/neat/_rules/importers/_rdf/_imf2rules/__init__.py +0 -3
  51. cognite/neat/_rules/importers/_rdf/_imf2rules/_imf2classes.py +0 -86
  52. cognite/neat/_rules/importers/_rdf/_imf2rules/_imf2metadata.py +0 -29
  53. cognite/neat/_rules/importers/_rdf/_imf2rules/_imf2properties.py +0 -130
  54. cognite/neat/_rules/importers/_rdf/_imf2rules/_imf2rules.py +0 -154
  55. cognite/neat/_rules/importers/_rdf/_owl2rules/__init__.py +0 -3
  56. cognite/neat/_rules/importers/_rdf/_owl2rules/_owl2classes.py +0 -58
  57. cognite/neat/_rules/importers/_rdf/_owl2rules/_owl2metadata.py +0 -65
  58. cognite/neat/_rules/importers/_rdf/_owl2rules/_owl2properties.py +0 -59
  59. cognite/neat/_rules/importers/_rdf/_owl2rules/_owl2rules.py +0 -39
  60. {cognite_neat-0.99.0.dist-info → cognite_neat-0.99.1.dist-info}/LICENSE +0 -0
  61. {cognite_neat-0.99.0.dist-info → cognite_neat-0.99.1.dist-info}/WHEEL +0 -0
  62. {cognite_neat-0.99.0.dist-info → cognite_neat-0.99.1.dist-info}/entry_points.txt +0 -0
@@ -1,471 +1,168 @@
1
- import datetime
1
+ from typing import cast
2
2
 
3
- import numpy as np
4
- import pandas as pd
5
- from rdflib import Literal
3
+ from rdflib import BNode, Graph
4
+ from rdflib.plugins.sparql import prepareQuery
5
+ from rdflib.query import ResultRow
6
6
 
7
- from cognite.neat._rules._constants import PATTERNS
8
- from cognite.neat._rules.models.data_types import _XSD_TYPES
9
- from cognite.neat._utils.rdf_ import remove_namespace_from_uri
7
+ from cognite.neat._issues._base import IssueList
8
+ from cognite.neat._issues.errors._general import NeatValueError
9
+ from cognite.neat._issues.warnings._resources import (
10
+ ResourceRedefinedWarning,
11
+ ResourceRetrievalWarning,
12
+ )
13
+ from cognite.neat._utils.rdf_ import convert_rdflib_content
10
14
 
11
15
 
12
- def parse_raw_classes_dataframe(query_results: list[tuple]) -> pd.DataFrame:
13
- df = pd.DataFrame(
14
- query_results,
15
- columns=[
16
- "Class",
17
- "Name",
18
- "Description",
19
- "Implements",
20
- ],
21
- )
22
-
23
- if df.empty:
24
- return df
25
-
26
- # # remove NaNs
27
- df.replace(np.nan, "", regex=True, inplace=True)
28
-
29
- df.Class = df.Class.apply(lambda x: remove_namespace_from_uri(x))
30
- df["Comment"] = len(df) * [None]
31
- df["Implements"] = df["Implements"].apply(lambda x: remove_namespace_from_uri(x))
32
-
33
- return df
34
-
35
-
36
- def clean_up_classes(df: pd.DataFrame) -> pd.DataFrame:
37
- clean_list = [
38
- {
39
- "Class": class_,
40
- "Name": group_df["Name"].unique()[0],
41
- "Description": "\n".join(list(group_df.Description.unique())),
42
- "Implements": ", ".join(list(group_df["Implements"].unique())),
43
- }
44
- for class_, group_df in df.groupby("Class")
45
- ]
46
-
47
- df = pd.DataFrame(clean_list)
48
-
49
- # bring NaNs back
50
- df.replace("", None, inplace=True)
51
-
52
- # split Implements column back into list
53
- df["Implements"] = df["Implements"].apply(lambda x: x.split(", ") if isinstance(x, str) else None)
54
-
55
- return df
56
-
57
-
58
- def make_classes_compliant(classes: pd.DataFrame, importer: str = "RDF-based") -> pd.DataFrame:
59
- """Make classes compliant.
60
-
61
- Returns:
62
- Dataframe containing compliant classes
63
-
64
- !!! note "About the compliant classes"
65
- The compliant classes are based on the OWL base ontology, but adapted to NEAT and use in CDF.
66
- One thing to note is that this method would not be able to fix issues with class ids which
67
- are not compliant with the CDF naming convention. For example, if a class id contains a space,
68
- starts with a number, etc. This will cause issues when trying to create the class in CDF.
69
- """
70
-
71
- # Add _object_property_class, _data_type_property_class, _thing_class to the dataframe
72
- classes = pd.concat(
73
- [
74
- classes,
75
- pd.DataFrame([object_property_class(), data_type_property_class(), thing_class()]),
76
- ],
77
- ignore_index=True,
78
- )
79
-
80
- # Reduce length of elements in the "Description" column to 1024 characters
81
- classes["Description"] = classes["Description"].apply(lambda x: x[:1024] if isinstance(x, str) else None)
82
-
83
- # Add missing parent classes to the dataframe
84
- classes = pd.concat(
85
- [classes, pd.DataFrame(add_parent_class(classes))],
86
- ignore_index=True,
87
- )
88
-
89
- return classes
90
-
91
-
92
- def object_property_class() -> dict:
93
- return {
94
- "Class": "ObjectProperty",
95
- "Name": None,
96
- "Description": "The class of object properties.",
97
- "Implements": None,
98
- }
99
-
100
-
101
- def data_type_property_class() -> dict:
102
- return {
103
- "Class": "DatatypeProperty",
104
- "Name": None,
105
- "Description": "The class of data properties.",
106
- "Implements": None,
107
- }
108
-
109
-
110
- def thing_class() -> dict:
111
- return {
112
- "Class": "Thing",
113
- "Name": None,
114
- "Description": "The class of holding class individuals.",
115
- "Implements": None,
116
- }
117
-
118
-
119
- def add_parent_class(df: pd.DataFrame) -> list[dict]:
120
- parent_set = {
121
- item for sublist in df["Implements"].tolist() if sublist for item in sublist if item != "" and item is not None
122
- }
123
- class_set = set(df["Class"].tolist())
124
-
125
- rows = []
126
- for missing_parent_class in parent_set.difference(class_set):
127
- rows += [
128
- {
129
- "Class": missing_parent_class,
130
- "Name": None,
131
- "Description": None,
132
- "Implements": None,
133
- }
134
- ]
135
-
136
- return rows
137
-
138
-
139
- def parse_raw_properties_dataframe(query_results: list[tuple]) -> pd.DataFrame:
140
- df = pd.DataFrame(
141
- query_results,
142
- columns=[
143
- "Class",
144
- "Property",
145
- "Name",
146
- "Description",
147
- "Value Type",
148
- "Min Count",
149
- "Max Count",
150
- "Default",
151
- "_property_type",
152
- ],
153
- )
154
- if df.empty:
155
- return df
156
-
157
- df.replace(np.nan, "", regex=True, inplace=True)
158
-
159
- df.Class = df.Class.apply(lambda x: remove_namespace_from_uri(x))
160
- df.Property = df.Property.apply(lambda x: remove_namespace_from_uri(x))
161
- df["Value Type"] = df["Value Type"].apply(lambda x: remove_namespace_from_uri(x))
162
- df["_property_type"] = df["_property_type"].apply(lambda x: remove_namespace_from_uri(x))
163
-
164
- return df
165
-
166
-
167
- def clean_up_properties(df: pd.DataFrame) -> pd.DataFrame:
168
- class_grouped_dfs = df.groupby("Class")
169
-
170
- clean_list = []
171
-
172
- for class_, class_grouped_df in class_grouped_dfs:
173
- property_grouped_dfs = class_grouped_df.groupby("Property")
174
- for property_, property_grouped_df in property_grouped_dfs:
175
- clean_list += [
176
- {
177
- "Class": class_,
178
- "Property": property_,
179
- "Name": property_grouped_df["Name"].unique()[0],
180
- "Description": "\n".join(list(property_grouped_df["Description"].unique()))[:1024],
181
- "Value Type": property_grouped_df["Value Type"].unique()[0],
182
- "Min Count": property_grouped_df["Min Count"].unique()[0],
183
- "Max Count": property_grouped_df["Max Count"].unique()[0],
184
- "Default": property_grouped_df["Default"].unique()[0],
185
- "_property_type": property_grouped_df["_property_type"].unique()[0],
186
- }
187
- ]
188
-
189
- df = pd.DataFrame(clean_list)
190
- df.replace("", None, inplace=True)
191
-
192
- return df
193
-
194
-
195
- def make_properties_compliant(properties: pd.DataFrame, importer: str = "RDF-based") -> pd.DataFrame:
196
- # default to 0 if "Min Count" is not specified
197
- properties["Min Count"] = properties["Min Count"].apply(lambda x: 0 if not isinstance(x, Literal) or x == "" else x)
198
-
199
- # default to 1 if "Max Count" is not specified
200
- properties["Max Count"] = properties["Max Count"].apply(lambda x: 1 if not isinstance(x, Literal) or x == "" else x)
201
-
202
- # Reduce length of elements in the "Description" column to 1024 characters
203
- properties["Description"] = properties["Description"].apply(lambda x: x[:1024] if isinstance(x, str) else None)
204
-
205
- # fixes and additions
206
- properties = fix_dangling_properties(properties)
207
- properties = fix_missing_property_value_type(properties)
208
-
209
- return properties
210
-
211
-
212
- def fix_dangling_properties(properties: pd.DataFrame) -> pd.DataFrame:
213
- """This method fixes properties which are missing a domain definition in the ontology.
214
-
215
- Args:
216
- properties: Dataframe containing properties
217
-
218
- Returns:
219
- Dataframe containing properties with fixed domain
220
- """
221
- domain = {
222
- "ObjectProperty": object_property_class()["Class"],
223
- "DatatypeProperty": data_type_property_class()["Class"],
224
- }
225
-
226
- # apply missing range
227
- properties["Class"] = properties.apply(
228
- lambda row: (
229
- domain[row._property_type]
230
- if row._property_type == "ObjectProperty" and pd.isna(row["Class"])
231
- else domain["DatatypeProperty"]
232
- if pd.isna(row["Class"])
233
- else row["Class"]
234
- ),
235
- axis=1,
236
- )
237
- return properties
238
-
239
-
240
- def fix_missing_property_value_type(properties: pd.DataFrame) -> pd.DataFrame:
241
- """This method fixes properties which are missing a range definition in the ontology.
16
+ def parse_classes(graph: Graph, query: str, language: str, issue_list: IssueList) -> tuple[dict, IssueList]:
17
+ """Parse classes from graph
242
18
 
243
19
  Args:
244
- properties: Dataframe containing properties
20
+ graph: Graph containing classes definitions
21
+ language: Language to use for parsing, by default "en"
245
22
 
246
23
  Returns:
247
- Dataframe containing properties with fixed range
24
+ Dataframe containing owl classes
248
25
  """
249
- # apply missing range
250
- properties["Value Type"] = properties.apply(
251
- lambda row: (
252
- thing_class()["Class"]
253
- if row._property_type == "ObjectProperty" and pd.isna(row["Value Type"])
254
- else "string"
255
- if pd.isna(row["Value Type"])
256
- else row["Value Type"]
257
- ),
258
- axis=1,
259
- )
260
26
 
261
- return properties
27
+ classes: dict[str, dict] = {}
262
28
 
29
+ query = prepareQuery(query.format(language=language), initNs={k: v for k, v in graph.namespaces()})
30
+ expected_keys = [str(v) for v in query.algebra._vars]
263
31
 
264
- def make_metadata_compliant(metadata: dict) -> dict:
265
- """Attempts to fix errors in metadata, otherwise defaults to values that will pass validation.
32
+ for raw in graph.query(query):
33
+ res: dict = convert_rdflib_content(cast(ResultRow, raw).asdict(), True)
34
+ res = {key: res.get(key, None) for key in expected_keys}
266
35
 
267
- Args:
268
- metadata: Dictionary containing metadata
36
+ class_id = res["class_"]
269
37
 
270
- Returns:
271
- Dictionary containing metadata with fixed errors
272
- """
38
+ # Safeguarding against incomplete semantic definitions
39
+ if res["implements"] and isinstance(res["implements"], BNode):
40
+ issue_list.append(
41
+ ResourceRetrievalWarning(
42
+ class_id,
43
+ "implements",
44
+ error=("Unable to determine class that is being implemented"),
45
+ )
46
+ )
47
+ continue
273
48
 
274
- metadata = fix_space(metadata)
275
- metadata = fix_version(metadata)
276
- metadata = fix_date(
277
- metadata,
278
- date_type="created",
279
- default=datetime.datetime.now().replace(microsecond=0),
280
- )
281
- metadata = fix_date(
282
- metadata,
283
- date_type="updated",
284
- default=datetime.datetime.now().replace(microsecond=0),
285
- )
286
- metadata = fix_name(metadata)
287
- metadata = fix_description(metadata)
288
- metadata = fix_author(metadata, "creator")
289
-
290
- return metadata
291
-
292
-
293
- def fix_author(metadata: dict, author_type: str = "creator", default: str = "NEAT") -> dict:
294
- if author := metadata.get(author_type, None):
295
- if not isinstance(author, str) or isinstance(author, list):
296
- metadata[author_type] = default
297
- elif isinstance(author, str) and len(author) == 0:
298
- metadata[author_type] = default
299
- else:
300
- metadata[author_type] = default
301
- return metadata
302
-
303
-
304
- def fix_description(metadata: dict, default: str = "This model has been inferred from OWL ontology") -> dict:
305
- if description := metadata.get("description", None):
306
- if not isinstance(description, str) or len(description) == 0:
307
- metadata["description"] = default
308
- elif isinstance(description, str) and len(description) > 1024:
309
- metadata["description"] = metadata["description"][:1024]
310
- else:
311
- metadata["description"] = default
312
- return metadata
313
-
314
-
315
- def fix_space(metadata: dict, default: str = "neat") -> dict:
316
- if space := metadata.get("space", None):
317
- if not isinstance(space, str) or not PATTERNS.space_compliance.match(space):
318
- metadata["space"] = default
319
- else:
320
- metadata["space"] = default
321
- return metadata
322
-
323
-
324
- def fix_date(
325
- metadata: dict,
326
- date_type: str,
327
- default: datetime.datetime,
328
- ) -> dict:
329
- if date := metadata.get(date_type, None):
330
- try:
331
- if isinstance(date, datetime.datetime):
332
- return metadata
333
- elif isinstance(date, datetime.date):
334
- metadata[date_type] = datetime.datetime.combine(metadata[date_type], datetime.datetime.min.time())
335
- elif isinstance(date, str):
336
- metadata[date_type] = datetime.datetime.strptime(metadata[date_type], "%Y-%m-%dT%H:%M:%SZ")
337
- else:
338
- metadata[date_type] = default
339
- except Exception:
340
- metadata[date_type] = default
341
- else:
342
- metadata[date_type] = default
343
-
344
- return metadata
345
-
346
-
347
- def fix_version(metadata: dict, default: str = "1.0.0") -> dict:
348
- if version := metadata.get("version", None):
349
- if not PATTERNS.version_compliance.match(version):
350
- metadata["version"] = default
351
- else:
352
- metadata["version"] = default
353
-
354
- return metadata
355
-
356
-
357
- def fix_name(metadata: dict, default: str = "OWL Inferred Data Model") -> dict:
358
- if name := metadata.get("name", None):
359
- if not isinstance(name, str):
360
- metadata["title"] = default
361
- elif isinstance(name, str) and len(name) == 0:
362
- metadata["title"] = default
363
- elif isinstance(name, str) and len(name) > 255:
364
- metadata["title"] = metadata["title"][:255]
49
+ if class_id not in classes:
50
+ classes[class_id] = res
365
51
  else:
366
- pass
367
- else:
368
- metadata["name"] = default
52
+ # Handling implements
53
+ if classes[class_id]["implements"] and isinstance(classes[class_id]["implements"], list):
54
+ if res["implements"] not in classes[class_id]["implements"]:
55
+ classes[class_id]["implements"].append(res["implements"])
369
56
 
370
- return metadata
57
+ elif classes[class_id]["implements"] and isinstance(classes[class_id]["implements"], str):
58
+ classes[class_id]["implements"] = [classes[class_id]["implements"]]
371
59
 
60
+ if res["implements"] not in classes[class_id]["implements"]:
61
+ classes[class_id]["implements"].append(res["implements"])
62
+ elif res["implements"]:
63
+ classes[class_id]["implements"] = [res["implements"]]
372
64
 
373
- def make_components_compliant(components: dict) -> dict:
374
- components = add_missing_classes(components)
375
- components = add_missing_value_types(components)
376
- components = add_default_property_to_dangling_classes(components)
65
+ handle_meta("class_", classes, class_id, res, "name", issue_list)
66
+ handle_meta("class_", classes, class_id, res, "description", issue_list)
377
67
 
378
- return components
68
+ if not classes:
69
+ issue_list.append(NeatValueError("Unable to parse classes"))
379
70
 
71
+ return classes, issue_list
380
72
 
381
- def add_missing_classes(components: dict[str, list[dict]]) -> dict:
382
- """Add missing classes to Classes.
383
73
 
384
- Args:
385
- tables: imported tables from owl ontology
386
-
387
- Returns:
388
- Updated tables with missing classes added to containers
389
- """
390
-
391
- missing_classes = {definition["Class"] for definition in components["Properties"]} - {
392
- definition["Class"] for definition in components["Classes"]
393
- }
394
-
395
- comment = (
396
- "Added by NEAT. "
397
- "This is a class that a domain of a property but was not defined in the ontology. "
398
- "It is added by NEAT to make the ontology compliant with CDF."
399
- )
400
-
401
- for class_ in missing_classes:
402
- components["Classes"].append({"Class": class_, "Comment": comment})
403
-
404
- return components
405
-
406
-
407
- def add_missing_value_types(components: dict) -> dict:
408
- """Add properties to classes that do not have any properties defined to them
74
+ def parse_properties(graph: Graph, query: str, language: str, issue_list: IssueList) -> tuple[dict, IssueList]:
75
+ """Parse properties from graph
409
76
 
410
77
  Args:
411
- tables: imported tables from owl ontology
78
+ graph: Graph containing owl classes
79
+ language: Language to use for parsing, by default "en"
412
80
 
413
81
  Returns:
414
- Updated tables with missing properties added to containers
82
+ Dataframe containing owl classes
415
83
  """
416
84
 
417
- xsd_types = _XSD_TYPES
418
- candidate_value_types = {definition["Value Type"] for definition in components["Properties"]} - {
419
- definition["Class"] for definition in components["Classes"]
420
- }
421
-
422
- # to avoid issue of case sensitivity for xsd types
423
- value_types_lower = {v.lower() for v in candidate_value_types}
424
-
425
- xsd_types_lower = {x.lower() for x in xsd_types}
426
-
427
- # Create a mapping from lowercase strings to original strings
428
- value_types_mapping = {v.lower(): v for v in candidate_value_types}
429
-
430
- # Find the difference
431
- difference = value_types_lower - xsd_types_lower
432
-
433
- # Convert the difference back to the original case
434
- difference_original_case = {value_types_mapping[d] for d in difference}
435
-
436
- for class_ in difference_original_case:
437
- components["Classes"].append(
438
- {
439
- "Class": class_,
440
- }
441
- )
442
-
443
- return components
444
-
445
-
446
- def add_default_property_to_dangling_classes(components: dict[str, list[dict]]) -> dict:
447
- """Add missing classes to Classes.
448
-
449
- Args:
450
- tables: imported tables from owl ontology
451
-
452
- Returns:
453
- Updated tables with missing classes added to containers
454
- """
455
-
456
- dangling_classes = {
457
- definition["Class"] for definition in components["Classes"] if not definition.get("Implements", None)
458
- } - {definition["Class"] for definition in components["Properties"]}
459
-
460
- for class_ in dangling_classes:
461
- components["Properties"].append(
462
- {
463
- "Class": class_,
464
- "Property": "label",
465
- "Value Type": "string",
466
- "Min Count": 0,
467
- "Max Count": 1,
468
- }
85
+ properties: dict[str, dict] = {}
86
+
87
+ query = prepareQuery(query.format(language=language), initNs={k: v for k, v in graph.namespaces()})
88
+ expected_keys = [str(v) for v in query.algebra._vars]
89
+
90
+ for raw in graph.query(query):
91
+ res: dict = convert_rdflib_content(cast(ResultRow, raw).asdict(), True)
92
+ res = {key: res.get(key, None) for key in expected_keys}
93
+
94
+ property_id = res["property_"]
95
+
96
+ # Safeguarding against incomplete semantic definitions
97
+ if not res["class_"] or isinstance(res["class_"], BNode):
98
+ issue_list.append(
99
+ ResourceRetrievalWarning(
100
+ property_id,
101
+ "property",
102
+ error=("Unable to determine to what class property is being defined"),
103
+ )
104
+ )
105
+ continue
106
+
107
+ # Safeguarding against incomplete semantic definitions
108
+ if not res["value_type"] or isinstance(res["value_type"], BNode):
109
+ issue_list.append(
110
+ ResourceRetrievalWarning(
111
+ property_id,
112
+ "property",
113
+ error=("Unable to determine value type of property"),
114
+ )
115
+ )
116
+ continue
117
+
118
+ id_ = f"{res['class_']}.{res['property_']}"
119
+
120
+ if id_ not in properties:
121
+ properties[id_] = res
122
+ properties[id_]["value_type"] = [properties[id_]["value_type"]]
123
+ else:
124
+ handle_meta("property", properties, id_, res, "name", issue_list)
125
+ handle_meta(
126
+ "property",
127
+ properties,
128
+ id_,
129
+ res,
130
+ "description",
131
+ issue_list,
132
+ )
133
+
134
+ # Handling multi-value types
135
+ if res["value_type"] not in properties[id_]["value_type"]:
136
+ properties[id_]["value_type"].append(res["value_type"])
137
+
138
+ for prop in properties.values():
139
+ prop["value_type"] = "|".join(prop["value_type"])
140
+
141
+ if not properties:
142
+ issue_list.append(NeatValueError("Unable to parse properties"))
143
+
144
+ return properties, issue_list
145
+
146
+
147
+ def handle_meta(
148
+ resource_type: str,
149
+ resources: dict[str, dict],
150
+ resource_id: str,
151
+ res: dict,
152
+ feature: str,
153
+ issue_list: IssueList,
154
+ ):
155
+ if not resources[resource_id][feature] and res[feature]:
156
+ resources[resource_id][feature] = res[feature]
157
+
158
+ # RAISE warning only if the feature is being redefined
159
+ elif resources[resource_id][feature] and res[feature]:
160
+ issue_list.append(
161
+ ResourceRedefinedWarning(
162
+ identifier=resource_id,
163
+ resource_type=resource_type,
164
+ feature=feature,
165
+ current_value=resources[resource_id][feature],
166
+ new_value=res[feature],
167
+ )
469
168
  )
470
-
471
- return components
@@ -10,6 +10,7 @@ from ._rules_input import (
10
10
  DMSInputRules,
11
11
  DMSInputView,
12
12
  )
13
+ from ._validation import DMSValidation
13
14
 
14
15
  __all__ = [
15
16
  "DMSRules",
@@ -27,4 +28,5 @@ __all__ = [
27
28
  "DMSInputContainer",
28
29
  "DMSInputNode",
29
30
  "DMSInputEnum",
31
+ "DMSValidation",
30
32
  ]