cognite-toolkit 0.6.97__py3-none-any.whl → 0.7.30__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 (136) hide show
  1. cognite_toolkit/_cdf.py +16 -17
  2. cognite_toolkit/_cdf_tk/apps/__init__.py +2 -0
  3. cognite_toolkit/_cdf_tk/apps/_core_app.py +13 -5
  4. cognite_toolkit/_cdf_tk/apps/_data_app.py +1 -1
  5. cognite_toolkit/_cdf_tk/apps/_dev_app.py +86 -0
  6. cognite_toolkit/_cdf_tk/apps/_download_app.py +692 -24
  7. cognite_toolkit/_cdf_tk/apps/_dump_app.py +43 -101
  8. cognite_toolkit/_cdf_tk/apps/_landing_app.py +18 -4
  9. cognite_toolkit/_cdf_tk/apps/_migrate_app.py +249 -9
  10. cognite_toolkit/_cdf_tk/apps/_modules_app.py +0 -3
  11. cognite_toolkit/_cdf_tk/apps/_purge.py +15 -43
  12. cognite_toolkit/_cdf_tk/apps/_run.py +11 -0
  13. cognite_toolkit/_cdf_tk/apps/_upload_app.py +45 -6
  14. cognite_toolkit/_cdf_tk/builders/__init__.py +2 -2
  15. cognite_toolkit/_cdf_tk/builders/_base.py +28 -42
  16. cognite_toolkit/_cdf_tk/cdf_toml.py +20 -1
  17. cognite_toolkit/_cdf_tk/client/_toolkit_client.py +23 -3
  18. cognite_toolkit/_cdf_tk/client/api/extended_functions.py +6 -9
  19. cognite_toolkit/_cdf_tk/client/api/infield.py +93 -1
  20. cognite_toolkit/_cdf_tk/client/api/migration.py +175 -1
  21. cognite_toolkit/_cdf_tk/client/api/streams.py +84 -0
  22. cognite_toolkit/_cdf_tk/client/api/three_d.py +50 -0
  23. cognite_toolkit/_cdf_tk/client/data_classes/base.py +25 -1
  24. cognite_toolkit/_cdf_tk/client/data_classes/canvas.py +46 -3
  25. cognite_toolkit/_cdf_tk/client/data_classes/charts.py +3 -3
  26. cognite_toolkit/_cdf_tk/client/data_classes/charts_data.py +95 -213
  27. cognite_toolkit/_cdf_tk/client/data_classes/infield.py +32 -18
  28. cognite_toolkit/_cdf_tk/client/data_classes/migration.py +10 -2
  29. cognite_toolkit/_cdf_tk/client/data_classes/streams.py +90 -0
  30. cognite_toolkit/_cdf_tk/client/data_classes/three_d.py +47 -0
  31. cognite_toolkit/_cdf_tk/client/testing.py +18 -2
  32. cognite_toolkit/_cdf_tk/commands/__init__.py +6 -6
  33. cognite_toolkit/_cdf_tk/commands/_changes.py +3 -42
  34. cognite_toolkit/_cdf_tk/commands/_download.py +21 -11
  35. cognite_toolkit/_cdf_tk/commands/_migrate/__init__.py +0 -2
  36. cognite_toolkit/_cdf_tk/commands/_migrate/command.py +22 -20
  37. cognite_toolkit/_cdf_tk/commands/_migrate/conversion.py +133 -91
  38. cognite_toolkit/_cdf_tk/commands/_migrate/data_classes.py +73 -22
  39. cognite_toolkit/_cdf_tk/commands/_migrate/data_mapper.py +311 -43
  40. cognite_toolkit/_cdf_tk/commands/_migrate/default_mappings.py +5 -5
  41. cognite_toolkit/_cdf_tk/commands/_migrate/issues.py +33 -0
  42. cognite_toolkit/_cdf_tk/commands/_migrate/migration_io.py +157 -8
  43. cognite_toolkit/_cdf_tk/commands/_migrate/selectors.py +9 -4
  44. cognite_toolkit/_cdf_tk/commands/_purge.py +27 -28
  45. cognite_toolkit/_cdf_tk/commands/_questionary_style.py +16 -0
  46. cognite_toolkit/_cdf_tk/commands/_upload.py +109 -86
  47. cognite_toolkit/_cdf_tk/commands/about.py +221 -0
  48. cognite_toolkit/_cdf_tk/commands/auth.py +19 -12
  49. cognite_toolkit/_cdf_tk/commands/build_cmd.py +15 -61
  50. cognite_toolkit/_cdf_tk/commands/clean.py +63 -16
  51. cognite_toolkit/_cdf_tk/commands/deploy.py +20 -17
  52. cognite_toolkit/_cdf_tk/commands/dump_resource.py +6 -4
  53. cognite_toolkit/_cdf_tk/commands/init.py +225 -3
  54. cognite_toolkit/_cdf_tk/commands/modules.py +20 -44
  55. cognite_toolkit/_cdf_tk/commands/pull.py +6 -19
  56. cognite_toolkit/_cdf_tk/commands/resources.py +179 -0
  57. cognite_toolkit/_cdf_tk/constants.py +20 -1
  58. cognite_toolkit/_cdf_tk/cruds/__init__.py +19 -5
  59. cognite_toolkit/_cdf_tk/cruds/_base_cruds.py +14 -70
  60. cognite_toolkit/_cdf_tk/cruds/_data_cruds.py +8 -17
  61. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/__init__.py +4 -1
  62. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/agent.py +11 -9
  63. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/auth.py +4 -14
  64. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/classic.py +44 -43
  65. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/configuration.py +4 -11
  66. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/data_organization.py +4 -13
  67. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/datamodel.py +205 -66
  68. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/extraction_pipeline.py +5 -17
  69. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/fieldops.py +116 -27
  70. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/file.py +6 -27
  71. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/function.py +9 -28
  72. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/hosted_extractors.py +12 -30
  73. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/industrial_tool.py +3 -7
  74. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/location.py +3 -15
  75. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/migration.py +4 -12
  76. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/raw.py +4 -10
  77. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/relationship.py +3 -8
  78. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/robotics.py +15 -44
  79. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/streams.py +94 -0
  80. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/three_d_model.py +3 -7
  81. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/timeseries.py +5 -15
  82. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/transformation.py +39 -31
  83. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/workflow.py +20 -40
  84. cognite_toolkit/_cdf_tk/cruds/_worker.py +24 -36
  85. cognite_toolkit/_cdf_tk/feature_flags.py +16 -36
  86. cognite_toolkit/_cdf_tk/plugins.py +2 -1
  87. cognite_toolkit/_cdf_tk/resource_classes/__init__.py +4 -0
  88. cognite_toolkit/_cdf_tk/resource_classes/capabilities.py +12 -0
  89. cognite_toolkit/_cdf_tk/resource_classes/functions.py +3 -1
  90. cognite_toolkit/_cdf_tk/resource_classes/infield_cdm_location_config.py +109 -0
  91. cognite_toolkit/_cdf_tk/resource_classes/migration.py +8 -17
  92. cognite_toolkit/_cdf_tk/resource_classes/streams.py +29 -0
  93. cognite_toolkit/_cdf_tk/storageio/__init__.py +9 -21
  94. cognite_toolkit/_cdf_tk/storageio/_annotations.py +19 -16
  95. cognite_toolkit/_cdf_tk/storageio/_applications.py +338 -26
  96. cognite_toolkit/_cdf_tk/storageio/_asset_centric.py +67 -104
  97. cognite_toolkit/_cdf_tk/storageio/_base.py +61 -29
  98. cognite_toolkit/_cdf_tk/storageio/_datapoints.py +276 -20
  99. cognite_toolkit/_cdf_tk/storageio/_file_content.py +436 -0
  100. cognite_toolkit/_cdf_tk/storageio/_instances.py +34 -2
  101. cognite_toolkit/_cdf_tk/storageio/_raw.py +26 -0
  102. cognite_toolkit/_cdf_tk/storageio/selectors/__init__.py +62 -4
  103. cognite_toolkit/_cdf_tk/storageio/selectors/_base.py +14 -2
  104. cognite_toolkit/_cdf_tk/storageio/selectors/_canvas.py +14 -0
  105. cognite_toolkit/_cdf_tk/storageio/selectors/_charts.py +14 -0
  106. cognite_toolkit/_cdf_tk/storageio/selectors/_datapoints.py +23 -3
  107. cognite_toolkit/_cdf_tk/storageio/selectors/_file_content.py +164 -0
  108. cognite_toolkit/_cdf_tk/tk_warnings/other.py +4 -0
  109. cognite_toolkit/_cdf_tk/tracker.py +2 -2
  110. cognite_toolkit/_cdf_tk/utils/dtype_conversion.py +9 -3
  111. cognite_toolkit/_cdf_tk/utils/fileio/__init__.py +2 -0
  112. cognite_toolkit/_cdf_tk/utils/fileio/_base.py +5 -1
  113. cognite_toolkit/_cdf_tk/utils/fileio/_readers.py +112 -20
  114. cognite_toolkit/_cdf_tk/utils/fileio/_writers.py +15 -15
  115. cognite_toolkit/_cdf_tk/utils/http_client/_client.py +284 -18
  116. cognite_toolkit/_cdf_tk/utils/http_client/_data_classes.py +50 -4
  117. cognite_toolkit/_cdf_tk/utils/http_client/_data_classes2.py +187 -0
  118. cognite_toolkit/_cdf_tk/utils/interactive_select.py +9 -14
  119. cognite_toolkit/_cdf_tk/utils/sql_parser.py +2 -3
  120. cognite_toolkit/_cdf_tk/utils/useful_types.py +6 -2
  121. cognite_toolkit/_cdf_tk/validation.py +79 -1
  122. cognite_toolkit/_repo_files/GitHub/.github/workflows/deploy.yaml +1 -1
  123. cognite_toolkit/_repo_files/GitHub/.github/workflows/dry-run.yaml +1 -1
  124. cognite_toolkit/_resources/cdf.toml +5 -4
  125. cognite_toolkit/_version.py +1 -1
  126. cognite_toolkit/config.dev.yaml +13 -0
  127. {cognite_toolkit-0.6.97.dist-info → cognite_toolkit-0.7.30.dist-info}/METADATA +24 -24
  128. {cognite_toolkit-0.6.97.dist-info → cognite_toolkit-0.7.30.dist-info}/RECORD +153 -143
  129. cognite_toolkit-0.7.30.dist-info/WHEEL +4 -0
  130. {cognite_toolkit-0.6.97.dist-info → cognite_toolkit-0.7.30.dist-info}/entry_points.txt +1 -0
  131. cognite_toolkit/_cdf_tk/commands/_migrate/canvas.py +0 -201
  132. cognite_toolkit/_cdf_tk/commands/dump_data.py +0 -489
  133. cognite_toolkit/_cdf_tk/commands/featureflag.py +0 -27
  134. cognite_toolkit/_cdf_tk/utils/table_writers.py +0 -434
  135. cognite_toolkit-0.6.97.dist-info/WHEEL +0 -4
  136. cognite_toolkit-0.6.97.dist-info/licenses/LICENSE +0 -18
