cognite-neat 0.125.1__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.
- cognite/neat/_client/__init__.py +4 -0
- cognite/neat/_client/api.py +8 -0
- cognite/neat/_client/client.py +19 -0
- cognite/neat/_client/config.py +40 -0
- cognite/neat/_client/containers_api.py +73 -0
- cognite/neat/_client/data_classes.py +10 -0
- cognite/neat/_client/data_model_api.py +63 -0
- cognite/neat/_client/spaces_api.py +67 -0
- cognite/neat/_client/views_api.py +82 -0
- cognite/neat/_data_model/_analysis.py +127 -0
- cognite/neat/_data_model/_constants.py +59 -0
- cognite/neat/_data_model/_shared.py +46 -0
- cognite/neat/_data_model/deployer/__init__.py +0 -0
- cognite/neat/_data_model/deployer/_differ.py +113 -0
- cognite/neat/_data_model/deployer/_differ_container.py +354 -0
- cognite/neat/_data_model/deployer/_differ_data_model.py +29 -0
- cognite/neat/_data_model/deployer/_differ_space.py +9 -0
- cognite/neat/_data_model/deployer/_differ_view.py +194 -0
- cognite/neat/_data_model/deployer/data_classes.py +176 -0
- cognite/neat/_data_model/exporters/__init__.py +4 -0
- cognite/neat/_data_model/exporters/_base.py +22 -0
- cognite/neat/_data_model/exporters/_table_exporter/__init__.py +0 -0
- cognite/neat/_data_model/exporters/_table_exporter/exporter.py +106 -0
- cognite/neat/_data_model/exporters/_table_exporter/workbook.py +414 -0
- cognite/neat/_data_model/exporters/_table_exporter/writer.py +391 -0
- cognite/neat/_data_model/importers/__init__.py +2 -1
- cognite/neat/_data_model/importers/_api_importer.py +88 -0
- cognite/neat/_data_model/importers/_table_importer/data_classes.py +48 -8
- cognite/neat/_data_model/importers/_table_importer/importer.py +102 -6
- cognite/neat/_data_model/importers/_table_importer/reader.py +860 -0
- cognite/neat/_data_model/models/dms/__init__.py +19 -1
- cognite/neat/_data_model/models/dms/_base.py +12 -8
- cognite/neat/_data_model/models/dms/_constants.py +1 -1
- cognite/neat/_data_model/models/dms/_constraints.py +2 -1
- cognite/neat/_data_model/models/dms/_container.py +5 -5
- cognite/neat/_data_model/models/dms/_data_model.py +3 -3
- cognite/neat/_data_model/models/dms/_data_types.py +8 -1
- cognite/neat/_data_model/models/dms/_http.py +18 -0
- cognite/neat/_data_model/models/dms/_indexes.py +2 -1
- cognite/neat/_data_model/models/dms/_references.py +17 -4
- cognite/neat/_data_model/models/dms/_space.py +11 -7
- cognite/neat/_data_model/models/dms/_view_property.py +7 -4
- cognite/neat/_data_model/models/dms/_views.py +16 -6
- cognite/neat/_data_model/validation/__init__.py +0 -0
- cognite/neat/_data_model/validation/_base.py +16 -0
- cognite/neat/_data_model/validation/dms/__init__.py +9 -0
- cognite/neat/_data_model/validation/dms/_orchestrator.py +68 -0
- cognite/neat/_data_model/validation/dms/_validators.py +139 -0
- cognite/neat/_exceptions.py +15 -3
- cognite/neat/_issues.py +39 -6
- cognite/neat/_session/__init__.py +3 -0
- cognite/neat/_session/_physical.py +88 -0
- cognite/neat/_session/_session.py +34 -25
- cognite/neat/_session/_wrappers.py +61 -0
- cognite/neat/_state_machine/__init__.py +10 -0
- cognite/neat/{_session/_state_machine → _state_machine}/_base.py +11 -1
- cognite/neat/_state_machine/_states.py +53 -0
- cognite/neat/_store/__init__.py +3 -0
- cognite/neat/_store/_provenance.py +55 -0
- cognite/neat/_store/_store.py +124 -0
- cognite/neat/_utils/_reader.py +194 -0
- cognite/neat/_utils/http_client/__init__.py +14 -20
- cognite/neat/_utils/http_client/_client.py +22 -61
- cognite/neat/_utils/http_client/_data_classes.py +167 -268
- cognite/neat/_utils/text.py +6 -0
- cognite/neat/_utils/useful_types.py +26 -2
- cognite/neat/_version.py +1 -1
- cognite/neat/v0/core/_data_model/importers/_rdf/_shared.py +2 -2
- cognite/neat/v0/core/_data_model/importers/_spreadsheet2data_model.py +2 -2
- cognite/neat/v0/core/_data_model/models/entities/_single_value.py +1 -1
- cognite/neat/v0/core/_data_model/models/physical/_unverified.py +1 -1
- cognite/neat/v0/core/_data_model/models/physical/_validation.py +2 -2
- cognite/neat/v0/core/_data_model/models/physical/_verified.py +3 -3
- cognite/neat/v0/core/_data_model/transformers/_converters.py +1 -1
- {cognite_neat-0.125.1.dist-info → cognite_neat-0.126.1.dist-info}/METADATA +1 -1
- {cognite_neat-0.125.1.dist-info → cognite_neat-0.126.1.dist-info}/RECORD +78 -40
- cognite/neat/_session/_state_machine/__init__.py +0 -23
- cognite/neat/_session/_state_machine/_states.py +0 -150
- {cognite_neat-0.125.1.dist-info → cognite_neat-0.126.1.dist-info}/WHEEL +0 -0
- {cognite_neat-0.125.1.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
|
|
3
|
-
from
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
27
|
+
message: str
|
|
34
28
|
|
|
35
29
|
|
|
36
|
-
@dataclass
|
|
37
30
|
class ResponseMessage(HTTPMessage):
|
|
38
|
-
|
|
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
|
-
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
71
|
-
class
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
115
|
+
def data(self) -> str:
|
|
109
116
|
raise NotImplementedError()
|
|
110
117
|
|
|
111
118
|
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
@
|
|
156
|
-
|
|
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
|
-
|
|
160
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
196
|
-
|
|
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 =
|
|
200
|
+
tracker: ItemsRequestTracker | None = None
|
|
200
201
|
|
|
201
|
-
def
|
|
202
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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.")
|
cognite/neat/_utils/text.py
CHANGED
|
@@ -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,10 +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
|
-
|
|
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
|
|
14
|
+
|
|
15
|
+
# The format expected for excel sheets representing a data model
|
|
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.
|
|
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"]]
|
|
@@ -377,7 +377,7 @@ def _replace_legacy_constraint_form(sheet: Worksheet) -> None:
|
|
|
377
377
|
constraints = []
|
|
378
378
|
for constraint in SPLIT_ON_COMMA_PATTERN.split(str(cell.value)):
|
|
379
379
|
# latest format, do nothing
|
|
380
|
-
if "
|
|
380
|
+
if "require" in constraint.lower():
|
|
381
381
|
constraints.append(constraint)
|
|
382
382
|
continue
|
|
383
383
|
|
|
@@ -385,7 +385,7 @@ def _replace_legacy_constraint_form(sheet: Worksheet) -> None:
|
|
|
385
385
|
container = ContainerEntity.load(constraint, space="default")
|
|
386
386
|
container_str = container.external_id if container.space == "default" else str(container)
|
|
387
387
|
constraints.append(
|
|
388
|
-
f"requires:{container.space}_{container.external_id}(
|
|
388
|
+
f"requires:{container.space}_{container.external_id}(require={container_str})"
|
|
389
389
|
)
|
|
390
390
|
replaced = True
|
|
391
391
|
except ValidationError:
|
|
@@ -666,7 +666,7 @@ class ContainerConstraintEntity(PhysicalEntity[None]):
|
|
|
666
666
|
type_: ClassVar[EntityTypes] = EntityTypes.container_constraint
|
|
667
667
|
prefix: _UndefinedType | Literal["uniqueness", "requires"] = Undefined # type: ignore[assignment]
|
|
668
668
|
suffix: str
|
|
669
|
-
|
|
669
|
+
require: ContainerEntity | None = None
|
|
670
670
|
|
|
671
671
|
def as_id(self) -> None:
|
|
672
672
|
return None
|
|
@@ -285,7 +285,7 @@ class UnverifiedPhysicalContainer(UnverifiedComponent[PhysicalContainer]):
|
|
|
285
285
|
for constraint_name, constraint_obj in (container.constraints or {}).items():
|
|
286
286
|
if isinstance(constraint_obj, dm.RequiresConstraint):
|
|
287
287
|
constraint = ContainerConstraintEntity(
|
|
288
|
-
prefix="requires", suffix=constraint_name,
|
|
288
|
+
prefix="requires", suffix=constraint_name, require=ContainerEntity.from_id(constraint_obj.require)
|
|
289
289
|
)
|
|
290
290
|
constraints.append(str(constraint))
|
|
291
291
|
|
|
@@ -123,8 +123,8 @@ class PhysicalValidation:
|
|
|
123
123
|
|
|
124
124
|
for container in self._containers or []:
|
|
125
125
|
for constraint in container.constraint or []:
|
|
126
|
-
if constraint.
|
|
127
|
-
imported_containers.add(cast(ContainerEntity, constraint.
|
|
126
|
+
if constraint.require not in existing_containers:
|
|
127
|
+
imported_containers.add(cast(ContainerEntity, constraint.require))
|
|
128
128
|
|
|
129
129
|
if include_views_with_no_properties:
|
|
130
130
|
extra_views = existing_views - view_with_properties
|