cognite-neat 0.88.2__py3-none-any.whl → 0.89.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.

Files changed (129) hide show
  1. cognite/neat/_version.py +1 -1
  2. cognite/neat/constants.py +3 -0
  3. cognite/neat/graph/__init__.py +0 -3
  4. cognite/neat/graph/extractors/_mock_graph_generator.py +2 -1
  5. cognite/neat/graph/loaders/_base.py +3 -3
  6. cognite/neat/graph/loaders/_rdf2asset.py +24 -25
  7. cognite/neat/graph/loaders/_rdf2dms.py +20 -15
  8. cognite/neat/issues/__init__.py +1 -3
  9. cognite/neat/issues/_base.py +261 -71
  10. cognite/neat/issues/errors/__init__.py +73 -0
  11. cognite/neat/issues/errors/_external.py +67 -0
  12. cognite/neat/issues/errors/_general.py +35 -0
  13. cognite/neat/issues/errors/_properties.py +62 -0
  14. cognite/neat/issues/errors/_resources.py +111 -0
  15. cognite/neat/issues/errors/_workflow.py +36 -0
  16. cognite/neat/issues/formatters.py +1 -1
  17. cognite/neat/issues/warnings/__init__.py +66 -0
  18. cognite/neat/issues/warnings/_external.py +40 -0
  19. cognite/neat/issues/warnings/_general.py +29 -0
  20. cognite/neat/issues/warnings/_models.py +92 -0
  21. cognite/neat/issues/warnings/_properties.py +44 -0
  22. cognite/neat/issues/warnings/_resources.py +55 -0
  23. cognite/neat/issues/warnings/user_modeling.py +113 -0
  24. cognite/neat/rules/_shared.py +53 -2
  25. cognite/neat/rules/analysis/_base.py +1 -1
  26. cognite/neat/rules/exporters/_base.py +7 -18
  27. cognite/neat/rules/exporters/_rules2dms.py +17 -20
  28. cognite/neat/rules/exporters/_rules2excel.py +9 -16
  29. cognite/neat/rules/exporters/_rules2ontology.py +77 -64
  30. cognite/neat/rules/exporters/_rules2yaml.py +6 -9
  31. cognite/neat/rules/exporters/_validation.py +11 -96
  32. cognite/neat/rules/importers/_base.py +9 -58
  33. cognite/neat/rules/importers/_dms2rules.py +188 -135
  34. cognite/neat/rules/importers/_dtdl2rules/dtdl_converter.py +48 -35
  35. cognite/neat/rules/importers/_dtdl2rules/dtdl_importer.py +36 -45
  36. cognite/neat/rules/importers/_dtdl2rules/spec.py +7 -0
  37. cognite/neat/rules/importers/_rdf/_imf2rules/_imf2classes.py +8 -4
  38. cognite/neat/rules/importers/_rdf/_imf2rules/_imf2metadata.py +3 -3
  39. cognite/neat/rules/importers/_rdf/_imf2rules/_imf2properties.py +18 -11
  40. cognite/neat/rules/importers/_rdf/_imf2rules/_imf2rules.py +12 -19
  41. cognite/neat/rules/importers/_rdf/_inference2rules.py +14 -37
  42. cognite/neat/rules/importers/_rdf/_owl2rules/_owl2classes.py +1 -0
  43. cognite/neat/rules/importers/_rdf/_owl2rules/_owl2properties.py +1 -0
  44. cognite/neat/rules/importers/_rdf/_owl2rules/_owl2rules.py +9 -20
  45. cognite/neat/rules/importers/_rdf/_shared.py +4 -4
  46. cognite/neat/rules/importers/_spreadsheet2rules.py +46 -97
  47. cognite/neat/rules/importers/_yaml2rules.py +32 -58
  48. cognite/neat/rules/models/__init__.py +21 -5
  49. cognite/neat/rules/models/_base_input.py +162 -0
  50. cognite/neat/rules/models/{_base.py → _base_rules.py} +1 -12
  51. cognite/neat/rules/models/_rdfpath.py +4 -4
  52. cognite/neat/rules/models/{_types/_field.py → _types.py} +5 -10
  53. cognite/neat/rules/models/asset/__init__.py +5 -2
  54. cognite/neat/rules/models/asset/_rules.py +3 -23
  55. cognite/neat/rules/models/asset/_rules_input.py +40 -115
  56. cognite/neat/rules/models/asset/_validation.py +14 -10
  57. cognite/neat/rules/models/data_types.py +150 -44
  58. cognite/neat/rules/models/dms/__init__.py +19 -7
  59. cognite/neat/rules/models/dms/_exporter.py +102 -34
  60. cognite/neat/rules/models/dms/_rules.py +65 -162
  61. cognite/neat/rules/models/dms/_rules_input.py +186 -254
  62. cognite/neat/rules/models/dms/_schema.py +87 -78
  63. cognite/neat/rules/models/dms/_serializer.py +44 -3
  64. cognite/neat/rules/models/dms/_validation.py +106 -68
  65. cognite/neat/rules/models/domain.py +52 -1
  66. cognite/neat/rules/models/entities/__init__.py +63 -0
  67. cognite/neat/rules/models/entities/_constants.py +73 -0
  68. cognite/neat/rules/models/entities/_loaders.py +76 -0
  69. cognite/neat/rules/models/entities/_multi_value.py +67 -0
  70. cognite/neat/rules/models/{entities.py → entities/_single_value.py} +74 -232
  71. cognite/neat/rules/models/entities/_types.py +86 -0
  72. cognite/neat/rules/models/{wrapped_entities.py → entities/_wrapped.py} +1 -1
  73. cognite/neat/rules/models/information/__init__.py +10 -2
  74. cognite/neat/rules/models/information/_rules.py +10 -22
  75. cognite/neat/rules/models/information/_rules_input.py +57 -204
  76. cognite/neat/rules/models/information/_validation.py +48 -25
  77. cognite/neat/rules/transformers/__init__.py +21 -0
  78. cognite/neat/rules/transformers/_base.py +81 -0
  79. cognite/neat/rules/{models/information/_converter.py → transformers/_converters.py} +217 -21
  80. cognite/neat/rules/transformers/_map_onto.py +97 -0
  81. cognite/neat/rules/transformers/_pipelines.py +61 -0
  82. cognite/neat/rules/transformers/_verification.py +136 -0
  83. cognite/neat/{graph/stores → store}/_provenance.py +10 -1
  84. cognite/neat/utils/auxiliary.py +2 -35
  85. cognite/neat/utils/cdf/data_classes.py +20 -0
  86. cognite/neat/utils/regex_patterns.py +6 -0
  87. cognite/neat/utils/text.py +17 -0
  88. cognite/neat/workflows/base.py +4 -4
  89. cognite/neat/workflows/cdf_store.py +3 -3
  90. cognite/neat/workflows/steps/data_contracts.py +1 -1
  91. cognite/neat/workflows/steps/lib/current/graph_extractor.py +3 -3
  92. cognite/neat/workflows/steps/lib/current/graph_loader.py +2 -2
  93. cognite/neat/workflows/steps/lib/current/graph_store.py +1 -1
  94. cognite/neat/workflows/steps/lib/current/rules_exporter.py +116 -47
  95. cognite/neat/workflows/steps/lib/current/rules_importer.py +30 -28
  96. cognite/neat/workflows/steps/lib/current/rules_validator.py +5 -6
  97. cognite/neat/workflows/steps/lib/io/io_steps.py +5 -5
  98. cognite/neat/workflows/steps_registry.py +4 -5
  99. {cognite_neat-0.88.2.dist-info → cognite_neat-0.89.0.dist-info}/METADATA +1 -1
  100. {cognite_neat-0.88.2.dist-info → cognite_neat-0.89.0.dist-info}/RECORD +105 -106
  101. cognite/neat/exceptions.py +0 -145
  102. cognite/neat/graph/exceptions.py +0 -90
  103. cognite/neat/issues/errors/external.py +0 -21
  104. cognite/neat/issues/errors/properties.py +0 -75
  105. cognite/neat/issues/errors/resources.py +0 -123
  106. cognite/neat/issues/errors/schema.py +0 -0
  107. cognite/neat/issues/neat_warnings/__init__.py +0 -2
  108. cognite/neat/issues/neat_warnings/identifier.py +0 -27
  109. cognite/neat/issues/neat_warnings/models.py +0 -22
  110. cognite/neat/issues/neat_warnings/properties.py +0 -77
  111. cognite/neat/issues/neat_warnings/resources.py +0 -125
  112. cognite/neat/rules/issues/__init__.py +0 -22
  113. cognite/neat/rules/issues/base.py +0 -63
  114. cognite/neat/rules/issues/dms.py +0 -549
  115. cognite/neat/rules/issues/fileread.py +0 -197
  116. cognite/neat/rules/issues/ontology.py +0 -298
  117. cognite/neat/rules/issues/spreadsheet.py +0 -563
  118. cognite/neat/rules/issues/spreadsheet_file.py +0 -151
  119. cognite/neat/rules/issues/tables.py +0 -72
  120. cognite/neat/rules/models/_constants.py +0 -1
  121. cognite/neat/rules/models/_types/__init__.py +0 -19
  122. cognite/neat/rules/models/asset/_converter.py +0 -4
  123. cognite/neat/rules/models/dms/_converter.py +0 -145
  124. cognite/neat/workflows/_exceptions.py +0 -41
  125. /cognite/neat/{graph/stores → store}/__init__.py +0 -0
  126. /cognite/neat/{graph/stores → store}/_base.py +0 -0
  127. {cognite_neat-0.88.2.dist-info → cognite_neat-0.89.0.dist-info}/LICENSE +0 -0
  128. {cognite_neat-0.88.2.dist-info → cognite_neat-0.89.0.dist-info}/WHEEL +0 -0
  129. {cognite_neat-0.88.2.dist-info → cognite_neat-0.89.0.dist-info}/entry_points.txt +0 -0