@@ -1,7 +1,7 @@
1
1
  import warnings
2
2
  from collections.abc import Sequence
3
3
  from itertools import groupby
4
- from typing import TypeVar, overload
4
+ from typing import Literal, TypeVar, cast, overload
5
5
 
6
6
  from cognite.client._constants import DEFAULT_LIMIT_READ
7
7
  from cognite.client.data_classes.data_modeling import (
@@ -10,6 +10,7 @@ from cognite.client.data_classes.data_modeling import (
10
10
  NodeApplyResultList,
11
11
  NodeId,
12
12
  NodeList,
13
+ ViewId,
13
14
  filters,
14
15
  query,
15
16
  )
@@ -347,9 +348,182 @@ class SpaceSourceAPI:
347
348
  return results
348
349
 
349
350
 
351
+ _T_Cached = TypeVar("_T_Cached", bound=NodeId | ViewId)
352
+
353
+
354
+ class LookupAPI:
355
+ def __init__(self, instance_api: ExtendedInstancesAPI, resource_type: AssetCentricType) -> None:
356
+ self._instance_api = instance_api
357
+ self._resource_type = resource_type
358
+ self._view_id = InstanceSource.get_source()
359
+ self._node_id_by_id: dict[int, NodeId | None] = {}
360
+ self._node_id_by_external_id: dict[str, NodeId | None] = {}
361
+ self._consumer_view_id_by_id: dict[int, ViewId | None] = {}
362
+ self._consumer_view_id_by_external_id: dict[str, ViewId | None] = {}
363
+ self._RETRIEVE_LIMIT = 1000
364
+
365
+ @overload
366
+ def __call__(self, id: int, external_id: None = None) -> NodeId | None: ...
367
+
368
+ @overload
369
+ def __call__(self, id: SequenceNotStr[int], external_id: None = None) -> dict[int, NodeId]: ...
370
+
371
+ @overload
372
+ def __call__(self, *, external_id: str) -> NodeId | None: ...
373
+
374
+ @overload
375
+ def __call__(self, *, external_id: SequenceNotStr[str]) -> dict[str, NodeId]: ...
376
+
377
+ def __call__(
378
+ self, id: int | SequenceNotStr[int] | None = None, external_id: str | SequenceNotStr[str] | None = None
379
+ ) -> dict[int, NodeId] | dict[str, NodeId] | NodeId | None:
380
+ """Lookup NodeId by either internal ID or external ID.
381
+
382
+ Args:
383
+ id (int | Sequence[int] | None): The internal ID(s) to lookup.
384
+ external_id (str | SequenceNotStr[str] | None): The external ID(s) to lookup.
385
+
386
+ Returns:
387
+ NodeId | dict[int, NodeId] | dict[str, NodeId] | None: The corresponding NodeId(s) if found, otherwise None.
388
+
389
+ """
390
+ if id is not None and external_id is None:
391
+ return self._lookup(
392
+ identifier=id,
393
+ cache=self._node_id_by_id,
394
+ property_name="id",
395
+ return_type=NodeId,
396
+ input_type=int,
397
+ )
398
+ elif external_id is not None and id is None:
399
+ return self._lookup(
400
+ identifier=external_id,
401
+ cache=self._node_id_by_external_id,
402
+ property_name="classicExternalId",
403
+ return_type=NodeId,
404
+ input_type=str,
405
+ )
406
+ else:
407
+ raise TypeError("Either id or external_id must be provided, but not both.")
408
+
409
+ @overload
410
+ def consumer_view(self, id: int, external_id: None = None) -> ViewId | None: ...
411
+
412
+ @overload
413
+ def consumer_view(self, id: SequenceNotStr[int], external_id: None = None) -> dict[int, ViewId]: ...
414
+
415
+ @overload
416
+ def consumer_view(self, *, external_id: str) -> ViewId | None: ...
417
+
418
+ @overload
419
+ def consumer_view(self, *, external_id: SequenceNotStr[str]) -> dict[str, ViewId]: ...
420
+
421
+ def consumer_view(
422
+ self, id: int | SequenceNotStr[int] | None = None, external_id: str | SequenceNotStr[str] | None = None
423
+ ) -> dict[int, ViewId] | dict[str, ViewId] | ViewId | None:
424
+ """Lookup Consumer ViewId by either internal ID or external ID.
425
+
426
+ Args:
427
+ id (int | Sequence[int] | None): The internal ID(s) to lookup.
428
+ external_id (str | SequenceNotStr[str] | None): The external ID(s) to lookup.
429
+ Returns:
430
+ ViewId | dict[int, ViewId] | dict[str, ViewId] | None
431
+ """
432
+ if id is not None and external_id is None:
433
+ return self._lookup(
434
+ identifier=id,
435
+ cache=self._consumer_view_id_by_id,
436
+ property_name="id",
437
+ return_type=ViewId,
438
+ input_type=int,
439
+ )
440
+ elif external_id is not None and id is None:
441
+ return self._lookup(
442
+ identifier=external_id,
443
+ cache=self._consumer_view_id_by_external_id,
444
+ property_name="classicExternalId",
445
+ return_type=ViewId,
446
+ input_type=str,
447
+ )
448
+ else:
449
+ raise TypeError("Either id or external_id must be provided, but not both.")
450
+
451
+ def _lookup(
452
+ self,
453
+ identifier: _T | SequenceNotStr[_T],
454
+ cache: dict[_T, _T_Cached | None],
455
+ property_name: Literal["id", "classicExternalId"],
456
+ return_type: type[_T_Cached],
457
+ input_type: type[_T],
458
+ ) -> dict[_T, _T_Cached] | _T_Cached | None:
459
+ """Generic lookup method for both NodeId and ViewId by id or external_id."""
460
+ is_single = isinstance(identifier, input_type)
461
+ # MyPy does not understand that if is_single is True, identifier is _T, else SequenceNotStr[_T].
462
+ identifiers: list[_T] = [identifier] if is_single else list(identifier) # type: ignore[arg-type, list-item]
463
+
464
+ missing = [id_ for id_ in identifiers if id_ not in cache]
465
+ if missing:
466
+ self._fetch_and_cache(missing, by=property_name)
467
+
468
+ if is_single:
469
+ return cache.get(identifier) # type: ignore[arg-type]
470
+
471
+ return {id_: value for id_ in identifiers if isinstance(value := cache.get(id_), return_type)}
472
+
473
+ def _fetch_and_cache(self, identifiers: Sequence[int | str], by: Literal["id", "classicExternalId"]) -> None:
474
+ for chunk in chunker_sequence(identifiers, self._RETRIEVE_LIMIT):
475
+ retrieve_query = query.Query(
476
+ with_={
477
+ "instanceSource": query.NodeResultSetExpression(
478
+ filter=filters.And(
479
+ filters.HasData(views=[self._view_id]),
480
+ filters.Equals(self._view_id.as_property_ref("resourceType"), self._resource_type),
481
+ filters.In(self._view_id.as_property_ref(by), list(chunk)),
482
+ ),
483
+ limit=len(chunk),
484
+ ),
485
+ },
486
+ select={"instanceSource": query.Select([query.SourceSelector(self._view_id, ["*"])])},
487
+ )
488
+ chunk_response = self._instance_api.query(retrieve_query)
489
+ items = chunk_response.get("instanceSource", [])
490
+ for item in items:
491
+ instance_source = InstanceSource._load(item.dump())
492
+ node_id = instance_source.as_id()
493
+ self._node_id_by_id[instance_source.id_] = node_id
494
+ self._consumer_view_id_by_id[instance_source.id_] = instance_source.consumer_view()
495
+ if instance_source.classic_external_id:
496
+ self._node_id_by_external_id[instance_source.classic_external_id] = node_id
497
+ self._consumer_view_id_by_external_id[instance_source.classic_external_id] = (
498
+ instance_source.consumer_view()
499
+ )
500
+ missing = set(chunk) - set(self._node_id_by_id.keys()) - set(self._node_id_by_external_id.keys())
501
+ if by == "id":
502
+ for missing_id in cast(set[int], missing):
503
+ if missing_id not in self._node_id_by_id:
504
+ self._node_id_by_id[missing_id] = None
505
+ if missing_id not in self._consumer_view_id_by_id:
506
+ self._consumer_view_id_by_id[missing_id] = None
507
+ elif by == "classicExternalId":
508
+ for missing_ext_id in cast(set[str], missing):
509
+ if missing_ext_id not in self._node_id_by_external_id:
510
+ self._node_id_by_external_id[missing_ext_id] = None
511
+ if missing_ext_id not in self._consumer_view_id_by_external_id:
512
+ self._consumer_view_id_by_external_id[missing_ext_id] = None
513
+
514
+
515
+ class MigrationLookupAPI:
516
+ def __init__(self, instance_api: ExtendedInstancesAPI) -> None:
517
+ self.assets = LookupAPI(instance_api, "asset")
518
+ self.events = LookupAPI(instance_api, "event")
519
+ self.files = LookupAPI(instance_api, "file")
520
+ self.time_series = LookupAPI(instance_api, "timeseries")
521
+
522
+
350
523
  class MigrationAPI:
351
524
  def __init__(self, instance_api: ExtendedInstancesAPI) -> None:
352
525
  self.instance_source = InstanceSourceAPI(instance_api)
353
526
  self.resource_view_mapping = ResourceViewMappingAPI(instance_api)
354
527
  self.created_source_system = CreatedSourceSystemAPI(instance_api)
355
528
  self.space_source = SpaceSourceAPI(instance_api)
529
+ self.lookup = MigrationLookupAPI(instance_api)
@@ -0,0 +1,84 @@
1
+ from collections.abc import Sequence
2
+
3
+ from rich.console import Console
4
+
5
+ from cognite_toolkit._cdf_tk.client.data_classes.api_classes import PagedResponse
6
+ from cognite_toolkit._cdf_tk.client.data_classes.streams import StreamRequest, StreamResponse
7
+ from cognite_toolkit._cdf_tk.utils.http_client import HTTPClient, ItemsRequest, ParamRequest
8
+
9
+
10
+ class StreamsAPI:
11
+ ENDPOINT = "/streams"
12
+
13
+ def __init__(self, http_client: HTTPClient, console: Console) -> None:
14
+ self._http_client = http_client
15
+ self._console = console
16
+ self._config = http_client.config
17
+
18
+ def create(self, items: Sequence[StreamRequest]) -> list[StreamResponse]:
19
+ """Create one or more streams.
20
+
21
+ Args:
22
+ items: Sequence of StreamRequest items to create.
23
+
24
+ Returns:
25
+ List of created StreamResponse items.
26
+ """
27
+ responses = self._http_client.request_with_retries(
28
+ ItemsRequest(
29
+ endpoint_url=self._config.create_api_url(self.ENDPOINT),
30
+ method="POST",
31
+ items=list(items),
32
+ )
33
+ )
34
+ responses.raise_for_status()
35
+ return PagedResponse[StreamResponse].model_validate(responses.get_first_body()).items
36
+
37
+ def delete(self, external_id: str) -> None:
38
+ """Delete stream using its external ID.
39
+
40
+ Args:
41
+ external_id: External ID of the stream to delete.
42
+ """
43
+ responses = self._http_client.request_with_retries(
44
+ ParamRequest(
45
+ endpoint_url=self._config.create_api_url(f"{self.ENDPOINT}/{external_id}"),
46
+ method="DELETE",
47
+ )
48
+ )
49
+ responses.raise_for_status()
50
+
51
+ def list(self) -> list[StreamResponse]:
52
+ """List streams.
53
+
54
+ Returns:
55
+ StreamResponseList containing the listed streams.
56
+ """
57
+ responses = self._http_client.request_with_retries(
58
+ ParamRequest(
59
+ endpoint_url=self._config.create_api_url(self.ENDPOINT),
60
+ method="GET",
61
+ )
62
+ )
63
+ responses.raise_for_status()
64
+ return PagedResponse[StreamResponse].model_validate(responses.get_first_body()).items
65
+
66
+ def retrieve(self, external_id: str, include_statistics: bool = True) -> StreamResponse:
67
+ """Retrieve a stream by its external ID.
68
+
69
+ Args:
70
+ external_id: External ID of the stream to retrieve.
71
+ include_statistics: Whether to include usage statistics in the response.
72
+ Returns:
73
+ StreamResponse item.
74
+ """
75
+ responses = self._http_client.request_with_retries(
76
+ ParamRequest(
77
+ endpoint_url=self._config.create_api_url(f"{self.ENDPOINT}/{external_id}"),
78
+ method="GET",
79
+ parameters={"includeStatistics": include_statistics},
80
+ )
81
+ )
82
+ responses.raise_for_status()
83
+ response_body = responses.get_first_body()
84
+ return StreamResponse.model_validate(response_body)
@@ -0,0 +1,50 @@
1
+ from rich.console import Console
2
+
3
+ from cognite_toolkit._cdf_tk.client.data_classes.api_classes import PagedResponse
4
+ from cognite_toolkit._cdf_tk.client.data_classes.three_d import ThreeDModelResponse
5
+ from cognite_toolkit._cdf_tk.utils.http_client import HTTPClient, ParamRequest
6
+ from cognite_toolkit._cdf_tk.utils.useful_types import PrimitiveType
7
+
8
+
9
+ class ThreeDModelAPI:
10
+ ENDPOINT = "/3d/models"
11
+
12
+ def __init__(self, http_client: HTTPClient, console: Console) -> None:
13
+ self._http_client = http_client
14
+ self._console = console
15
+ self._config = http_client.config
16
+
17
+ def iterate(
18
+ self,
19
+ published: bool | None = None,
20
+ include_revision_info: bool = False,
21
+ limit: int = 100,
22
+ cursor: str | None = None,
23
+ ) -> PagedResponse[ThreeDModelResponse]:
24
+ if not (0 < limit <= 1000):
25
+ raise ValueError("Limit must be between 1 and 1000.")
26
+ parameters: dict[str, PrimitiveType] = {
27
+ # There is a bug in the API. The parameter includeRevisionInfo is expected to be lower case and not
28
+ # camel case as documented. You get error message: Unrecognized query parameter includeRevisionInfo,
29
+ # did you mean includerevisioninfo?
30
+ "includerevisioninfo": include_revision_info,
31
+ "limit": limit,
32
+ }
33
+ if published is not None:
34
+ parameters["published"] = published
35
+ if cursor is not None:
36
+ parameters["cursor"] = cursor
37
+ responses = self._http_client.request_with_retries(
38
+ ParamRequest(
39
+ endpoint_url=self._config.create_api_url(self.ENDPOINT),
40
+ method="GET",
41
+ parameters=parameters,
42
+ )
43
+ )
44
+ responses.raise_for_status()
45
+ return PagedResponse[ThreeDModelResponse].model_validate(responses.get_first_body())
46
+
47
+
48
+ class ThreeDAPI:
49
+ def __init__(self, http_client: HTTPClient, console: Console) -> None:
50
+ self.models = ThreeDModelAPI(http_client, console)
@@ -1,10 +1,14 @@
1
1
  import sys
2
2
  from abc import ABC, abstractmethod
3
- from typing import Any, Generic, TypeVar
3
+ from collections import UserList
4
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar
4
5
 
5
6
  from pydantic import BaseModel, ConfigDict
6
7
  from pydantic.alias_generators import to_camel
7
8
 
9
+ if TYPE_CHECKING:
10
+ from cognite.client import CogniteClient
11
+
8
12
  if sys.version_info >= (3, 11):
9
13
  from typing import Self
10
14
  else:
@@ -61,3 +65,23 @@ class Identifier(BaseModel):
61
65
 
62
66
  def as_id(self) -> Self:
63
67
  return self
68
+
69
+
70
+ T_Resource = TypeVar("T_Resource", bound=RequestResource | ResponseResource)
71
+
72
+
73
+ class BaseResourceList(UserList[T_Resource]):
74
+ """Base class for resource lists."""
75
+
76
+ _RESOURCE: type[T_Resource]
77
+
78
+ def __init__(self, initlist: list[T_Resource] | None = None, **_: Any) -> None:
79
+ super().__init__(initlist or [])
80
+
81
+ def dump(self, camel_case: bool = True) -> list[dict[str, Any]]:
82
+ return [item.dump(camel_case) for item in self.data]
83
+
84
+ @classmethod
85
+ def load(cls, data: list[dict[str, Any]], cognite_client: "CogniteClient | None" = None) -> Self:
86
+ items = [cls._RESOURCE.model_validate(item) for item in data]
87
+ return cls(items) # type: ignore[arg-type]
@@ -2,7 +2,7 @@ import sys
2
2
  from abc import ABC
3
3
  from collections.abc import Sequence
4
4
  from datetime import datetime, timezone
5
- from typing import Any
5
+ from typing import Any, TypeVar
6
6
  from uuid import uuid4
7
7
 
8
8
  from cognite.client import CogniteClient
@@ -31,6 +31,7 @@ from cognite.client.data_classes.data_modeling.instances import (
31
31
  )
32
32
 
33
33
  from cognite_toolkit._cdf_tk.client.data_classes.migration import AssetCentricId
34
+ from cognite_toolkit._cdf_tk.utils.useful_types import JsonVal
34
35
 
35
36
  if sys.version_info >= (3, 11):
36
37
  from typing import Self
@@ -68,6 +69,9 @@ class ExtendedTypedNodeApply(TypedNodeApply, ABC):
68
69
  return output
69
70
 
70
71
 
72
+ T_ExtendedTypedNodeApply = TypeVar("T_ExtendedTypedNodeApply", bound=ExtendedTypedNodeApply)
73
+
74
+
71
75
  class _CanvasProperties:
72
76
  created_by = PropertyOptions("createdBy")
73
77
  updated_at = PropertyOptions("updatedAt")
@@ -901,7 +905,7 @@ class IndustrialCanvasApply(CogniteResource):
901
905
  raise TypeError(f"Unexpected instance type: {type(instance)}")
902
906
  return ids
903
907
 
904
- def dump(self, keep_existing_version: bool = True) -> dict[str, object]:
908
+ def dump(self, keep_existing_version: bool = True) -> dict[str, JsonVal]:
905
909
  """Dump the IndustrialCanvasApply to a dictionary."""
906
910
  return {
907
911
  "canvas": self.canvas.dump(keep_existing_version=keep_existing_version),
@@ -953,7 +957,46 @@ class IndustrialCanvasApply(CogniteResource):
953
957
  @classmethod
954
958
  def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self:
955
959
  """Load an IndustrialCanvasApply instance from a resource dictionary."""
956
- raise NotImplementedError("IndustrialCanvasApply cannot be loaded from dict.")
960
+ if "canvas" not in resource:
961
+ raise ValueError("Resource does not contain a canvas node.")
962
+ canvas_resource = resource["canvas"]
963
+ if isinstance(canvas_resource, dict):
964
+ canvas = CanvasApply._load(canvas_resource)
965
+ elif isinstance(canvas_resource, CanvasApply):
966
+ canvas = canvas_resource
967
+ elif isinstance(canvas_resource, NodeApply):
968
+ canvas = CanvasApply._load(canvas_resource.dump())
969
+ else:
970
+ raise TypeError(f"Canvas resource {type(canvas_resource)} is not supported.")
971
+ return cls(
972
+ canvas=canvas,
973
+ annotations=cls._load_apply_items(resource.get("annotations"), CanvasAnnotationApply),
974
+ container_references=cls._load_apply_items(resource.get("containerReferences"), ContainerReferenceApply),
975
+ fdm_instance_container_references=cls._load_apply_items(
976
+ resource.get("fdmInstanceContainerReferences"), FdmInstanceContainerReferenceApply
977
+ ),
978
+ solution_tags=cls._load_apply_items(resource.get("solutionTags"), CogniteSolutionTagApply),
979
+ )
980
+
981
+ @classmethod
982
+ def _load_apply_items(
983
+ cls, items: object | None, node_cls: type[T_ExtendedTypedNodeApply]
984
+ ) -> list[T_ExtendedTypedNodeApply]:
985
+ if items is None:
986
+ return []
987
+ elif isinstance(items, Sequence):
988
+ nodes: list[T_ExtendedTypedNodeApply] = []
989
+ for node in items:
990
+ if isinstance(node, dict):
991
+ nodes.append(node_cls._load(node))
992
+ elif isinstance(node, node_cls):
993
+ nodes.append(node)
994
+ elif isinstance(node, NodeApply):
995
+ nodes.append(node_cls._load(node.dump()))
996
+ else:
997
+ raise TypeError(f"Expected a sequence of {node_cls.__name__}, got {type(node).__name__}")
998
+ return nodes
999
+ raise TypeError(f"Expected a sequence of {node_cls.__name__}, got {type(items).__name__}")
957
1000
 
958
1001
 
959
1002
  class IndustrialCanvas(WriteableCogniteResource[IndustrialCanvasApply]):
@@ -36,7 +36,7 @@ class ChartCore(WriteableCogniteResource["ChartWrite"], ABC):
36
36
  def dump(self, camel_case: bool = True) -> dict[str, Any]:
37
37
  """Convert the chart to a dictionary representation."""
38
38
  output = super().dump(camel_case=camel_case)
39
- output["data"] = self.data.dump(camel_case=camel_case)
39
+ output["data"] = self.data.model_dump(mode="json", by_alias=camel_case, exclude_unset=True)
40
40
  return output
41
41
 
42
42
 
@@ -58,7 +58,7 @@ class ChartWrite(ChartCore):
58
58
  return cls(
59
59
  external_id=resource["externalId"],
60
60
  visibility=resource["visibility"],
61
- data=ChartData._load(resource["data"], cognite_client=cognite_client),
61
+ data=ChartData._load(resource["data"]),
62
62
  )
63
63
 
64
64
 
@@ -98,7 +98,7 @@ class Chart(ChartCore):
98
98
  created_time=resource["createdTime"],
99
99
  last_updated_time=resource["lastUpdatedTime"],
100
100
  visibility=resource["visibility"],
101
- data=ChartData._load(resource["data"], cognite_client=cognite_client),
101
+ data=ChartData._load(resource["data"]),
102
102
  owner_id=resource["ownerId"],
103
103
  )
104
104