cognite-toolkit 0.6.89__py3-none-any.whl → 0.6.91__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-toolkit might be problematic. Click here for more details.

Files changed (34) hide show
  1. cognite_toolkit/_cdf_tk/client/_toolkit_client.py +5 -0
  2. cognite_toolkit/_cdf_tk/client/api/infield.py +156 -0
  3. cognite_toolkit/_cdf_tk/client/data_classes/api_classes.py +17 -0
  4. cognite_toolkit/_cdf_tk/client/data_classes/base.py +63 -0
  5. cognite_toolkit/_cdf_tk/client/data_classes/infield.py +102 -0
  6. cognite_toolkit/_cdf_tk/client/data_classes/instance_api.py +157 -0
  7. cognite_toolkit/_cdf_tk/client/testing.py +3 -0
  8. cognite_toolkit/_cdf_tk/commands/_utils.py +1 -24
  9. cognite_toolkit/_cdf_tk/commands/build_cmd.py +1 -1
  10. cognite_toolkit/_cdf_tk/commands/clean.py +11 -5
  11. cognite_toolkit/_cdf_tk/commands/deploy.py +14 -10
  12. cognite_toolkit/_cdf_tk/commands/pull.py +19 -13
  13. cognite_toolkit/_cdf_tk/cruds/__init__.py +3 -0
  14. cognite_toolkit/_cdf_tk/cruds/_base_cruds.py +28 -25
  15. cognite_toolkit/_cdf_tk/cruds/_data_cruds.py +10 -7
  16. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/__init__.py +2 -1
  17. cognite_toolkit/_cdf_tk/cruds/_resource_cruds/fieldops.py +111 -2
  18. cognite_toolkit/_cdf_tk/cruds/_worker.py +24 -20
  19. cognite_toolkit/_cdf_tk/data_classes/_build_variables.py +120 -14
  20. cognite_toolkit/_cdf_tk/data_classes/_built_resources.py +1 -1
  21. cognite_toolkit/_cdf_tk/protocols.py +97 -0
  22. cognite_toolkit/_cdf_tk/resource_classes/__init__.py +2 -0
  23. cognite_toolkit/_cdf_tk/resource_classes/agent.py +1 -0
  24. cognite_toolkit/_cdf_tk/resource_classes/infield_cdmv1.py +94 -0
  25. cognite_toolkit/_cdf_tk/utils/text.py +23 -0
  26. cognite_toolkit/_repo_files/GitHub/.github/workflows/deploy.yaml +1 -1
  27. cognite_toolkit/_repo_files/GitHub/.github/workflows/dry-run.yaml +1 -1
  28. cognite_toolkit/_resources/cdf.toml +1 -1
  29. cognite_toolkit/_version.py +1 -1
  30. {cognite_toolkit-0.6.89.dist-info → cognite_toolkit-0.6.91.dist-info}/METADATA +1 -1
  31. {cognite_toolkit-0.6.89.dist-info → cognite_toolkit-0.6.91.dist-info}/RECORD +34 -27
  32. {cognite_toolkit-0.6.89.dist-info → cognite_toolkit-0.6.91.dist-info}/WHEEL +0 -0
  33. {cognite_toolkit-0.6.89.dist-info → cognite_toolkit-0.6.91.dist-info}/entry_points.txt +0 -0
  34. {cognite_toolkit-0.6.89.dist-info → cognite_toolkit-0.6.91.dist-info}/licenses/LICENSE +0 -0
@@ -8,11 +8,11 @@ from functools import cached_property
8
8
  from pathlib import Path
9
9
  from typing import Any, Literal, SupportsIndex, overload
10
10
 
11
+ from cognite_toolkit._cdf_tk.cruds._resource_cruds.transformation import TransformationCRUD
12
+ from cognite_toolkit._cdf_tk.data_classes._module_directories import ModuleLocation
11
13
  from cognite_toolkit._cdf_tk.exceptions import ToolkitValueError
12
14
  from cognite_toolkit._cdf_tk.feature_flags import Flags
13
15
 
14
- from ._module_directories import ModuleLocation
15
-
16
16
  if sys.version_info >= (3, 11):
17
17
  from typing import Self