@@ -1,15 +1,21 @@
1
1
  import sys
2
2
  import warnings
3
- from abc import ABC, abstractmethod
3
+ from abc import ABC
4
4
  from collections import UserList
5
- from collections.abc import Sequence
6
- from dataclasses import dataclass
5
+ from collections.abc import Collection, Hashable, Iterable, Sequence
6
+ from dataclasses import dataclass, fields
7
7
  from functools import total_ordering
8
- from typing import Any, ClassVar, TypeVar
8
+ from pathlib import Path
9
+ from types import UnionType
10
+ from typing import Any, ClassVar, Literal, TypeAlias, TypeVar, get_args, get_origin
9
11
  from warnings import WarningMessage
10
12
 
11
13
  import pandas as pd
12
- from pydantic_core import ErrorDetails, PydanticCustomError
14
+ from cognite.client.data_classes.data_modeling import ContainerId, ViewId
15
+ from pydantic_core import ErrorDetails
16
+
17
+ from cognite.neat.utils.spreadsheet import SpreadsheetRead
18
+ from cognite.neat.utils.text import humanize_collection, to_camel, to_snake
13
19
 
14
20
  if sys.version_info < (3, 11):
15
21
  from exceptiongroup import ExceptionGroup
@@ -27,55 +33,151 @@ __all__ = [
27
33
  "MultiValueError",
28
34
  ]
