cognite-toolkit 0.7.43__py3-none-any.whl → 0.7.45__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.
Files changed (57) hide show
  1. cognite_toolkit/_cdf_tk/client/_toolkit_client.py +6 -0
  2. cognite_toolkit/_cdf_tk/client/api/assets.py +2 -2
  3. cognite_toolkit/_cdf_tk/client/api/events.py +2 -2
  4. cognite_toolkit/_cdf_tk/client/api/filemetadata.py +150 -0
  5. cognite_toolkit/_cdf_tk/client/api/raw.py +174 -0
  6. cognite_toolkit/_cdf_tk/client/api/simulator_models.py +128 -0
  7. cognite_toolkit/_cdf_tk/client/api/simulators.py +8 -0
  8. cognite_toolkit/_cdf_tk/client/api/timeseries.py +2 -2
  9. cognite_toolkit/_cdf_tk/client/cdf_client/__init__.py +2 -1
  10. cognite_toolkit/_cdf_tk/client/cdf_client/api.py +114 -8
  11. cognite_toolkit/_cdf_tk/client/data_classes/annotation.py +79 -0
  12. cognite_toolkit/_cdf_tk/client/data_classes/base.py +13 -3
  13. cognite_toolkit/_cdf_tk/client/data_classes/data_modeling/__init__.py +16 -0
  14. cognite_toolkit/_cdf_tk/client/data_classes/data_modeling/_instance.py +143 -0
  15. cognite_toolkit/_cdf_tk/client/data_classes/data_modeling/_references.py +8 -0
  16. cognite_toolkit/_cdf_tk/client/data_classes/dataset.py +35 -0
  17. cognite_toolkit/_cdf_tk/client/data_classes/extraction_pipeline.py +59 -0
  18. cognite_toolkit/_cdf_tk/client/data_classes/filemetadata.py +7 -1
  19. cognite_toolkit/_cdf_tk/client/data_classes/hosted_extractor_destination.py +34 -0
  20. cognite_toolkit/_cdf_tk/client/data_classes/hosted_extractor_job.py +134 -0
  21. cognite_toolkit/_cdf_tk/client/data_classes/hosted_extractor_mapping.py +72 -0
  22. cognite_toolkit/_cdf_tk/client/data_classes/hosted_extractor_source/__init__.py +63 -0
  23. cognite_toolkit/_cdf_tk/client/data_classes/hosted_extractor_source/_auth.py +63 -0
  24. cognite_toolkit/_cdf_tk/client/data_classes/hosted_extractor_source/_base.py +26 -0
  25. cognite_toolkit/_cdf_tk/client/data_classes/hosted_extractor_source/_certificate.py +20 -0
  26. cognite_toolkit/_cdf_tk/client/data_classes/hosted_extractor_source/_eventhub.py +31 -0
  27. cognite_toolkit/_cdf_tk/client/data_classes/hosted_extractor_source/_kafka.py +53 -0
  28. cognite_toolkit/_cdf_tk/client/data_classes/hosted_extractor_source/_mqtt.py +36 -0
  29. cognite_toolkit/_cdf_tk/client/data_classes/hosted_extractor_source/_rest.py +49 -0
  30. cognite_toolkit/_cdf_tk/client/data_classes/identifiers.py +8 -0
  31. cognite_toolkit/_cdf_tk/client/data_classes/label.py +27 -0
  32. cognite_toolkit/_cdf_tk/client/data_classes/raw.py +3 -2
  33. cognite_toolkit/_cdf_tk/client/data_classes/securitycategory.py +24 -0
  34. cognite_toolkit/_cdf_tk/client/data_classes/sequence.py +45 -0
  35. cognite_toolkit/_cdf_tk/client/data_classes/transformation.py +140 -0
  36. cognite_toolkit/_cdf_tk/client/data_classes/workflow.py +27 -0
  37. cognite_toolkit/_cdf_tk/client/data_classes/workflow_trigger.py +63 -0
  38. cognite_toolkit/_cdf_tk/client/data_classes/workflow_version.py +155 -0
  39. cognite_toolkit/_cdf_tk/client/testing.py +5 -0
  40. cognite_toolkit/_cdf_tk/commands/_migrate/conversion.py +4 -3
  41. cognite_toolkit/_cdf_tk/cruds/__init__.py +5 -1
  42. cognite_toolkit/_cdf_tk/cruds/_data_cruds.py +7 -3
  43. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/__init__.py +2 -0
  44. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/file.py +56 -59
  45. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/relationship.py +1 -1
  46. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/simulators.py +119 -0
  47. cognite_toolkit/_cdf_tk/feature_flags.py +4 -0
  48. cognite_toolkit/_cdf_tk/storageio/_asset_centric.py +34 -29
  49. cognite_toolkit/_cdf_tk/storageio/_file_content.py +22 -19
  50. cognite_toolkit/_repo_files/GitHub/.github/workflows/deploy.yaml +1 -1
  51. cognite_toolkit/_repo_files/GitHub/.github/workflows/dry-run.yaml +1 -1
  52. cognite_toolkit/_resources/cdf.toml +1 -1
  53. cognite_toolkit/_version.py +1 -1
  54. {cognite_toolkit-0.7.43.dist-info → cognite_toolkit-0.7.45.dist-info}/METADATA +11 -1
  55. {cognite_toolkit-0.7.43.dist-info → cognite_toolkit-0.7.45.dist-info}/RECORD +57 -30
  56. {cognite_toolkit-0.7.43.dist-info → cognite_toolkit-0.7.45.dist-info}/WHEEL +1 -1
  57. {cognite_toolkit-0.7.43.dist-info → cognite_toolkit-0.7.45.dist-info}/entry_points.txt +0 -0