18
18
  else:
@@ -161,16 +161,19 @@ class BuildVariables(tuple, Sequence[BuildVariable]):
161
161
  ]
162
162
 
163
163
  @overload
164
- def replace(self, content: str, file_suffix: str = ".yaml", use_placeholder: Literal[False] = False) -> str: ...
164
+ def replace(self, content: str, file_path: Path | None = None, use_placeholder: Literal[False] = False) -> str: ...
165
165
 
166
166
  @overload
167
167
  def replace(
168
- self, content: str, file_suffix: str = ".yaml", use_placeholder: Literal[True] = True
168
+ self, content: str, file_path: Path | None = None, use_placeholder: Literal[True] = True
169
169
  ) -> tuple[str, dict[str, BuildVariable]]: ...
170
170
 
171
171
  def replace(
172
- self, content: str, file_suffix: str = ".yaml", use_placeholder: bool = False
172
+ self, content: str, file_path: Path | None = None, use_placeholder: bool = False
173
173
  ) -> str | tuple[str, dict[str, BuildVariable]]:
174
+ # Extract file suffix from path, default to .yaml if not provided
175
+ file_suffix = file_path.suffix if file_path and file_path.suffix else ".yaml"
176
+
174
177
  variable_by_placeholder: dict[str, BuildVariable] = {}
175
178
  for variable in self:
176
179
  if not use_placeholder:
@@ -180,22 +183,125 @@ class BuildVariables(tuple, Sequence[BuildVariable]):
180
183
  variable_by_placeholder[replace] = variable
181
184
 
182
185
  _core_pattern = rf"{{{{\s*{variable.key}\s*}}}}"
183
- if file_suffix in {".yaml", ".yml", ".json"}:
184
- # Preserve data types
185
- pattern = _core_pattern
186
- if isinstance(replace, str) and (replace.isdigit() or replace.endswith(":")):
187
- replace = f'"{replace}"'
188
- pattern = rf"'{_core_pattern}'|{_core_pattern}|" + rf'"{_core_pattern}"'
189
- elif replace is None:
190
- replace = "null"
191
- content = re.sub(pattern, str(replace), content)
186
+ if file_suffix == ".sql":
187
+ # For SQL files, convert lists to SQL-style tuples
188
+ if isinstance(replace, list):
189
+ replace = self._format_list_as_sql_tuple(replace)
190
+ content = re.sub(_core_pattern, str(replace), content)
191
+ elif file_suffix in {".yaml", ".yml", ".json"}:
192
+ # Check if this is a transformation file (ends with Transformation.yaml/yml)
193
+ is_transformation_file = file_path is not None and f".{TransformationCRUD.kind}." in file_path.name
194
+ # Check if variable is within a query field (SQL context)
195
+ is_in_query_field = self._is_in_query_field(content, variable.key)
196
+
197
+ # For lists in query fields, use SQL-style tuples
198
+ # For transformation files, ensure SQL conversion is applied to query property variables
199
+ if is_transformation_file and is_in_query_field and isinstance(replace, list):
200
+ replace = self._format_list_as_sql_tuple(replace)
201
+ # Use simple pattern for SQL context (no YAML quoting needed)
202
+ content = re.sub(_core_pattern, str(replace), content)
203
+ else:
204
+ # Preserve data types for YAML
205
+ pattern = _core_pattern
206
+ if isinstance(replace, str) and (replace.isdigit() or replace.endswith(":")):
207
+ replace = f'"{replace}"'
208
+ pattern = rf"'{_core_pattern}'|{_core_pattern}|" + rf'"{_core_pattern}"'
209
+ elif replace is None:
210
+ replace = "null"
211
+ content = re.sub(pattern, str(replace), content)
192
212
  else:
213
+ # For other file types, use simple string replacement
193
214
  content = re.sub(_core_pattern, str(replace), content)
194
215
  if use_placeholder:
195
216
  return content, variable_by_placeholder
196
217
  else:
197
218
  return content
198
219
 