29
35
 
36
+ T_Identifier = TypeVar("T_Identifier", bound=Hashable)
37
+
38
+ T_ReferenceIdentifier = TypeVar("T_ReferenceIdentifier", bound=Hashable)
39
+
40
+ ResourceType: TypeAlias = (
41
+ Literal[
42
+ "view",
43
+ "container",
44
+ "view property",
45
+ "container property",
46
+ "space",
47
+ "class",
48
+ "asset",
49
+ "relationship",
50
+ "data model",
51
+ "edge",
52
+ "node",
53
+ "enum collection",
54
+ "unknown",
55
+ ]
56
+ # String to handle all unknown types in different importers.
57
+ | str
58
+ )
59
+
30
60
 
31
61
  @total_ordering
32
62
  @dataclass(frozen=True)
33
- class NeatIssue(ABC):
63
+ class NeatIssue:
34
64
  """This is the base class for all exceptions and warnings (issues) used in Neat."""
35
65
 
36
- description: ClassVar[str]
37
66
  extra: ClassVar[str | None] = None
38
67
  fix: ClassVar[str | None] = None
39
68
 
40
- def message(self) -> str:
41
- """Return a human-readable message for the issue.
42
-
43
- This is the default implementation, which returns the description.
44
- It is recommended to override this method in subclasses with a more
45
- specific message.
46
- """
47
- return self.__doc__ or "Missing"
69
+ def as_message(self) -> str:
70
+ """Return a human-readable message for the issue."""
71
+ template = self.__doc__
72
+ if not template:
73
+ return "Missing"
74
+ variables, has_all_optional = self._get_variables()
75
+
76
+ msg = template.format(**variables)
77
+ if self.extra and has_all_optional:
78
+ msg += "\n" + self.extra.format(**variables)
79
+ if self.fix:
80
+ msg += f"\nFix: {self.fix.format(**variables)}"
81
+ name = type(self).__name__
82
+ return f"{name}: {msg}"
83
+
84
+ def _get_variables(self) -> tuple[dict[str, str], bool]:
85
+ variables: dict[str, str] = {}
86
+ has_all_optional = True
87
+ for name, var_ in vars(self).items():
88
+ if var_ is None:
89
+ has_all_optional = False
90
+ elif isinstance(var_, str):
91
+ variables[name] = var_
92
+ elif isinstance(var_, Path):
93
+ variables[name] = var_.as_posix()
94
+ elif isinstance(var_, Collection):
95
+ variables[name] = humanize_collection(var_)
96
+ else:
97
+ variables[name] = repr(var_)
98
+ return variables, has_all_optional
48
99
 