@@ -5,6 +5,7 @@ that handle CRUD operations for CDF Data Modeling API resources.
5
5
  """
6
6
 
7
7
  from abc import ABC, abstractmethod
8
+ from collections import defaultdict
8
9
  from collections.abc import Callable, Iterable, Sequence
9
10
  from dataclasses import dataclass
10
11
  from functools import partial
@@ -18,7 +19,13 @@ from cognite_toolkit._cdf_tk.client.data_classes.base import (
18
19
  T_RequestResource,
19
20
  T_ResponseResource,
20
21
  )
21
- from cognite_toolkit._cdf_tk.client.http_client import HTTPClient, RequestMessage2, SuccessResponse2
22
+ from cognite_toolkit._cdf_tk.client.http_client import (
23
+ HTTPClient,
24
+ ItemsRequest2,
25
+ ItemsSuccessResponse2,
26
+ RequestMessage2,
27
+ SuccessResponse2,
28
+ )
22
29
  from cognite_toolkit._cdf_tk.utils.collection import chunker_sequence
23
30
 
24
31
  from .responses import PagedResponse, ResponseItems
@@ -67,7 +74,7 @@ class CDFResourceAPI(Generic[T_Identifier, T_RequestResource, T_ResponseResource
67
74
  return [item.as_update(mode=mode) for item in items]
68
75
 
69
76
  @abstractmethod
70
- def _page_response(self, response: SuccessResponse2) -> PagedResponse[T_ResponseResource]:
77
+ def _page_response(self, response: SuccessResponse2 | ItemsSuccessResponse2) -> PagedResponse[T_ResponseResource]:
71
78
  """Parse a single item response."""
72
79
  raise NotImplementedError()
73
80
 
@@ -97,9 +104,10 @@ class CDFResourceAPI(Generic[T_Identifier, T_RequestResource, T_ResponseResource
97
104
  method: APIMethod,
98
105
  params: dict[str, Any] | None = None,
99
106
  extra_body: dict[str, Any] | None = None,
107
+ endpoint: str | None = None,
100
108
  ) -> list[T_ResponseResource]:
101
109
  response_items: list[T_ResponseResource] = []
102
- for response in self._chunk_requests(items, method, self._serialize_items, params, extra_body):
110
+ for response in self._chunk_requests(items, method, self._serialize_items, params, extra_body, endpoint):
103
111
  response_items.extend(self._page_response(response).items)
104
112
  return response_items
105
113
 
@@ -121,8 +129,9 @@ class CDFResourceAPI(Generic[T_Identifier, T_RequestResource, T_ResponseResource
121
129
  method: APIMethod,
122
130
  params: dict[str, Any] | None = None,
123
131
  extra_body: dict[str, Any] | None = None,
132
+ endpoint: str | None = None,
124
133
  ) -> None:
125
- list(self._chunk_requests(items, method, self._serialize_items, params, extra_body))
134
+ list(self._chunk_requests(items, method, self._serialize_items, params, extra_body, endpoint))
126
135
  return None
127
136
 
128
137
  def _chunk_requests(
@@ -132,14 +141,28 @@ class CDFResourceAPI(Generic[T_Identifier, T_RequestResource, T_ResponseResource
132
141
  serialization: Callable[[Sequence[_T_BaseModel]], list[dict[str, JsonValue]]],
133
142
  params: dict[str, Any] | None = None,
134
143
  extra_body: dict[str, Any] | None = None,
144
+ endpoint_path: str | None = None,
135
145
  ) -> Iterable[SuccessResponse2]:
146
+ """Send requests in chunks and yield responses.
147
+
148
+ Args:
149
+ items: The items to process.
150
+ method: The API method to use. This is used ot look the up the endpoint.
151
+ serialization: A function to serialize the items to JSON-compatible dicts.
152
+ params: Optional query parameters for the request.
153
+ extra_body: Optional extra body content to include in the request.
154
+ endpoint_path: Optional override for the endpoint path.
155
+
156
+ Yields:
157
+ The successful responses from the API.
158
+ """
136
159
  # Filter out None
137
160
  request_params = self._filter_out_none_values(params)
138
161
  endpoint = self._method_endpoint_map[method]
139
162
 
140
163
  for chunk in chunker_sequence(items, endpoint.item_limit):
141
164
  request = RequestMessage2(
142
- endpoint_url=f"{self._make_url(endpoint.path)}",
165
+ endpoint_url=f"{self._make_url(endpoint_path or endpoint.path)}",
143
166
  method=endpoint.method,
144
167
  body_content={"items": serialization(chunk), **(extra_body or {})}, # type: ignore[dict-item]
145
168
  parameters=request_params,
@@ -147,6 +170,72 @@ class CDFResourceAPI(Generic[T_Identifier, T_RequestResource, T_ResponseResource
147
170
  response = self._http_client.request_single_retries(request)
148
171
  yield response.get_success_or_raise()
149
172
 
173
+ def _request_item_split_retries(
174
+ self,
175
+ items: Sequence[_T_BaseModel],
176
+ method: APIMethod,
177
+ params: dict[str, Any] | None = None,
178
+ extra_body: dict[str, Any] | None = None,
179
+ ) -> list[T_ResponseResource]:
180
+ """Request items with retries, splitting on failures.
181
+
182
+ This method handles large batches of items by chunking them according to the endpoint's item limit.
183
+ If a single item fails, it splits the request into individual item requests to isolate the failure.
184
+
185
+ Args:
186
+ items: Sequence of items to request.
187
+ method: API method to use for the request.
188
+ params: Optional query parameters for the request.
189
+ extra_body: Optional additional body fields for the request.
190
+ Returns:
191
+ List of response items.
192
+ """
193
+ response_items: list[T_ResponseResource] = []
194
+ for response in self._chunk_requests_items_split_retries(items, method, params, extra_body):
195
+ response_items.extend(self._page_response(response).items)
196
+ return response_items
197
+
198
+ def _chunk_requests_items_split_retries(
199
+ self,
200
+ items: Sequence[_T_BaseModel],
201
+ method: APIMethod,
202
+ params: dict[str, Any] | None = None,
203
+ extra_body: dict[str, Any] | None = None,
204
+ ) -> Iterable[ItemsSuccessResponse2]:
205
+ """Request items with retries and splitting on failures.
206
+
207
+ This method handles large batches of items by chunking them according to the endpoint's item limit.
208
+ If a single item fails, it splits the request into individual item requests to isolate the failure.
209
+
210
+ This can be useful to create ignore unknown IDs behavior when retrieving items for API endpoints that
211
+ do not support it natively.
212
+
213
+ Args:
214
+ items: Sequence of items to request.
215
+ method: API method to use for the request.
216
+ params: Optional query parameters for the request.
217
+ extra_body: Optional additional body fields for the request.
218
+ Yields
219
+ SuccessResponse2: Successful responses from the API. All failed items are skipped.
220
+
221
+ """
222
+ # Filter out None
223
+ request_params = self._filter_out_none_values(params)
224
+ endpoint = self._method_endpoint_map[method]
225
+
226
+ for chunk in chunker_sequence(items, endpoint.item_limit):
227
+ request = ItemsRequest2(
228
+ endpoint_url=f"{self._make_url(endpoint.path)}",
229
+ method=endpoint.method,
230
+ parameters=request_params,
231
+ items=chunk,
232
+ extra_body_fields=extra_body,
233
+ )
234
+ responses = self._http_client.request_items_retries(request)
235
+ for response in responses:
236
+ if isinstance(response, ItemsSuccessResponse2):
237
+ yield response
238
+
150
239
  @classmethod
151
240
  def _filter_out_none_values(cls, params: dict[str, Any] | None) -> dict[str, Any] | None:
152
241
  request_params: dict[str, Any] | None = None
@@ -154,12 +243,24 @@ class CDFResourceAPI(Generic[T_Identifier, T_RequestResource, T_ResponseResource
154
243
  request_params = {k: v for k, v in params.items() if v is not None}
155
244
  return request_params
156
245
 
246
+ @classmethod
247
+ def _group_items_by_text_field(
248
+ cls, items: Sequence[_T_BaseModel], field_name: str
249
+ ) -> dict[str, list[_T_BaseModel]]:
250
+ """Group items by a text field."""
251
+ grouped_items: dict[str, list[_T_BaseModel]] = defaultdict(list)
252
+ for item in items:
253
+ key = str(getattr(item, field_name))
254
+ grouped_items[key].append(item)
255
+ return grouped_items
256
+
157
257
  def _iterate(
158
258
  self,
159
259
  limit: int,
160
260
  cursor: str | None = None,
161
261
  params: dict[str, Any] | None = None,
162
262
  body: dict[str, Any] | None = None,
263
+ endpoint_path: str | None = None,
163
264
  ) -> PagedResponse[T_ResponseResource]:
164
265
  """Fetch a single page of resources.