220
+ @staticmethod
221
+ def _is_transformation_file(file_path: Path) -> bool:
222
+ """Check if the file path indicates a transformation YAML file.
223
+
224
+ Transformation files are YAML files in the "transformations" folder.
225
+
226
+ Args:
227
+ file_path: The file path to check
228
+
229
+ Returns:
230
+ True if the file is a transformation YAML file
231
+ """
232
+ # Check if path contains "transformations" folder and ends with .yaml/.yml
233
+ path_str = file_path.as_posix().lower()
234
+ return "transformations" in path_str and file_path.suffix.lower() in {".yaml", ".yml"}
235
+
236
+ @staticmethod
237
+ def _format_list_as_sql_tuple(replace: list[Any]) -> str:
238
+ """Format a list as a SQL-style tuple string.
239
+
240
+ Args:
241
+ replace: The list to format
242
+
243
+ Returns:
244
+ SQL tuple string, e.g., "('A', 'B', 'C')" or "()" for empty lists
245
+ """
246
+ if not replace:
247
+ # Empty list becomes empty SQL tuple
248
+ return "()"
249
+ else:
250
+ # Format list as SQL tuple: ('A', 'B', 'C')
251
+ formatted_items = []
252
+ for item in replace:
253
+ if item is None:
254
+ formatted_items.append("NULL")
255
+ elif isinstance(item, str):
256
+ formatted_items.append(f"'{item}'")
257
+ else:
258
+ formatted_items.append(str(item))
259
+ return f"({', '.join(formatted_items)})"
260
+
261
+ @staticmethod
262
+ def _is_in_query_field(content: str, variable_key: str) -> bool:
263
+ """Check if a variable is within a query field in YAML.
264
+
265
+ Assumes query is a top-level property. This detects various YAML formats:
266
+ - query: >-
267
+ - query: |
268
+ - query: "..."
269
+ - query: ...
270
+ """
271
+ lines = content.split("\n")
272
+ variable_pattern = rf"{{{{\s*{re.escape(variable_key)}\s*}}}}"
273
+ in_query_field = False
274
+
275
+ for line in lines:
276
+ # Check if this line starts a top-level query field
277
+ query_match = re.match(r"^query\s*:\s*(.*)$", line)
278
+ if query_match:
279
+ in_query_field = True
280
+ query_content_start = query_match.group(1).strip()
281
+
282
+ # Check if variable is on the same line as query: declaration
283
+ if re.search(variable_pattern, line):
284
+ return True
285
+
286
+ # If query content starts on same line (not a block scalar), check it
287
+ if query_content_start and not query_content_start.startswith(("|", ">", "|-", ">-", "|+", ">+")):
288
+ if re.search(variable_pattern, query_content_start):
289
+ return True
290
+ continue
291
+
292
+ # Check if we're still in the query field
293
+ if in_query_field:
294
+ # If we hit another top-level property, we've exited the query field
295
+ if re.match(r"^\w+\s*:", line):
296
+ in_query_field = False
297
+ continue
298
+
299
+ # We're still in the query field, check for variable
300
+ if re.search(variable_pattern, line):
301
+ return True
302
+
303
+ return False
304
+
199
305
  # Implemented to get correct type hints
200
306
  def __iter__(self) -> Iterator[BuildVariable]:
201
307
  return super().__iter__()
@@ -158,7 +158,7 @@ class BuiltResourceFull(BuiltResource[T_ID]):
158
158
  def load_resource_dict(
159
159
  self, environment_variables: dict[str, str | None], validate: bool = False
160
160
  ) -> dict[str, Any]:
161
- content = self.build_variables.replace(safe_read(self.source.path))
161
+ content = self.build_variables.replace(safe_read(self.source.path), self.source.path)
162
162
  loader = cast(ResourceCRUD, get_crud(self.resource_dir, self.kind))