49
- @abstractmethod
50
100
  def dump(self) -> dict[str, Any]:
51
101
  """Return a dictionary representation of the issue."""
52
- raise NotImplementedError()
102
+ variables = vars(self)
103
+ output = {to_camel(key): self._dump_value(value) for key, value in variables.items() if value is not None}
104
+ output["NeatIssue"] = type(self).__name__
105
+ return output
106
+
107
+ @classmethod
108
+ def _dump_value(cls, value: Any) -> list | int | bool | float | str | dict:
109
+ if isinstance(value, str | int | bool | float):
110
+ return value
111
+ elif isinstance(value, frozenset):
112
+ return [cls._dump_value(item) for item in value]
113
+ elif isinstance(value, Path):
114
+ return value.as_posix()
115
+ elif isinstance(value, tuple):
116
+ return [cls._dump_value(item) for item in value]
117
+ elif isinstance(value, ViewId | ContainerId):
118
+ return value.dump(camel_case=True, include_type=True)
119
+ raise ValueError(f"Unsupported type: {type(value)}")
120
+
121
+ @classmethod
122
+ def load(cls, data: dict[str, Any]) -> "NeatIssue":
123
+ """Create an instance of the issue from a dictionary."""
124
+ from cognite.neat.issues.errors import _NEAT_ERRORS_BY_NAME, NeatValueError
125
+ from cognite.neat.issues.warnings import _NEAT_WARNINGS_BY_NAME
126
+
127
+ if "NeatIssue" not in data:
128
+ raise NeatValueError("The data does not contain a NeatIssue key.")
129
+ issue_type = data.pop("NeatIssue")
130
+ args = {to_snake(key): value for key, value in data.items()}
131
+ if issue_type in _NEAT_ERRORS_BY_NAME:
132
+ return cls._load_values(_NEAT_ERRORS_BY_NAME[issue_type], args)
133
+ elif issue_type in _NEAT_WARNINGS_BY_NAME:
134
+ return cls._load_values(_NEAT_WARNINGS_BY_NAME[issue_type], args)
135
+ else:
136
+ raise NeatValueError(f"Unknown issue type: {issue_type}")
137
+
138
+ @classmethod
139
+ def _load_values(cls, neat_issue_cls: "type[NeatIssue]", data: dict[str, Any]) -> "NeatIssue":
140
+ args: dict[str, Any] = {}
141
+ for f in fields(neat_issue_cls):
142
+ if f.name not in data:
143
+ continue
144
+ value = data[f.name]
145
+ args[f.name] = cls._load_value(f.type, value)
146
+ return neat_issue_cls(**args)
147
+
148
+ @classmethod
149
+ def _load_value(cls, type_: type, value: Any) -> Any:
150
+ if isinstance(type_, UnionType) or get_origin(type_) is UnionType:
151
+ args = get_args(type_)
152
+ return cls._load_value(args[0], value)
153
+ elif type_ is frozenset or get_origin(type_) is frozenset:
154
+ subtype = get_args(type_)[0]
155
+ return frozenset(cls._load_value(subtype, item) for item in value)
156
+ elif type_ is Path:
157
+ return Path(value)
158
+ elif type_ is tuple or get_origin(type_) is tuple:
159
+ subtype = get_args(type_)[0]
160
+ return tuple(cls._load_value(subtype, item) for item in value)
161
+ elif type_ is ViewId:
162
+ return ViewId.load(value)
163
+ elif type_ is ContainerId:
164
+ return ContainerId.load(value)
165
+ return value
53
166
 
54
167
  def __lt__(self, other: "NeatIssue") -> bool:
55
168
  if not isinstance(other, NeatIssue):
56
169
  return NotImplemented
57
- return (type(self).__name__, self.message()) < (type(other).__name__, other.message())
170
+ return (type(self).__name__, self.as_message()) < (type(other).__name__, other.as_message())
58
171
 
59
172
  def __eq__(self, other: object) -> bool:
60
173
  if not isinstance(other, NeatIssue):
61
174
  return NotImplemented
62
- return (type(self).__name__, self.message()) == (type(other).__name__, other.message())
175
+ return (type(self).__name__, self.as_message()) == (type(other).__name__, other.as_message())
63
176
 
64
177
 