165
266
 
@@ -171,8 +272,11 @@ class CDFResourceAPI(Generic[T_Identifier, T_RequestResource, T_ResponseResource
171
272
  - space: Filter by space
172
273
  - includeGlobal: Whether to include global resources
173
274
  body : Body content for the request, if applicable.
275
+ limit: Maximum number of items to return in the page.
276
+ cursor: Cursor for pagination.
174
277
  limit: Maximum number of items to return in the page.
175
278
  cursor: Cursor for pagination.
279
+
176
280
  Returns:
177
281
  A Page containing the items and the cursor for the next page.
178
282
  """
@@ -194,7 +298,7 @@ class CDFResourceAPI(Generic[T_Identifier, T_RequestResource, T_ResponseResource
194
298
  raise NotImplementedError(f"Unsupported method {endpoint.method} for pagination.")
195
299
 
196
300
  request = RequestMessage2(
197
- endpoint_url=self._make_url(endpoint.path),
301
+ endpoint_url=self._make_url(endpoint_path or endpoint.path),
198
302
  method=endpoint.method,
199
303
  parameters=request_params,
200
304
  body_content=body,
@@ -203,7 +307,9 @@ class CDFResourceAPI(Generic[T_Identifier, T_RequestResource, T_ResponseResource
203
307
  response = result.get_success_or_raise()
204
308
  return self._page_response(response)
205
309
 
206
- def _list(self, limit: int | None = None, params: dict[str, Any] | None = None) -> list[T_ResponseResource]:
310
+ def _list(
311
+ self, limit: int | None = None, params: dict[str, Any] | None = None, endpoint_path: str | None = None
312
+ ) -> list[T_ResponseResource]:
207
313
  """List all resources, handling pagination automatically."""
208
314
  all_items: list[T_ResponseResource] = []
209
315
  next_cursor: str | None = None
@@ -211,7 +317,7 @@ class CDFResourceAPI(Generic[T_Identifier, T_RequestResource, T_ResponseResource
211
317
  endpoint = self._method_endpoint_map["list"]
212
318
  while True:
213
319
  page_limit = endpoint.item_limit if limit is None else min(limit - total, endpoint.item_limit)
214
- page = self._iterate(limit=page_limit, cursor=next_cursor, params=params)
320
+ page = self._iterate(limit=page_limit, cursor=next_cursor, params=params, endpoint_path=endpoint_path)
215
321
  all_items.extend(page.items)
216
322
  total += len(page.items)
217
323
  if page.next_cursor is None or (limit is not None and total >= limit):
@@ -0,0 +1,79 @@
1
+ from typing import Any, Literal, TypeAlias
2
+
3
+ from pydantic import Field, JsonValue
4
+
5
+ from cognite_toolkit._cdf_tk.client.data_classes.base import BaseModelObject, RequestUpdateable, ResponseResource
6
+
7
+ from .identifiers import InternalId
8
+
9
+ AnnotationStatus: TypeAlias = Literal["suggested", "rejected", "approved"]
10
+ AnnotationType: TypeAlias = Literal[
11
+ "diagrams.AssetLink",
12
+ "diagrams.FileLink",
13
+ "diagrams.InstanceLink",
14
+ "diagrams.Junction",
15
+ "diagrams.Line",
16
+ "diagrams.UnhandledSymbolObject",
17
+ "diagrams.UnhandledTextObject",
18
+ "documents.ExtractedText",
19
+ "forms.Detection",
20
+ "images.AssetLink",
21
+ "images.Classification",
22
+ "images.InstanceLink",
23
+ "images.KeypointCollection",
24
+ "images.ObjectDetection",
25
+ "images.TextRegion",
26
+ "isoplan.IsoPlanAnnotation",
27
+ "pointcloud.BoundingVolume",
28
+ ]
29
+
30
+
31
+ class Annotation(BaseModelObject):
32
+ annotated_resource_type: str
33
+ annotated_resource_id: int
34
+ annotation_type: AnnotationType
35
+ creating_app: str
36
+ creating_app_version: str
37
+ creating_user: str | None
38
+ data: dict[str, JsonValue]
39
+ status: AnnotationStatus
40
+
41
+
42
+ class AnnotationRequest(Annotation, RequestUpdateable):
43
+ """Request data class for annotations."""
44
+
45
+ # The 'id' field is not part of the request when creating a new resource,
46
+ # but is needed when updating an existing resource.
47
+ id: int | None = Field(default=None, exclude=True)
48
+
49
+ def as_id(self) -> InternalId:
50
+ if self.id is None:
51
+ raise ValueError("Cannot convert AnnotationRequest to InternalId when id is None")
52
+ return InternalId(id=self.id)
53
+
54
+ def as_update(self, mode: Literal["patch", "replace"]) -> dict[str, Any]:
55
+ """Converts the request to an update payload for the API."""
56
+ if self.id is None:
57
+ raise ValueError("id must be provided to create an update dictionary")
58
+ return {
59
+ "id": self.id,
60
+ "update": {
61
+ "annotationType": {"set": self.annotation_type},
62
+ "data": {"set": self.data},
63
+ "status": {"set": self.status},
64
+ },
65
+ }
66
+
67
+
68
+ class AnnotationResponse(Annotation, ResponseResource[AnnotationRequest]):
69
+ """Response data class for annotations."""
70
+
71
+ id: int
72
+ created_time: int
73
+ last_updated_time: int
74
+
75
+ def as_id(self) -> InternalId:
76
+ return InternalId(id=self.id)
77
+
78
+ def as_request_resource(self) -> AnnotationRequest:
79
+ return AnnotationRequest.model_validate(self.dump(), extra="ignore")
@@ -20,11 +20,21 @@ class BaseModelObject(BaseModel):
20
20
  # We allow extra fields to support forward compatibility.
21
21
  model_config = ConfigDict(alias_generator=to_camel, extra="allow", populate_by_name=True)
22
22
 
23
- def dump(self, camel_case: bool = True) -> dict[str, Any]:
23
+ def dump(self, camel_case: bool = True, exclude_extra: bool = False) -> dict[str, Any]:
24
24
  """Dump the resource to a dictionary.
25
25
 
26
- This is the default serialization method for request resources.
26
+ Args:
27
+ camel_case (bool): Whether to use camelCase for the keys. Default is True.
28
+ exclude_extra (bool): Whether to exclude extra fields not defined in the model. Default is False.
29
+
27
30
  """
31
+ if exclude_extra:
32
+ return self.model_dump(
33
+ mode="json",
34
+ by_alias=camel_case,
35
+ exclude_unset=True,
36
+ exclude=set(self.__pydantic_extra__) if self.__pydantic_extra__ else None,
37
+ )
28
38
  return self.model_dump(mode="json", by_alias=camel_case, exclude_unset=True)
29
39
 
30
40
  @classmethod
@@ -39,7 +49,7 @@ class Identifier(BaseModelObject):
39
49
 
40
50
  model_config = ConfigDict(alias_generator=to_camel, extra="ignore", populate_by_name=True, frozen=True)
41
51
 
42
- def dump(self, camel_case: bool = True) -> dict[str, Any]:
52
+ def dump(self, camel_case: bool = True, exclude_extra: bool = False) -> dict[str, Any]:
43
53
  """Dump the resource to a dictionary.
44
54
 
45
55
  This is the default serialization method for request resources.
@@ -42,6 +42,15 @@ from ._indexes import (
42
42
  IndexDefinition,
43
43
  InvertedIndex,
44
44
  )
45
+ from ._instance import (
46
+ EdgeRequest,
47
+ EdgeResponse,
48
+ InstanceDefinition,
49
+ InstanceResponseDefinition,
50
+ InstanceSource,
51
+ NodeRequest,
52
+ NodeResponse,
53
+ )
45
54
  from ._references import (
46
55
  ContainerConstraintReference,
47
56
  ContainerDirectReference,
@@ -99,6 +108,8 @@ __all__ = [
99
108
  "DateProperty",
100
109
  "DirectNodeRelation",
101
110
  "EdgeProperty",
111
+ "EdgeRequest",
112
+ "EdgeResponse",
102
113
  "EnumProperty",
103
114
  "EnumValue",
104
115
  "FileCDFExternalIdReference",
@@ -108,6 +119,9 @@ __all__ = [
108
119
  "Index",
109
120
  "IndexAdapter",
110
121
  "IndexDefinition",
122
+ "InstanceDefinition",
123
+ "InstanceResponseDefinition",
124
+ "InstanceSource",
111
125
  "Int32Property",
112
126
  "Int64Property",
113
127
  "InvertedIndex",
@@ -117,6 +131,8 @@ __all__ = [
117
131
  "MultiReverseDirectRelationPropertyRequest",
118
132
  "MultiReverseDirectRelationPropertyResponse",
119
133
  "NodeReference",
134
+ "NodeRequest",
135
+ "NodeResponse",
120
136
  "PropertyTypeDefinition",
121
137
  "RequiresConstraintDefinition",
122
138
  "ReverseDirectRelationProperty",
@@ -0,0 +1,143 @@
1
+ from abc import ABC
2
+ from typing import Any, Generic, Literal
3
+
4
+ from pydantic import JsonValue, field_serializer, field_validator
5
+
6
+ from cognite_toolkit._cdf_tk.client.data_classes.base import (
7
+ BaseModelObject,
8
+ RequestResource,
9
+ ResponseResource,
10
+ T_RequestResource,
11
+ )
12
+
13
+ from ._references import ContainerReference, EdgeReference, NodeReference, ViewReference
14
+
15
+
16
+ class InstanceDefinition(BaseModelObject, ABC):
17
+ """Base class for node and edge instances."""
18
+
19
+ instance_type: str # "node" | "edge"
20
+ space: str
21
+ external_id: str
22
+
23
+
24
+ class InstanceSource(BaseModelObject):
25
+ source: ViewReference | ContainerReference
26
+ properties: dict[str, JsonValue] | None = None
27
+
28
+
29
+ class InstanceRequestDefinition(InstanceDefinition, RequestResource, ABC):
30
+ existing_version: int | None = None
31
+ sources: list[InstanceSource]
32
+
33
+
34
+ class InstanceResponseDefinition(InstanceDefinition, ResponseResource, Generic[T_RequestResource], ABC):
35
+ version: int
36
+ created_time: int
37
+ last_updated_time: int
38
+ deleted_time: int | None = None
39
+ properties: dict[ViewReference | ContainerReference, dict[str, JsonValue]] | None = None
40
+
41
+ @field_validator("properties", mode="before")
42
+ def parse_reference(cls, value: Any) -> Any:
43
+ if not isinstance(value, dict):
44
+ return value
45
+ parsed: dict[ViewReference | ContainerReference, dict[str, Any]] = {}
46
+ for space, inner_dict in value.items():
47
+ if not isinstance(inner_dict, dict) or not isinstance(space, str):
48
+ raise ValueError(
49
+ f"Invalid properties format expected dict[str, dict[...]], got: dict[{type(space).__name__}, {type(inner_dict).__name__}]"
50
+ )
51
+ for view_or_container_identifier, prop in inner_dict.items():
52
+ if not isinstance(view_or_container_identifier, str):
53
+ raise ValueError(
54
+ "Invalid properties format expected dict[str, dict[str, ...]]], "
55
+ f"got: dict[{type(space).__name__}, "
56
+ f"dict[{type(view_or_container_identifier).__name__}, ...]]"
57
+ )
58
+ source_ref: ViewReference | ContainerReference
59
+ if "/" in view_or_container_identifier:
60
+ external_id, version = view_or_container_identifier.split("/", 1)
61
+ source_ref = ViewReference(space=space, external_id=external_id, version=version)
62
+ else:
63
+ source_ref = ContainerReference(space=space, external_id=view_or_container_identifier)
64
+ parsed[source_ref] = prop
65
+ return parsed
66
+
67
+ @field_serializer("properties", mode="plain")
68
+ def serialize_properties(self, value: dict[ViewReference | ContainerReference, dict[str, Any]] | None) -> Any:
69
+ if value is None:
70
+ return None
71
+ serialized: dict[str, dict[str, Any]] = {}
72
+ for source_ref, props in value.items():
73
+ space = source_ref.space
74
+ if space not in serialized:
75
+ serialized[space] = {}
76
+ if isinstance(source_ref, ViewReference):
77
+ identifier = f"{source_ref.external_id}/{source_ref.version}"
78
+ else:
79
+ identifier = source_ref.external_id
80
+ serialized[space][identifier] = props
81
+ return serialized
82
+
83
+
84
+ class NodeRequest(InstanceRequestDefinition):
85
+ """A node request resource."""
86
+
87
+ instance_type: Literal["node"] = "node"
88
+ type: NodeReference | None = None
89
+
90
+ def as_id(self) -> NodeReference:
91
+ return NodeReference(space=self.space, external_id=self.external_id)
92
+
93
+
94
+ class EdgeRequest(InstanceRequestDefinition):
95
+ """An edge request resource."""
96
+
97
+ instance_type: Literal["edge"] = "edge"
98
+ type: NodeReference
99
+ start_node: NodeReference
100
+ end_node: NodeReference
101
+
102
+ def as_id(self) -> EdgeReference:
103
+ return EdgeReference(space=self.space, external_id=self.external_id)
104
+
105
+
106
+ class NodeResponse(InstanceResponseDefinition[NodeRequest]):
107
+ """A node response from the API."""
108
+
109
+ instance_type: Literal["node"] = "node"
110
+ type: NodeReference | None = None
111
+
112
+ def as_id(self) -> NodeReference:
113
+ return NodeReference(space=self.space, external_id=self.external_id)
114
+
115
+ def as_request_resource(self) -> NodeRequest:
116
+ dumped = self.dump()
117
+ if self.properties:
118
+ dumped["sources"] = [
119
+ InstanceSource(source=source_ref, properties=props) for source_ref, props in self.properties.items()
120
+ ]
121
+ dumped["existingVersion"] = dumped.pop("version", None)
122
+ return NodeRequest.model_validate(dumped, extra="ignore")
123
+
124
+
125
+ class EdgeResponse(InstanceResponseDefinition[EdgeRequest]):
126
+ """An edge response from the API."""
127
+
128
+ instance_type: Literal["edge"] = "edge"
129
+ type: NodeReference
130
+ start_node: NodeReference
131
+ end_node: NodeReference
132
+
133
+ def as_id(self) -> EdgeReference:
134
+ return EdgeReference(space=self.space, external_id=self.external_id)
135
+
136
+ def as_request_resource(self) -> EdgeRequest:
137
+ dumped = self.dump()
138
+ if self.properties:
139
+ dumped["sources"] = [
140
+ InstanceSource(source=source_ref, properties=props) for source_ref, props in self.properties.items()
141
+ ]
142
+ dumped["existingVersion"] = dumped.pop("version", None)
143
+ return EdgeRequest.model_validate(dumped, extra="ignore")
@@ -48,6 +48,14 @@ class NodeReference(Identifier):
48
48
  return f"{self.space}:{self.external_id}"
49
49
 
50
50
 
51
+ class EdgeReference(Identifier):
52
+ space: str
53
+ external_id: str
54
+
55
+ def __str__(self) -> str:
56
+ return f"{self.space}:{self.external_id}"
57
+
58
+
51
59
  class ContainerDirectReference(Identifier):
52
60
  source: ContainerReference
53
61
  identifier: str
@@ -0,0 +1,35 @@
1
+ from typing import ClassVar
2
+
3
+ from cognite_toolkit._cdf_tk.client.data_classes.base import (
4
+ BaseModelObject,
5
+ RequestUpdateable,
6
+ ResponseResource,
7
+ )
8
+
9
+ from .identifiers import ExternalId
10
+
11
+
12
+ class DataSet(BaseModelObject):
13
+ external_id: str | None = None
14
+ name: str | None = None
15
+ description: str | None = None
16
+ metadata: dict[str, str] | None = None
17
+ write_protected: bool = False
18
+
19
+ def as_id(self) -> ExternalId:
20
+ if self.external_id is None:
21
+ raise ValueError("Cannot convert DataSet to ExternalId when external_id is None")
22
+ return ExternalId(external_id=self.external_id)
23
+
24
+
25
+ class DataSetRequest(DataSet, RequestUpdateable):
26
+ container_fields: ClassVar[frozenset[str]] = frozenset({"metadata"})
27
+
28
+
29
+ class DataSetResponse(DataSet, ResponseResource[DataSetRequest]):
30
+ id: int
31
+ created_time: int
32
+ last_updated_time: int
33
+
34
+ def as_request_resource(self) -> DataSetRequest:
35
+ return DataSetRequest.model_validate(self.dump(), extra="ignore")
@@ -0,0 +1,59 @@
1
+ from typing import ClassVar
2
+
3
+ from cognite_toolkit._cdf_tk.client.data_classes.base import (
4
+ BaseModelObject,
5
+ RequestUpdateable,
6
+ ResponseResource,
7
+ )
8
+
9
+ from .identifiers import ExternalId
10
+
11
+
12
+ class RawTable(BaseModelObject):
13
+ db_name: str
14
+ table_name: str
15
+
16
+
17
+ class Contact(BaseModelObject):
18
+ name: str | None = None
19
+ email: str | None = None
20
+ role: str | None = None
21
+ send_notification: bool | None = None
22
+
23
+
24
+ class NotificationConfig(BaseModelObject):
25
+ allowed_not_seen_range_in_minutes: int | None = None
26
+
27
+
28
+ class ExtractionPipeline(BaseModelObject):
29
+ external_id: str
30
+ name: str
31
+ description: str | None = None
32
+ data_set_id: int
33
+ raw_tables: list[RawTable] | None = None
34
+ schedule: str | None = None
35
+ contacts: list[Contact] | None = None
36
+ metadata: dict[str, str] | None = None
37
+ source: str | None = None
38
+ documentation: str | None = None
39
+ notification_config: NotificationConfig | None = None
40
+ created_by: str | None = None
41
+
42
+ def as_id(self) -> ExternalId:
43
+ return ExternalId(external_id=self.external_id)
44
+
45
+
46
+ class ExtractionPipelineRequest(ExtractionPipeline, RequestUpdateable):
47
+ container_fields: ClassVar[frozenset[str]] = frozenset({"raw_tables", "contacts", "metadata"})
48
+ non_nullable_fields: ClassVar[frozenset[str]] = frozenset(
49
+ {"documentation", "source", "notification_config", "schedule", "description"}
50
+ )
51
+
52
+
53
+ class ExtractionPipelineResponse(ExtractionPipeline, ResponseResource[ExtractionPipelineRequest]):
54
+ id: int
55
+ created_time: int
56
+ last_updated_time: int
57
+
58
+ def as_request_resource(self) -> ExtractionPipelineRequest:
59
+ return ExtractionPipelineRequest.model_validate(self.dump(), extra="ignore")
@@ -1,6 +1,6 @@
1
1
  from typing import ClassVar, Literal
2
2
 
3
- from pydantic import JsonValue
3
+ from pydantic import Field, JsonValue
4
4
 
5
5
  from cognite_toolkit._cdf_tk.client.data_classes.base import BaseModelObject, RequestUpdateable, ResponseResource
6
6
 
@@ -32,6 +32,10 @@ class FileMetadata(BaseModelObject):
32
32
  class FileMetadataRequest(FileMetadata, RequestUpdateable):
33
33
  container_fields: ClassVar[frozenset[str]] = frozenset({"metadata", "labels", "asset_ids", "security_categories"})
34
34
  non_nullable_fields: ClassVar[frozenset[str]] = frozenset({"asset_ids", "security_categories"})
35
+ # This field is not part of the request when creating or updating a resource
36
+ # but we added it here for convenience so that it is available when converting
37
+ # from response to request.
38
+ instance_id: NodeReference | None = Field(default=None, exclude=True)
35
39
 
36
40
 
37
41
  class FileMetadataResponse(FileMetadata, ResponseResource[FileMetadataRequest]):
@@ -41,6 +45,8 @@ class FileMetadataResponse(FileMetadata, ResponseResource[FileMetadataRequest]):
41
45
  uploaded: bool
42
46
  id: int
43
47
  instance_id: NodeReference | None = None
48
+ # This field is required in the upload endpoint response, but not in any other file metadata response
49
+ upload_url: str | None = None
44
50
 
45
51
  def as_request_resource(self) -> FileMetadataRequest:
46
52
  return FileMetadataRequest.model_validate(self.dump(), extra="ignore")