163
163
  raw = load_yaml_inject_variables(
164
164
  content,
@@ -0,0 +1,97 @@
1
+ import sys
2
+ from collections.abc import Collection, Iterator
3
+ from typing import Any, Generic, Protocol, TypeVar
4
+
5
+ from cognite.client import CogniteClient
6
+
7
+ if sys.version_info >= (3, 11):
8
+ from typing import Self
9
+ else:
10
+ from typing_extensions import Self
11
+
12
+
13
+ class ResourceRequestProtocol(Protocol):
14
+ @classmethod
15
+ def _load(cls, data: dict[str, Any]) -> Self: ...
16
+
17
+ def dump(self, camel_case: bool = True) -> dict[str, Any]: ...
18
+
19
+
20
+ class ResourceResponseProtocol(Protocol):
21
+ def as_write(self) -> ResourceRequestProtocol: ...
22
+
23
+
24
+ T_ResourceRequest = TypeVar("T_ResourceRequest", bound=ResourceRequestProtocol)
25
+ T_ResourceResponse = TypeVar("T_ResourceResponse", bound=ResourceResponseProtocol)
26
+
27
+
28
+ class ResourceRequestListProtocol(Protocol, Generic[T_ResourceRequest]):
29
+ def __init__(self, collection: Collection[T_ResourceRequest]): ...
30
+
31
+ def __getitem__(self, index: int) -> T_ResourceRequest: ...
32
+
33
+ def __setitem__(self, index: int, value: T_ResourceRequest) -> None: ...
34
+
35
+ def __delitem__(self, index: int) -> None: ...
36
+
37
+ def __len__(self) -> int: ...
38
+
39
+ def __iter__(self) -> Iterator[T_ResourceRequest]: ...
40
+
41
+ def __contains__(self, value: object) -> bool: ...
42
+
43
+ def __reversed__(self) -> Iterator[T_ResourceRequest]: ...
44
+
45
+ def insert(self, index: int, value: T_ResourceRequest) -> None: ...
46
+
47
+ def append(self, value: T_ResourceRequest) -> None: ...
48
+
49
+ def extend(self, values: Collection[T_ResourceRequest]) -> None: ...
50
+
51
+ def pop(self, index: int = -1) -> T_ResourceRequest: ...
52
+
53
+ def remove(self, value: T_ResourceRequest) -> None: ...
54
+
55
+ def clear(self) -> None: ...
56
+
57
+ @classmethod
58
+ def load(cls, data: list[dict[str, Any]], cognite_client: CogniteClient | None = None) -> Self: ...
59
+
60
+
61
+ class ResourceResponseListProtocol(Protocol, Generic[T_ResourceResponse]):
62
+ def __init__(self, collection: Collection[T_ResourceResponse]): ...
63
+
64
+ def __getitem__(self, index: int) -> T_ResourceResponse: ...
65
+
66
+ def __setitem__(self, index: int, value: T_ResourceResponse) -> None: ...
67
+
68
+ def __delitem__(self, index: int) -> None: ...
69
+
70
+ def __len__(self) -> int: ...
71
+
72
+ def __iter__(self) -> Iterator[T_ResourceResponse]: ...
73
+
74
+ def __contains__(self, value: object) -> bool: ...
75
+
76
+ def __reversed__(self) -> Iterator[T_ResourceResponse]: ...
77
+
78
+ def insert(self, index: int, value: T_ResourceResponse) -> None: ...
79
+
80
+ def append(self, value: T_ResourceResponse) -> None: ...
81
+
82
+ def extend(self, values: Collection[T_ResourceResponse]) -> None: ...
83
+
84
+ def pop(self, index: int = -1) -> T_ResourceResponse: ...
85
+
86
+ def remove(self, value: T_ResourceResponse) -> None: ...
87
+
88
+ def clear(self) -> None: ...
89
+
90
+ def as_write(self) -> ResourceRequestListProtocol: ...
91
+
92
+ @classmethod
93
+ def load(cls, data: list[dict[str, Any]], cognite_client: CogniteClient | None = None) -> Self: ...
94
+
95
+
96
+ T_ResourceRequestList = TypeVar("T_ResourceRequestList", bound=ResourceRequestListProtocol)
97
+ T_ResourceResponseList = TypeVar("T_ResourceResponseList", bound=ResourceResponseListProtocol)
@@ -27,6 +27,7 @@ from .hosted_extractor_destination import HostedExtractorDestinationYAML
27
27
  from .hosted_extractor_job import HostedExtractorJobYAML
28
28
  from .hosted_extractor_mapping import HostedExtractorMappingYAML
29
29
  from .hosted_extractor_source import HostedExtractorSourceYAML
30
+ from .infield_cdmv1 import InfieldLocationConfigYAML
30
31
  from .infield_v1 import InfieldV1YAML
31
32
  from .instance import EdgeYAML, NodeYAML
32
33
  from .labels import LabelsYAML
@@ -74,6 +75,7 @@ __all__ = [
74
75
  "HostedExtractorJobYAML",
75
76
  "HostedExtractorMappingYAML",
76
77
  "HostedExtractorSourceYAML",
78
+ "InfieldLocationConfigYAML",
77
79
  "InfieldV1YAML",
78
80
  "LabelsYAML",
79
81
  "LocationYAML",
@@ -55,3 +55,4 @@ class AgentYAML(ToolkitResource):
55
55
  "azure/gpt-4o-mini", description="The name of the model to use. Defaults to your CDF project's default model."
56
56
  )
57
57
  tools: list[AgentTool] | None = Field(None, description="A list of tools available to the agent.", max_length=20)
58
+ runtime_version: str | None = Field(None, description="The runtime version")
@@ -0,0 +1,94 @@
1
+ from typing import Any
2
+
3
+ from .base import BaseModelResource, ToolkitResource
4
+
5
+
6
+ class ObservationFeatureToggles(BaseModelResource):
7
+ """Feature toggles for observations."""
8
+
9
+ is_enabled: bool | None = None
10
+ is_write_back_enabled: bool | None = None
11
+ notifications_endpoint_external_id: str | None = None
12
+ attachments_endpoint_external_id: str | None = None
13
+
14
+
15
+ class FeatureToggles(BaseModelResource):
16
+ """Feature toggles for InField location configuration."""
17
+
18
+ three_d: bool | None = None
19
+ trends: bool | None = None
20
+ documents: bool | None = None
21
+ workorders: bool | None = None
22
+ notifications: bool | None = None
23
+ media: bool | None = None
24
+ template_checklist_flow: bool | None = None
25
+ workorder_checklist_flow: bool | None = None
26
+ observations: ObservationFeatureToggles | None = None
27
+
28
+
29
+ class AccessManagement(BaseModelResource):
30
+ """Access management configuration."""
31
+
32
+ template_admins: list[str] | None = None # list of CDF group external IDs
33
+ checklist_admins: list[str] | None = None # list of CDF group external IDs
34
+
35
+
36
+ class ResourceFilters(BaseModelResource):
37
+ """Resource filters."""
38
+
39
+ spaces: list[str] | None = None
40
+
41
+
42
+ class RootLocationDataFilters(BaseModelResource):
43
+ """Data filters for root location."""
44
+
45
+ general: ResourceFilters | None = None
46
+ assets: ResourceFilters | None = None
47
+ files: ResourceFilters | None = None
48
+ timeseries: ResourceFilters | None = None
49
+
50
+
51
+ class DataExplorationConfig(BaseModelResource):
52
+ """Properties for DataExplorationConfig node.
53
+
54
+ Contains configuration for data exploration features:
55
+ - observations: Observations feature configuration
56
+ - activities: Activities configuration
57
+ - documents: Document configuration
58
+ - notifications: Notifications configuration
59
+ - assets: Asset page configuration
60
+ """
61
+
62
+ space: str | None = None
63
+ external_id: str | None = None
64
+
65
+ observations: dict[str, Any] | None = None # ObservationsConfigFeature
66
+ activities: dict[str, Any] | None = None # ActivitiesConfiguration
67
+ documents: dict[str, Any] | None = None # DocumentConfiguration
68
+ notifications: dict[str, Any] | None = None # NotificationsConfiguration
69
+ assets: dict[str, Any] | None = None # AssetPageConfiguration
70
+
71
+
72
+ class InfieldLocationConfigYAML(ToolkitResource):
73
+ """Properties for InFieldLocationConfig node.
74
+
75
+ Currently migrated fields:
76
+ - root_location_external_id: Reference to the LocationFilterDTO external ID
77
+ - feature_toggles: Feature toggles migrated from old configuration
78
+ - rootAsset: Direct relation to the root asset (space and externalId)
79
+ - app_instance_space: Application instance space from appDataInstanceSpace
80
+ - access_management: Template and checklist admin groups (from templateAdmins and checklistAdmins)
81
+ - disciplines: List of disciplines (from disciplines in FeatureConfiguration)
82
+ - data_filters: Data filters for general, assets, files, and timeseries (from dataFilters in old configuration)
83
+ - data_exploration_config: Direct relation to the DataExplorationConfig node (shared across all locations)
84
+ """
85
+
86
+ space: str
87
+ external_id: str
88
+
89
+ root_location_external_id: str | None = None
90
+ feature_toggles: FeatureToggles | None = None
91
+ app_instance_space: str | None = None
92
+ access_management: AccessManagement | None = None
93
+ data_filters: RootLocationDataFilters | None = None
94
+ data_exploration_config: DataExplorationConfig | None = None
@@ -1,8 +1,10 @@
1
+ import hashlib
1
2
  import re
2
3
  from collections.abc import Hashable
3
4
 
4
5
  from rich.console import Console
5
6
 
7
+ from cognite_toolkit._cdf_tk.exceptions import ToolkitValueError
6
8
  from cognite_toolkit._cdf_tk.tk_warnings import LowSeverityWarning
7
9
 
8
10
  INVALID_TITLE_REGEX = re.compile(r"[\\*?:/\[\]]")
@@ -90,3 +92,24 @@ def to_sentence_case(text: str) -> str:
90
92
  text = re.sub(r"(?<=[a-z])([A-Z])", r" \1", text)
91
93
  # Convert to lowercase
92
94
  return text.casefold()
95
+
96
+
97
+ def sanitize_instance_external_id(external_id: str) -> str:
98
+ """Sanitize an instance external ID to be compatible with CDF requirements.
99
+
100
+ Args:
101
+ external_id: The external ID to sanitize.
102
+
103
+ Returns:
104
+ The sanitized external ID.
105
+ """
106
+ # CDF instance external IDs must be between 1 and 256 characters,
107
+ if not external_id or external_id == "\x00":
108
+ raise ToolkitValueError("External ID cannot be empty.")
109
+ elif len(external_id) <= 256:
110
+ return external_id
111
+ hasher = hashlib.sha256()
112
+ hasher.update(external_id.encode("utf-8"))
113
+ hash_digest = hasher.hexdigest()[:8]
114
+ sanitized_external_id = f"{external_id[:247]}_{hash_digest}"
115
+ return sanitized_external_id
@@ -12,7 +12,7 @@ jobs:
12
12
  environment: dev
13
13
  name: Deploy
14
14
  container:
15
- image: cognite/toolkit:0.6.89
15
+ image: cognite/toolkit:0.6.91
16
16
  env:
17
17
  CDF_CLUSTER: ${{ vars.CDF_CLUSTER }}
18
18
  CDF_PROJECT: ${{ vars.CDF_PROJECT }}
@@ -10,7 +10,7 @@ jobs:
10
10
  environment: dev
11
11
  name: Deploy Dry Run
12
12
  container:
13
- image: cognite/toolkit:0.6.89
13
+ image: cognite/toolkit:0.6.91
14
14
  env:
15
15
  CDF_CLUSTER: ${{ vars.CDF_CLUSTER }}
16
16
  CDF_PROJECT: ${{ vars.CDF_PROJECT }}
@@ -4,7 +4,7 @@ default_env = "<DEFAULT_ENV_PLACEHOLDER>"
4
4
  [modules]
5
5
  # This is the version of the modules. It should not be changed manually.
6
6
  # It will be updated by the 'cdf modules upgrade' command.
7
- version = "0.6.89"
7
+ version = "0.6.91"
8
8
 
9
9
  [alpha_flags]
10
10
  external-libraries = true
@@ -1 +1 @@
1
- __version__ = "0.6.89"
1
+ __version__ = "0.6.91"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cognite_toolkit
3
- Version: 0.6.89
3
+ Version: 0.6.91
4
4
  Summary: Official Cognite Data Fusion tool for project templates and configuration deployment
5
5
  Project-URL: Homepage, https://docs.cognite.com/cdf/deploy/cdf_toolkit/
6
6
  Project-URL: Changelog, https://github.com/cognitedata/toolkit/releases