65
178
  @dataclass(frozen=True)
66
- class NeatError(NeatIssue, ABC):
67
- def dump(self) -> dict[str, Any]:
68
- return {"errorType": type(self).__name__}
69
-
70
- def as_exception(self) -> ValueError:
71
- return ValueError(self.message())
72
-
73
- def as_pydantic_exception(self) -> PydanticCustomError:
74
- return PydanticCustomError(
75
- type(self).__name__,
76
- self.message(),
77
- dict(description=self.__doc__, fix=self.fix),
78
- )
179
+ class NeatError(NeatIssue, Exception):
180
+ """This is the base class for all exceptions (errors) used in Neat."""
79
181
 
80
182
  @classmethod
81
183
  def from_pydantic_errors(cls, errors: list[ErrorDetails], **kwargs) -> "list[NeatError]":
@@ -84,23 +186,60 @@ class NeatError(NeatIssue, ABC):
84
186
  This is intended to be overridden in subclasses to handle specific error types.
85
187
  """
86
188
  all_errors: list[NeatError] = []
189
+ read_info_by_sheet = kwargs.get("read_info_by_sheet")
190
+
87
191
  for error in errors:
88
- if isinstance(ctx := error.get("ctx"), dict) and isinstance(
89
- multi_error := ctx.get("error"), MultiValueError
90
- ):
192
+ ctx = error.get("ctx")
193
+ if isinstance(ctx, dict) and isinstance(multi_error := ctx.get("error"), MultiValueError):
194
+ if read_info_by_sheet:
195
+ for caught_error in multi_error.errors:
196
+ cls._adjust_row_numbers(caught_error, read_info_by_sheet) # type: ignore[arg-type]
91
197
  all_errors.extend(multi_error.errors) # type: ignore[arg-type]
198
+ elif isinstance(ctx, dict) and isinstance(single_error := ctx.get("error"), NeatError):
199
+ if read_info_by_sheet:
200
+ cls._adjust_row_numbers(single_error, read_info_by_sheet)
201
+ all_errors.append(single_error)
202
+ elif len(error["loc"]) >= 4 and read_info_by_sheet:
203
+ all_errors.append(RowError.from_pydantic_error(error, read_info_by_sheet))
92
204
  else:
93
205
  all_errors.append(DefaultPydanticError.from_pydantic_error(error))
94
206
  return all_errors
95
207
 
208
+ @staticmethod
209
+ def _adjust_row_numbers(caught_error: "NeatError", read_info_by_sheet: dict[str, SpreadsheetRead]) -> None:
210
+ from cognite.neat.issues.errors._properties import PropertyDefinitionDuplicatedError
211
+ from cognite.neat.issues.errors._resources import ResourceNotDefinedError
212
+
213
+ reader = read_info_by_sheet.get("Properties", SpreadsheetRead())
214
+
215
+ if isinstance(caught_error, PropertyDefinitionDuplicatedError) and caught_error.location_name == "rows":
216
+ adjusted_row_number = (
217
+ tuple(
218
+ reader.adjusted_row_number(row_no) if isinstance(row_no, int) else row_no
219
+ for row_no in caught_error.locations or []
220
+ )
221
+ or None
222
+ )
223
+ # The error is frozen, so we have to use __setattr__ to change the row number
224
+ object.__setattr__(caught_error, "locations", adjusted_row_number)
225
+ elif isinstance(caught_error, RowError):
226
+ # Adjusting the row number to the actual row number in the spreadsheet
227
+ new_row = reader.adjusted_row_number(caught_error.row)
228
+ # The error is frozen, so we have to use __setattr__ to change the row number
229
+ object.__setattr__(caught_error, "row", new_row)
230
+ elif isinstance(caught_error, ResourceNotDefinedError):
231
+ if isinstance(caught_error.row_number, int) and caught_error.sheet_name == "Properties":
232
+ new_row = reader.adjusted_row_number(caught_error.row_number)
233
+ object.__setattr__(caught_error, "row_number", new_row)
234
+
96
235
 
97
236
  @dataclass(frozen=True)
98
- class DefaultPydanticError(NeatError):
237
+ class DefaultPydanticError(NeatError, ValueError):
238
+ """{type}: {msg} [loc={loc}]"""
239
+
99
240
  type: str
100
241
  loc: tuple[int | str, ...]
101
242
  msg: str
102
- input: Any
103
- ctx: dict[str, Any] | None
104
243
 
105
244
  @classmethod
106
245
  def from_pydantic_error(cls, error: ErrorDetails) -> "DefaultPydanticError":
@@ -108,20 +247,9 @@ class DefaultPydanticError(NeatError):
108
247
  type=error["type"],
109
248
  loc=error["loc"],
110
249
  msg=error["msg"],
111
- input=error.get("input"),
112
- ctx=error.get("ctx"),
113
250
  )
114
251
 
115
- def dump(self) -> dict[str, Any]:
116
- output = super().dump()
117
- output["type"] = self.type
118
- output["loc"] = self.loc
119
- output["msg"] = self.msg
120
- output["input"] = self.input
121
- output["ctx"] = self.ctx
122
- return output
123
-
124
- def message(self) -> str:
252
+ def as_message(self) -> str:
125
253
  if self.loc and len(self.loc) == 1:
126
254
  return f"{self.loc[0]} sheet: {self.msg}"
127
255
  elif self.loc and len(self.loc) == 2:
@@ -131,30 +259,68 @@ class DefaultPydanticError(NeatError):
131
259
 
132
260
 
133
261
  @dataclass(frozen=True)
134
- class NeatWarning(NeatIssue, ABC, UserWarning):
135
- def dump(self) -> dict[str, Any]:
136
- return {"warningType": type(self).__name__}
262
+ class RowError(NeatError, ValueError):
263
+ """In {sheet_name}, row={row}, column={column}: {msg}. [type={type}, input_value={input}]"""
264
+
265
+ extra = "For further information visit {url}"
266
+
267
+ sheet_name: str
268
+ column: str
269
+ row: int
270
+ type: str
271
+ msg: str
272
+ input: Any
273
+ url: str | None = None
274
+
275
+ @classmethod
276
+ def from_pydantic_error(
277
+ cls,
278
+ error: ErrorDetails,
279
+ read_info_by_sheet: dict[str, SpreadsheetRead] | None = None,
280
+ ) -> Self:
281
+ sheet_name, _, row, column, *__ = error["loc"]
282
+ reader = (read_info_by_sheet or {}).get(str(sheet_name), SpreadsheetRead())
283
+ return cls(
284
+ sheet_name=str(sheet_name),
285
+ column=str(column),
286
+ row=reader.adjusted_row_number(int(row)),
287
+ type=error["type"],
288
+ msg=error["msg"],
289
+ input=error.get("input"),
290
+ url=str(url) if (url := error.get("url")) else None,
291
+ )
292
+
293
+ def as_message(self) -> str:
294
+ input_str = str(self.input) if self.input is not None else ""
295
+ input_str = input_str[:50] + "..." if len(input_str) > 50 else input_str
296
+ output = (
297
+ f"In {self.sheet_name}, row={self.row}, column={self.column}: {self.msg}. "
298
+ f"[type={self.type}, input_value={input_str}]"
299
+ )
300
+ if self.url:
301
+ output += f" For further information visit {self.url}"
302
+ return output
303
+
304
+
305
+ @dataclass(frozen=True)
306
+ class NeatWarning(NeatIssue, UserWarning):
307
+ """This is the base class for all warnings used in Neat."""
137
308
 
138
309
  @classmethod
139
310
  def from_warning(cls, warning: WarningMessage) -> "NeatWarning":
311
+ """Create a NeatWarning from a WarningMessage."""
140
312
  return DefaultWarning.from_warning_message(warning)
141
313
 
142
314
 
143
315
  @dataclass(frozen=True)
144
316
  class DefaultWarning(NeatWarning):
145
- description = "A warning was raised during validation."
146
- fix = "No fix is available."
317
+ """{category}: {warning}"""
147
318
 
148
- warning: str | Warning
149
- category: type[Warning]
150
- source: str | None = None
319
+ extra = "Source: {source}"
151
320
 
152
- def dump(self) -> dict[str, Any]:
153
- output = super().dump()
154
- output["msg"] = str(self.warning)
155
- output["category"] = self.category.__name__
156
- output["source"] = self.source
157
- return output
321
+ warning: str
322
+ category: str
323
+ source: str | None = None
158
324
 
159
325
  @classmethod
160
326
  def from_warning_message(cls, warning: WarningMessage) -> NeatWarning:
@@ -162,52 +328,61 @@ class DefaultWarning(NeatWarning):
162
328
  return warning.message
163
329
 
164
330
  return cls(
165
- warning=warning.message,
166
- category=warning.category,
331
+ warning=str(warning.message),
332
+ category=warning.category.__name__,
167
333
  source=warning.source,
168
334
  )
169
335
 
170
- def message(self) -> str:
336
+ def as_message(self) -> str:
171
337
  return str(self.warning)
172
338
 
173
339
 
174
340
  T_NeatIssue = TypeVar("T_NeatIssue", bound=NeatIssue)
175
341
 
176
342
 
177
- class NeatIssueList(UserList[T_NeatIssue], ABC):
343
+ class NeatIssueList(UserList[T_NeatIssue], Sequence[T_NeatIssue], ABC):
344
+ """This is a generic list of NeatIssues."""
345
+
178
346
  def __init__(self, issues: Sequence[T_NeatIssue] | None = None, title: str | None = None):
179
347
  super().__init__(issues or [])
180
348
  self.title = title
181
349
 
182
350
  @property
183
351
  def errors(self) -> Self:
352
+ """Return all the errors in this list."""
184
353
  return type(self)([issue for issue in self if isinstance(issue, NeatError)]) # type: ignore[misc]
185
354
 
186
355
  @property
187
356
  def has_errors(self) -> bool:
357
+ """Return True if this list contains any errors."""
188
358
  return any(isinstance(issue, NeatError) for issue in self)
189
359
 
190
360
  @property
191
361
  def warnings(self) -> Self:
362
+ """Return all the warnings in this list."""
192
363
  return type(self)([issue for issue in self if isinstance(issue, NeatWarning)]) # type: ignore[misc]
193
364
 
194
365
  def as_errors(self) -> ExceptionGroup:
366
+ """Return an ExceptionGroup with all the errors in this list."""
195
367
  return ExceptionGroup(
196
368
  "Operation failed",
197
- [ValueError(issue.message()) for issue in self if isinstance(issue, NeatError)],
369
+ [issue for issue in self if isinstance(issue, NeatError)],
198
370
  )
199
371
 
200
372
  def trigger_warnings(self) -> None:
373
+ """Trigger all warnings in this list."""
201
374
  for warning in [issue for issue in self if isinstance(issue, NeatWarning)]:
202
375
  warnings.warn(warning, stacklevel=2)
203
376
 
204
377
  def to_pandas(self) -> pd.DataFrame:
378
+ """Return a pandas DataFrame representation of this list."""
205
379
  return pd.DataFrame([issue.dump() for issue in self])
206
380
 
207
381
  def _repr_html_(self) -> str | None:
208
382
  return self.to_pandas()._repr_html_() # type: ignore[operator]
209
383
 
210
384
  def as_exception(self) -> "MultiValueError":
385
+ """Return a MultiValueError with all the errors in this list."""
211
386
  return MultiValueError(self.errors)
212
387
 
213
388
 
@@ -219,8 +394,23 @@ class MultiValueError(ValueError):
219
394
 
220
395
  """
