cognite-neat 0.126.0__py3-none-any.whl → 0.126.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 (75) hide show
  1. cognite/neat/_client/__init__.py +4 -0
  2. cognite/neat/_client/api.py +8 -0
  3. cognite/neat/_client/client.py +19 -0
  4. cognite/neat/_client/config.py +40 -0
  5. cognite/neat/_client/containers_api.py +73 -0
  6. cognite/neat/_client/data_classes.py +10 -0
  7. cognite/neat/_client/data_model_api.py +63 -0
  8. cognite/neat/_client/spaces_api.py +67 -0
  9. cognite/neat/_client/views_api.py +82 -0
  10. cognite/neat/_data_model/_analysis.py +127 -0
  11. cognite/neat/_data_model/_constants.py +59 -0
  12. cognite/neat/_data_model/_shared.py +46 -0
  13. cognite/neat/_data_model/deployer/__init__.py +0 -0
  14. cognite/neat/_data_model/deployer/_differ.py +113 -0
  15. cognite/neat/_data_model/deployer/_differ_container.py +354 -0
  16. cognite/neat/_data_model/deployer/_differ_data_model.py +29 -0
  17. cognite/neat/_data_model/deployer/_differ_space.py +9 -0
  18. cognite/neat/_data_model/deployer/_differ_view.py +194 -0
  19. cognite/neat/_data_model/deployer/data_classes.py +176 -0
  20. cognite/neat/_data_model/exporters/__init__.py +4 -0
  21. cognite/neat/_data_model/exporters/_base.py +6 -1
  22. cognite/neat/_data_model/exporters/_table_exporter/__init__.py +0 -0
  23. cognite/neat/_data_model/exporters/_table_exporter/exporter.py +106 -0
  24. cognite/neat/_data_model/exporters/_table_exporter/workbook.py +414 -0
  25. cognite/neat/_data_model/exporters/_table_exporter/writer.py +391 -0
  26. cognite/neat/_data_model/importers/__init__.py +2 -1
  27. cognite/neat/_data_model/importers/_api_importer.py +88 -0
  28. cognite/neat/_data_model/importers/_table_importer/data_classes.py +48 -8
  29. cognite/neat/_data_model/importers/_table_importer/importer.py +74 -5
  30. cognite/neat/_data_model/importers/_table_importer/reader.py +63 -7
  31. cognite/neat/_data_model/models/dms/__init__.py +17 -1
  32. cognite/neat/_data_model/models/dms/_base.py +12 -8
  33. cognite/neat/_data_model/models/dms/_constants.py +1 -1
  34. cognite/neat/_data_model/models/dms/_constraints.py +2 -1
  35. cognite/neat/_data_model/models/dms/_container.py +5 -5
  36. cognite/neat/_data_model/models/dms/_data_model.py +3 -3
  37. cognite/neat/_data_model/models/dms/_data_types.py +8 -1
  38. cognite/neat/_data_model/models/dms/_http.py +18 -0
  39. cognite/neat/_data_model/models/dms/_indexes.py +2 -1
  40. cognite/neat/_data_model/models/dms/_references.py +17 -4
  41. cognite/neat/_data_model/models/dms/_space.py +11 -7
  42. cognite/neat/_data_model/models/dms/_view_property.py +7 -4
  43. cognite/neat/_data_model/models/dms/_views.py +16 -6
  44. cognite/neat/_data_model/validation/__init__.py +0 -0
  45. cognite/neat/_data_model/validation/_base.py +16 -0
  46. cognite/neat/_data_model/validation/dms/__init__.py +9 -0
  47. cognite/neat/_data_model/validation/dms/_orchestrator.py +68 -0
  48. cognite/neat/_data_model/validation/dms/_validators.py +139 -0
  49. cognite/neat/_exceptions.py +15 -3
  50. cognite/neat/_issues.py +39 -6
  51. cognite/neat/_session/__init__.py +3 -0
  52. cognite/neat/_session/_physical.py +88 -0
  53. cognite/neat/_session/_session.py +34 -25
  54. cognite/neat/_session/_wrappers.py +61 -0
  55. cognite/neat/_state_machine/__init__.py +10 -0
  56. cognite/neat/{_session/_state_machine → _state_machine}/_base.py +11 -1
  57. cognite/neat/_state_machine/_states.py +53 -0
  58. cognite/neat/_store/__init__.py +3 -0
  59. cognite/neat/_store/_provenance.py +55 -0
  60. cognite/neat/_store/_store.py +124 -0
  61. cognite/neat/_utils/_reader.py +194 -0
  62. cognite/neat/_utils/http_client/__init__.py +14 -20
  63. cognite/neat/_utils/http_client/_client.py +22 -61
  64. cognite/neat/_utils/http_client/_data_classes.py +167 -268
  65. cognite/neat/_utils/text.py +6 -0
  66. cognite/neat/_utils/useful_types.py +23 -2
  67. cognite/neat/_version.py +1 -1
  68. cognite/neat/v0/core/_data_model/importers/_rdf/_shared.py +2 -2
  69. {cognite_neat-0.126.0.dist-info → cognite_neat-0.126.1.dist-info}/METADATA +1 -1
  70. {cognite_neat-0.126.0.dist-info → cognite_neat-0.126.1.dist-info}/RECORD +72 -38
  71. cognite/neat/_data_model/exporters/_table_exporter.py +0 -35
  72. cognite/neat/_session/_state_machine/__init__.py +0 -23
  73. cognite/neat/_session/_state_machine/_states.py +0 -150
  74. {cognite_neat-0.126.0.dist-info → cognite_neat-0.126.1.dist-info}/WHEEL +0 -0
  75. {cognite_neat-0.126.0.dist-info → cognite_neat-0.126.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,45 +1,66 @@
1
+ import sys
1
2
  from abc import ABC, abstractmethod
2
- from collections.abc import Callable, Sequence
3
- from dataclasses import dataclass, field
4
- from typing import Generic, Literal, TypeAlias
3
+ from collections import UserList
4
+ from collections.abc import MutableSequence, Sequence
5
+ from typing import Generic, Literal, TypeAlias, TypeVar
5
6
 
6
7
  import httpx
8
+ from pydantic import BaseModel, ConfigDict, Field, JsonValue, ValidationError, model_serializer
7
9
 
10
+ from cognite.neat._exceptions import CDFAPIException
8
11
  from cognite.neat._utils.http_client._tracker import ItemsRequestTracker
9
- from cognite.neat._utils.useful_types import T_ID, JsonVal
12
+ from cognite.neat._utils.useful_types import PrimaryTypes, ReferenceObject, T_Reference
13
+
14
+ if sys.version_info >= (3, 11):
15
+ from typing import Self
16
+ else:
17
+ from typing_extensions import Self
10
18
 
11
19
  StatusCode: TypeAlias = int
12
20
 
13
21
 
14
- @dataclass
15
- class HTTPMessage:
22
+ class HTTPMessage(BaseModel):
16
23
  """Base class for HTTP messages (requests and responses)"""
17
24
 
18
- def dump(self) -> dict[str, JsonVal]:
19
- """Dumps the message to a JSON serializable dictionary.
20
-
21
- Returns:
22
- dict[str, JsonVal]: The message as a dictionary.
23
- """
24
- # We avoid using the asdict function as we know we have a shallow structure,
25
- # and this roughly ~10x faster.
26
- output = self.__dict__.copy()
27
- output["type"] = type(self).__name__
28
- return output
29
-
30
25
 
31
- @dataclass
32
26
  class FailedRequestMessage(HTTPMessage):
33
- error: str
27
+ message: str
34
28
 
35
29
 
36
- @dataclass
37
30
  class ResponseMessage(HTTPMessage):
38
- status_code: StatusCode
31
+ code: StatusCode
32
+ body: str
33
+
34
+
35
+ class SuccessResponse(ResponseMessage): ...
36
+
37
+
38
+ class ErrorDetails(BaseModel):
39
+ """This is the structure of failure responses from CDF APIs"""
40
+
41
+ code: StatusCode
42
+ message: str
43
+ missing: list[JsonValue] | None = None
44
+ duplicated: list[JsonValue] | None = None
45
+ is_auto_retryable: bool | None = Field(None, alias="isAutoRetryable")
46
+
47
+ @classmethod
48
+ def from_response(cls, response: httpx.Response) -> "ErrorDetails":
49
+ try:
50
+ return _ErrorResponse.model_validate_json(response.text).error
51
+ except ValidationError:
52
+ return cls(code=response.status_code, message=response.text)
53
+
54
+
55
+ class _ErrorResponse(BaseModel):
56
+ error: ErrorDetails
57
+
58
+
59
+ class FailedResponse(ResponseMessage):
60
+ error: ErrorDetails
39
61
 
40
62
 
41
- @dataclass
42
- class RequestMessage(HTTPMessage):
63
+ class RequestMessage(HTTPMessage, ABC):
43
64
  """Base class for HTTP request messages"""
44
65
 
45
66
  endpoint_url: str
@@ -54,12 +75,11 @@ class RequestMessage(HTTPMessage):
54
75
  return self.connect_attempt + self.read_attempt + self.status_attempt
55
76
 
56
77
  @abstractmethod
57
- def create_responses(
58
- self,
59
- response: httpx.Response,
60
- response_body: dict[str, JsonVal] | None = None,
61
- error_message: str | None = None,
62
- ) -> Sequence[HTTPMessage]:
78
+ def create_success_response(self, response: httpx.Response) -> Sequence[HTTPMessage]:
79
+ raise NotImplementedError()
80
+
81
+ @abstractmethod
82
+ def create_failure_response(self, response: httpx.Response) -> Sequence[HTTPMessage]:
63
83
  raise NotImplementedError()
64
84
 
65
85
  @abstractmethod
@@ -67,158 +87,120 @@ class RequestMessage(HTTPMessage):
67
87
  raise NotImplementedError()
68
88
 
69
89
 
70
- @dataclass
71
- class SuccessResponse(ResponseMessage):
72
- body: dict[str, JsonVal] | None = None
90
+ class SimpleRequest(RequestMessage):
91
+ """Base class for requests with a simple success/fail response structure"""
73
92
 
93
+ def create_success_response(self, response: httpx.Response) -> Sequence[HTTPMessage]:
94
+ return [SuccessResponse(code=response.status_code, body=response.text)]
74
95
 
75
- @dataclass
76
- class FailedResponse(ResponseMessage):
77
- error: str
78
- body: dict[str, JsonVal] | None = None
96
+ def create_failure_response(self, response: httpx.Response) -> Sequence[HTTPMessage]:
97
+ return [
98
+ FailedResponse(code=response.status_code, body=response.text, error=ErrorDetails.from_response(response))
99
+ ]
79
100
 
101
+ def create_failed_request(self, error_message: str) -> Sequence[HTTPMessage]:
102
+ return [FailedRequestMessage(message=error_message)]
80
103
 
81
- @dataclass
82
- class SimpleRequest(RequestMessage):
83
- """Base class for requests with a simple success/fail response structure"""
84
104
 
85
- @classmethod
86
- def create_responses(
87
- cls,
88
- response: httpx.Response,
89
- response_body: dict[str, JsonVal] | None = None,
90
- error_message: str | None = None,
91
- ) -> Sequence[ResponseMessage]:
92
- if 200 <= response.status_code < 300 and error_message is None:
93
- return [SuccessResponse(status_code=response.status_code, body=response_body)]
94
- if error_message is None:
95
- error_message = f"Request failed with status code {response.status_code}"
96
- return [FailedResponse(status_code=response.status_code, error=error_message, body=response_body)]
105
+ class ParametersRequest(SimpleRequest):
106
+ """Base class for HTTP request messages with query parameters"""
97
107
 
98
- @classmethod
99
- def create_failed_request(cls, error_message: str) -> Sequence[HTTPMessage]:
100
- return [FailedRequestMessage(error=error_message)]
108
+ parameters: dict[str, PrimaryTypes] | None = None
101
109
 
102
110
 
103
- @dataclass
104
- class BodyRequest(RequestMessage, ABC):
111
+ class BodyRequest(ParametersRequest, ABC):
105
112
  """Base class for HTTP request messages with a body"""
106
113
 
107
114
  @abstractmethod
108
- def body(self) -> dict[str, JsonVal]:
115
+ def data(self) -> str:
109
116
  raise NotImplementedError()
110
117
 
111
118
 
112
- @dataclass
113
- class ParamRequest(SimpleRequest):
114
- """Base class for HTTP request messages with query parameters"""
115
-
116
- parameters: dict[str, str] | None = None
117
-
119
+ class SimpleBodyRequest(BodyRequest):
120
+ body: str
118
121
 
119
- @dataclass
120
- class SimpleBodyRequest(SimpleRequest, BodyRequest):
121
- body_content: dict[str, JsonVal] = field(default_factory=dict)
122
+ def data(self) -> str:
123
+ return self.body
122
124
 
123
- def body(self) -> dict[str, JsonVal]:
124
- return self.body_content
125
125
 
126
-
127
- @dataclass
128
- class ItemMessage:
126
+ class ItemMessage(BaseModel, Generic[T_Reference], ABC):
129
127
  """Base class for message related to a specific item"""
130
128
 
131
- ...
129
+ ids: Sequence[T_Reference]
132
130
 
133
131
 
134
- @dataclass
135
- class ItemIDMessage(Generic[T_ID], ItemMessage, ABC):
136
- """Base class for message related to a specific item identified by an ID"""
132
+ class SuccessResponseItems(ItemMessage[T_Reference], SuccessResponse): ...
137
133
 
138
- id: T_ID
139
134
 
135
+ class FailedResponseItems(ItemMessage[T_Reference], FailedResponse): ...
140
136
 
141
- @dataclass
142
- class ItemResponse(ItemIDMessage, ResponseMessage, ABC): ...
143
137
 
138
+ class FailedRequestItems(ItemMessage[T_Reference], FailedRequestMessage): ...
144
139
 
145
- @dataclass
146
- class SuccessItem(ItemResponse):
147
- item: JsonVal | None = None
148
140
 
141
+ T_BaseModel = TypeVar("T_BaseModel", bound=BaseModel)
149
142
 
150
- @dataclass
151
- class FailedItem(ItemResponse):
152
- error: str
153
143
 
144
+ class ItemBody(BaseModel, Generic[T_Reference, T_BaseModel], ABC):
145
+ items: Sequence[T_BaseModel]
146
+ extra_args: dict[str, JsonValue] | None = None
154
147
 
155
- @dataclass
156
- class MissingItem(ItemResponse): ...
148
+ @model_serializer(mode="plain", return_type=dict)
149
+ def serialize(self) -> dict[str, JsonValue]:
150
+ data: dict[str, JsonValue] = {
151
+ "items": [item.model_dump(exclude_unset=True, by_alias=True) for item in self.items]
152
+ }
153
+ if isinstance(self.extra_args, dict):
154
+ data.update(self.extra_args)
155
+ return data
157
156
 
157
+ @abstractmethod
158
+ def as_ids(self) -> list[T_Reference]:
159
+ """Returns the list of item identifiers for the items in the body."""
160
+ raise NotImplementedError()
158
161
 
159
- @dataclass
160
- class UnexpectedItem(ItemResponse):
161
- item: JsonVal | None = None
162
+ def split(self, mid: int) -> tuple[Self, Self]:
163
+ """Splits the body into two smaller bodies.
162
164
 
165
+ This is useful for retrying requests that fail due to size limits or timeouts.
163
166
 
164
- @dataclass
165
- class FailedRequestItem(ItemIDMessage, FailedRequestMessage): ...
167
+ Args:
168
+ mid: The index at which to split the items.
169
+ Returns:
170
+ A tuple containing two new ItemBody instances, each with half of the original items.
166
171
 
172
+ """
167
173
 
168
- @dataclass
169
- class UnknownRequestItem(ItemMessage, FailedRequestMessage):
170
- item: JsonVal | None = None
174
+ type_ = type(self)
175
+ return type_(items=self.items[:mid], extra_args=self.extra_args), type_(
176
+ items=self.items[mid:], extra_args=self.extra_args
177
+ )
171
178
 
172
179
 
173
- @dataclass
174
- class UnknownResponseItem(ItemMessage, ResponseMessage):
175
- error: str
176
- item: JsonVal | None = None
180
+ class ItemIDBody(ItemBody[ReferenceObject, ReferenceObject]):
181
+ def as_ids(self) -> list[ReferenceObject]:
182
+ return list(self.items)
177
183
 
178
184
 
179
- @dataclass
180
- class ItemsRequest(Generic[T_ID], BodyRequest):
185
+ class ItemsRequest(BodyRequest, Generic[T_Reference, T_BaseModel]):
181
186
  """Requests message for endpoints that accept multiple items in a single request.
182
187
 
183
188
  This class provides functionality to split large requests into smaller ones, handle responses for each item,
184
189
  and manage errors effectively.
185
190
 
186
191
  Attributes:
187
- items (list[JsonVal]): The list of items to be sent in the request body.
188
- extra_body_fields (dict[str, JsonVal]): Additional fields to include in the request body
189
- as_id (Callable[[JsonVal], T_ID] | None): A function to extract the ID from each item. If None,
190
- IDs are not used.
192
+ body (ItemBody): The body of the request containing the items to be processed.
191
193
  max_failures_before_abort (int): The maximum number of failed split requests before aborting further splits.
192
194
 
193
195
  """
194
196
 
195
- items: list[JsonVal] = field(default_factory=list)
196
- extra_body_fields: dict[str, JsonVal] = field(default_factory=dict)
197
- as_id: Callable[[JsonVal], T_ID] | None = None
197
+ model_config = ConfigDict(arbitrary_types_allowed=True)
198
+ body: ItemBody[T_Reference, T_BaseModel]
198
199
  max_failures_before_abort: int = 50
199
- tracker: ItemsRequestTracker | None = field(default=None, init=False)
200
+ tracker: ItemsRequestTracker | None = None
200
201
 
201
- def dump(self) -> dict[str, JsonVal]:
202
- """Dumps the message to a JSON serializable dictionary.
203
-
204
- This override removes the 'as_id' attribute as it is not serializable.
205
-
206
- Returns:
207
- dict[str, JsonVal]: The message as a dictionary.
208
- """
209
- output = super().dump()
210
- if self.as_id is not None:
211
- # We cannot serialize functions
212
- del output["as_id"]
213
- if self.tracker is not None:
214
- # We cannot serialize the tracker
215
- del output["tracker"]
216
- return output
217
-
218
- def body(self) -> dict[str, JsonVal]:
219
- if self.extra_body_fields:
220
- return {"items": self.items, **self.extra_body_fields}
221
- return {"items": self.items}
202
+ def data(self) -> str:
203
+ return self.body.model_dump_json(exclude_unset=True, by_alias=True)
222
204
 
223
205
  def split(self, status_attempts: int) -> "list[ItemsRequest]":
224
206
  """Splits the request into two smaller requests.
@@ -235,155 +217,72 @@ class ItemsRequest(Generic[T_ID], BodyRequest):
235
217
  A list containing two new ItemsRequest instances, each with half of the original items.
236
218
 
237
219
  """
238
- mid = len(self.items) // 2
220
+ mid = len(self.body.items) // 2
239
221
  if mid == 0:
240
222
  return [self]
241
223
  tracker = self.tracker or ItemsRequestTracker(self.max_failures_before_abort)
242
224
  tracker.register_failure()
243
- first_half = ItemsRequest[T_ID](
244
- endpoint_url=self.endpoint_url,
245
- method=self.method,
246
- items=self.items[:mid],
247
- extra_body_fields=self.extra_body_fields,
248
- as_id=self.as_id,
249
- connect_attempt=self.connect_attempt,
250
- read_attempt=self.read_attempt,
251
- status_attempt=status_attempts,
252
- )
253
- first_half.tracker = tracker
254
- second_half = ItemsRequest[T_ID](
255
- endpoint_url=self.endpoint_url,
256
- method=self.method,
257
- items=self.items[mid:],
258
- extra_body_fields=self.extra_body_fields,
259
- as_id=self.as_id,
260
- connect_attempt=self.connect_attempt,
261
- read_attempt=self.read_attempt,
262
- status_attempt=status_attempts,
263
- )
264
- second_half.tracker = tracker
265
- return [first_half, second_half]
266
-
267
- def create_responses(
268
- self,
269
- response: httpx.Response,
270
- response_body: dict[str, JsonVal] | None = None,
271
- error_message: str | None = None,
272
- ) -> Sequence[HTTPMessage]:
225
+ messages: list[ItemsRequest] = []
226
+ for body in self.body.split(mid):
227
+ item_request = ItemsRequest(
228
+ endpoint_url=self.endpoint_url,
229
+ method=self.method,
230
+ body=body,
231
+ connect_attempt=self.connect_attempt,
232
+ read_attempt=self.read_attempt,
233
+ status_attempt=status_attempts,
234
+ )
235
+ item_request.tracker = tracker
236
+ messages.append(item_request)
237
+ return messages
238
+
239
+ def create_success_response(self, response: httpx.Response) -> Sequence[HTTPMessage]:
240
+ return [SuccessResponseItems(code=response.status_code, body=response.text, ids=self.body.as_ids())]
241
+
242
+ def create_failure_response(self, response: httpx.Response) -> Sequence[HTTPMessage]:
273
243
  """Creates response messages based on the HTTP response and the original request.
274
244
 
275
245
  Args:
276
246
  response: The HTTP response received from the server.
277
- response_body: The parsed JSON body of the response, if available.
278
- error_message: An optional error message to use if the response indicates a failure.
279
-
280
247
  Returns:
281
248
  A sequence of HTTPMessage instances representing the outcome for each item in the request.
282
249
  """
283
- if self.as_id is None:
284
- return SimpleBodyRequest.create_responses(response, response_body, error_message)
285
- request_items_by_id, errors = self._create_items_by_id()
286
- responses: list[HTTPMessage] = list(errors)
287
- error_message = error_message or "Unknown error"
288
-
289
- if not self._is_items_response(response_body):
290
- return self._handle_non_items_response(responses, response, error_message, request_items_by_id)
291
-
292
- # Process items from response
293
- if response_body is not None:
294
- self._process_response_items(responses, response, response_body, error_message, request_items_by_id)
295
-
296
- # Handle missing items
297
- self._handle_missing_items(responses, response, request_items_by_id)
298
-
299
- return responses
300
-
301
- @staticmethod
302
- def _handle_non_items_response(
303
- responses: list[HTTPMessage],
304
- response: httpx.Response,
305
- error_message: str,
306
- request_items_by_id: dict[T_ID, JsonVal],
307
- ) -> list[HTTPMessage]:
308
- """Handles responses that do not contain an 'items' field in the body."""
309
- if 200 <= response.status_code < 300:
310
- responses.extend(
311
- SuccessItem(status_code=response.status_code, id=id_) for id_ in request_items_by_id.keys()
312
- )
313
- else:
314
- responses.extend(
315
- FailedItem(status_code=response.status_code, error=error_message, id=id_)
316
- for id_ in request_items_by_id.keys()
250
+ return [
251
+ FailedResponseItems(
252
+ code=response.status_code,
253
+ body=response.text,
254
+ error=ErrorDetails.from_response(response),
255
+ ids=self.body.as_ids(),
317
256
  )
318
- return responses
319
-
320
- def _process_response_items(
321
- self,
322
- responses: list[HTTPMessage],
323
- response: httpx.Response,
324
- response_body: dict[str, JsonVal],
325
- error_message: str,
326
- request_items_by_id: dict[T_ID, JsonVal],
327
- ) -> None:
328
- """Processes each item in the response body and categorizes them based on their status."""
329
- for response_item in response_body["items"]: # type: ignore[union-attr]
330
- try:
331
- item_id = self.as_id(response_item) # type: ignore[misc]
332
- except Exception as e:
333
- responses.append(
334
- UnknownResponseItem(
335
- status_code=response.status_code, item=response_item, error=f"Error extracting ID: {e!s}"
336
- )
337
- )
338
- continue
339
- request_item = request_items_by_id.pop(item_id, None)
340
- if request_item is None:
341
- responses.append(UnexpectedItem(status_code=response.status_code, id=item_id, item=response_item))
342
- elif 200 <= response.status_code < 300:
343
- responses.append(SuccessItem(status_code=response.status_code, id=item_id, item=response_item))
344
- else:
345
- responses.append(FailedItem(status_code=response.status_code, id=item_id, error=error_message))
346
-
347
- @staticmethod
348
- def _handle_missing_items(
349
- responses: list[HTTPMessage], response: httpx.Response, request_items_by_id: dict[T_ID, JsonVal]
350
- ) -> None:
351
- """Handles items that were in the request but not present in the response."""
352
- for item_id in request_items_by_id.keys():
353
- responses.append(MissingItem(status_code=response.status_code, id=item_id))
257
+ ]
354
258
 
355
259
  def create_failed_request(self, error_message: str) -> Sequence[HTTPMessage]:
356
- if self.as_id is None:
357
- return SimpleBodyRequest.create_failed_request(error_message)
358
- items_by_id, errors = self._create_items_by_id()
359
- results: list[HTTPMessage] = []
360
- results.extend(errors)
361
- results.extend(FailedRequestItem(id=item_id, error=error_message) for item_id in items_by_id.keys())
362
- return results
363
-
364
- def _create_items_by_id(self) -> tuple[dict[T_ID, JsonVal], list[FailedRequestItem | UnknownRequestItem]]:
365
- if self.as_id is None:
366
- raise ValueError("as_id function must be provided to create items by ID")
367
- items_by_id: dict[T_ID, JsonVal] = {}
368
- errors: list[FailedRequestItem | UnknownRequestItem] = []
369
- for item in self.items:
370
- try:
371
- item_id = self.as_id(item)
372
- except Exception as e:
373
- errors.append(UnknownRequestItem(error=f"Error extracting ID: {e!s}", item=item))
374
- continue
375
- if item_id in items_by_id:
376
- errors.append(FailedRequestItem(id=item_id, error=f"Duplicate item ID: {item_id!r}"))
377
- else:
378
- items_by_id[item_id] = item
379
- return items_by_id, errors
380
-
381
- @staticmethod
382
- def _is_items_response(body: dict[str, JsonVal] | None = None) -> bool:
383
- if body is None:
384
- return False
385
- if "items" not in body:
386
- return False
387
- if not isinstance(body["items"], list):
388
- return False
389
- return True
260
+ """Creates failed request messages for each item in the request.
261
+
262
+ Args:
263
+ error_message: The error message to include in the failed request messages.
264
+
265
+ Returns:
266
+ A sequence of HTTPMessage instances representing the failed request for each item.
267
+ """
268
+ return [FailedRequestItems(message=error_message, ids=self.body.as_ids())]
269
+
270
+
271
+ class APIResponse(UserList, MutableSequence[ResponseMessage | FailedRequestMessage]):
272
+ def __init__(self, collection: Sequence[ResponseMessage | FailedRequestMessage] | None = None):
273
+ super().__init__(collection or [])
274
+
275
+ def raise_for_status(self) -> None:
276
+ error_messages = [message for message in self.data if not isinstance(message, SuccessResponse)]
277
+ if error_messages:
278
+ raise CDFAPIException(error_messages)
279
+
280
+ @property
281
+ def success_response(self) -> SuccessResponse:
282
+ success = [msg for msg in self.data if isinstance(msg, SuccessResponse)]
283
+ if len(success) == 1:
284
+ return success[0]
285
+ elif success:
286
+ raise ValueError("Multiple successful HTTP responses found in the messages.")
287
+ else:
288
+ raise ValueError("No successful HTTP response found in the messages.")
@@ -1,3 +1,4 @@
1
+ import re
1
2
  from collections.abc import Collection
2
3
  from typing import Any
3
4
 
@@ -60,3 +61,8 @@ def title_case(s: str) -> str:
60
61
  'Hello World'
61
62
  """
62
63
  return " ".join(word.capitalize() for word in s.replace("_", " ").replace("-", " ").split())
64
+
65
+
66
+ def split_on_capitals(text: str) -> list[str]:
67
+ """Split a string at capital letters."""
68
+ return re.findall(r"[A-Z][a-z]*", text)
@@ -2,13 +2,34 @@ from collections.abc import Hashable
2
2
  from datetime import date, datetime, time, timedelta
3
3
  from typing import TypeAlias, TypeVar
4
4
 
5
- JsonVal: TypeAlias = None | str | int | float | bool | dict[str, "JsonVal"] | list["JsonVal"]
5
+ from pydantic import BaseModel
6
+ from pydantic.alias_generators import to_camel
6
7
 
8
+ JsonVal: TypeAlias = None | str | int | float | bool | dict[str, "JsonVal"] | list["JsonVal"]
9
+ PrimaryTypes: TypeAlias = str | int | float | bool
7
10
 
8
11
  T_ID = TypeVar("T_ID", bound=Hashable)
9
-
10
12
  # These are the types that openpyxl supports in cells
11
13
  CellValueType: TypeAlias = str | int | float | bool | datetime | date | time | timedelta | None
12
14
 
13
15
  # The format expected for excel sheets representing a data model
14
16
  DataModelTableType: TypeAlias = dict[str, list[dict[str, CellValueType]]]
17
+ PrimitiveType: TypeAlias = str | int | float | bool
18
+
19
+
20
+ class BaseModelObject(BaseModel, alias_generator=to_camel, extra="ignore"):
21
+ """Base class for all object. This includes resources and nested objects."""
22
+
23
+ ...
24
+
25
+
26
+ T_Item = TypeVar("T_Item", bound=BaseModelObject)
27
+
28
+
29
+ class ReferenceObject(BaseModelObject, frozen=True, populate_by_name=True):
30
+ """Base class for all reference objects - these are identifiers."""
31
+
32
+ ...
33
+
34
+
35
+ T_Reference = TypeVar("T_Reference", bound=ReferenceObject, covariant=True)
cognite/neat/_version.py CHANGED
@@ -1,2 +1,2 @@
1
- __version__ = "0.126.0"
1
+ __version__ = "0.126.1"
2
2
  __engine__ = "^2.0.4"
@@ -67,13 +67,13 @@ def parse_concepts(
67
67
  else:
68
68
  # Handling implements
69
69
  if concepts[concept_id]["implements"] and isinstance(concepts[concept_id]["implements"], list):
70
- if res["implements"] not in concepts[concept_id]["implements"]:
70
+ if res["implements"] and res["implements"] not in concepts[concept_id]["implements"]:
71
71
  concepts[concept_id]["implements"].append(res["implements"])
72
72
 
73
73
  elif concepts[concept_id]["implements"] and isinstance(concepts[concept_id]["implements"], str):
74
74
  concepts[concept_id]["implements"] = [concepts[concept_id]["implements"]]
75
75
 
76
- if res["implements"] not in concepts[concept_id]["implements"]:
76
+ if res["implements"] and res["implements"] not in concepts[concept_id]["implements"]:
77
77
  concepts[concept_id]["implements"].append(res["implements"])
78
78
  elif res["implements"]:
79
79
  concepts[concept_id]["implements"] = [res["implements"]]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cognite-neat
3
- Version: 0.126.0
3
+ Version: 0.126.1
4
4
  Summary: Knowledge graph transformation
5
5
  Project-URL: Documentation, https://cognite-neat.readthedocs-hosted.com/
6
6
  Project-URL: Homepage, https://cognite-neat.readthedocs-hosted.com/