221
396
 
222
- def __init__(self, errors: Sequence[T_NeatIssue]):
397
+ def __init__(self, errors: Sequence[NeatIssue]):
223
398
  self.errors = list(errors)
224
399
 
225
400
 
226
- class IssueList(NeatIssueList[NeatIssue]): ...
401
+ class IssueList(NeatIssueList[NeatIssue]):
402
+ """This is a list of NeatIssues."""
403
+
404
+ ...
405
+
406
+
407
+ T_Cls = TypeVar("T_Cls")
408
+
409
+
410
+ def _get_subclasses(cls_: type[T_Cls], include_base: bool = False) -> Iterable[type[T_Cls]]:
411
+ """Get all subclasses of a class."""
412
+ if include_base:
413
+ yield cls_
414
+ for s in cls_.__subclasses__():
415
+ yield s
416
+ yield from _get_subclasses(s, False)
@@ -0,0 +1,73 @@
1
+ from cognite.neat.issues._base import DefaultPydanticError, NeatError, RowError, _get_subclasses
2
+
3
+ from ._external import (
4
+ AuthorizationError,
5
+ FileMissingRequiredFieldError,
6
+ FileNotAFileError,
7
+ FileNotFoundNeatError,
8
+ FileReadError,
9
+ FileTypeUnexpectedError,
10
+ NeatYamlError,
11
+ )
12
+ from ._general import NeatImportError, NeatTypeError, NeatValueError, RegexViolationError
13
+ from ._properties import (
14
+ PropertyDefinitionDuplicatedError,
15
+ PropertyDefinitionError,
16
+ PropertyMappingDuplicatedError,
17
+ PropertyNotFoundError,
18
+ PropertyTypeNotSupportedError,
19
+ )
20
+ from ._resources import (
21
+ ResourceChangedError,
22
+ ResourceConvertionError,
23
+ ResourceCreationError,
24
+ ResourceDuplicatedError,
25
+ ResourceError,
26
+ ResourceMissingIdentifierError,
27
+ ResourceNotDefinedError,
28
+ ResourceNotFoundError,
29
+ ResourceRetrievalError,
30
+ )
31
+ from ._workflow import (
32
+ WorkflowConfigurationNotSetError,
33
+ WorkFlowMissingDataError,
34
+ WorkflowStepNotInitializedError,
35
+ WorkflowStepOutputError,
36
+ )
37
+
38
+ __all__ = [
39
+ "NeatError",
40
+ "NeatValueError",
41
+ "NeatImportError",
42
+ "RegexViolationError",
43
+ "AuthorizationError",
44
+ "NeatYamlError",
45
+ "FileReadError",
46
+ "ResourceCreationError",
47
+ "FileNotFoundNeatError",
48
+ "FileMissingRequiredFieldError",
49
+ "PropertyDefinitionError",
50
+ "PropertyTypeNotSupportedError",
51
+ "PropertyNotFoundError",
52
+ "PropertyDefinitionDuplicatedError",
53
+ "ResourceChangedError",
54
+ "ResourceDuplicatedError",
55
+ "ResourceRetrievalError",
56
+ "ResourceNotFoundError",
57
+ "ResourceError",
58
+ "ResourceNotDefinedError",
59
+ "ResourceMissingIdentifierError",
60
+ "ResourceConvertionError",
61
+ "WorkflowConfigurationNotSetError",
62
+ "WorkFlowMissingDataError",
63
+ "WorkflowStepNotInitializedError",
64
+ "WorkflowStepOutputError",
65
+ "FileTypeUnexpectedError",
66
+ "FileNotAFileError",
67
+ "DefaultPydanticError",
68
+ "PropertyMappingDuplicatedError",
69
+ "RowError",
70
+ "NeatTypeError",
71
+ ]
72
+
73
+ _NEAT_ERRORS_BY_NAME = {error.__name__: error for error in _get_subclasses(NeatError, include_base=True)}
@@ -0,0 +1,67 @@
1
+ from dataclasses import dataclass
2
+ from pathlib import Path
3
+
4
+ from yaml import YAMLError
5
+
6
+ from cognite.neat.issues import NeatError
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class AuthorizationError(NeatError, RuntimeError):
11
+ """Missing authorization for {action}: {reason}"""
12
+
13
+ action: str
14
+ reason: str
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class FileReadError(NeatError, RuntimeError):
19
+ """Error when reading file, {filepath}: {reason}"""
20
+
21
+ fix = "Is the {filepath} open in another program? Is the file corrupted?"
22
+ filepath: Path
23
+ reason: str
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class FileNotFoundNeatError(NeatError, FileNotFoundError):
28
+ """File {filepath} not found"""
29
+
30
+ fix = "Make sure to provide a valid file"
31
+ filepath: Path
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class FileMissingRequiredFieldError(NeatError, ValueError):
36
+ """Missing required {field_name} in {filepath}: {field}"""
37
+
38
+ filepath: Path
39
+ field_name: str
40
+ field: str
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class NeatYamlError(NeatError, YAMLError):
45
+ """Invalid YAML: {reason}"""
46
+
47
+ extra = "Expected format: {expected_format}"
48
+ fix = "Check if the file is a valid YAML file"
49
+
50
+ reason: str
51
+ expected_format: str | None = None
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class FileTypeUnexpectedError(NeatError, TypeError):
56
+ """Unexpected file type: {filepath}. Expected format: {expected_format}"""
57
+
58
+ filepath: Path
59
+ expected_format: frozenset[str]
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class FileNotAFileError(NeatError, FileNotFoundError):
64
+ """{filepath} is not a file"""
65
+
66
+ fix = "Make sure to provide a valid file"
67
+ filepath: Path
@@ -0,0 +1,35 @@
1
+ from dataclasses import dataclass
2
+
3
+ from cognite.neat.issues import NeatError
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class NeatValueError(NeatError, ValueError):
8
+ """{raw_message}"""
9
+
10
+ raw_message: str
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class NeatTypeError(NeatError, TypeError):
15
+ """{raw_message}"""
16
+
17
+ raw_message: str
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class RegexViolationError(NeatError, ValueError):
22
+ """Value, {value} failed regex, {regex}, validation. Make sure that the name follows the regex pattern."""
23
+
24
+ value: str
25
+ regex: str
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class NeatImportError(NeatError, ImportError):
30
+ """The functionality requires {module}. You can include it
31
+ in your neat installation with `pip install "cognite-neat[{neat_extra}]"`.
32
+ """
33
+
34
+ module: str
35
+ neat_extra: str