cognite-toolkit 0.6.90__py3-none-any.whl → 0.6.92__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.
- cognite_toolkit/_cdf_tk/client/_toolkit_client.py +5 -0
- cognite_toolkit/_cdf_tk/client/api/infield.py +156 -0
- cognite_toolkit/_cdf_tk/client/data_classes/api_classes.py +17 -0
- cognite_toolkit/_cdf_tk/client/data_classes/base.py +63 -0
- cognite_toolkit/_cdf_tk/client/data_classes/infield.py +102 -0
- cognite_toolkit/_cdf_tk/client/data_classes/instance_api.py +157 -0
- cognite_toolkit/_cdf_tk/client/testing.py +3 -0
- cognite_toolkit/_cdf_tk/commands/_utils.py +1 -24
- cognite_toolkit/_cdf_tk/commands/clean.py +11 -5
- cognite_toolkit/_cdf_tk/commands/deploy.py +14 -10
- cognite_toolkit/_cdf_tk/commands/pull.py +13 -8
- cognite_toolkit/_cdf_tk/cruds/__init__.py +3 -0
- cognite_toolkit/_cdf_tk/cruds/_base_cruds.py +28 -25
- cognite_toolkit/_cdf_tk/cruds/_data_cruds.py +10 -7
- cognite_toolkit/_cdf_tk/cruds/_resource_cruds/__init__.py +2 -1
- cognite_toolkit/_cdf_tk/cruds/_resource_cruds/classic.py +15 -0
- cognite_toolkit/_cdf_tk/cruds/_resource_cruds/fieldops.py +111 -2
- cognite_toolkit/_cdf_tk/cruds/_worker.py +24 -20
- cognite_toolkit/_cdf_tk/protocols.py +97 -0
- cognite_toolkit/_cdf_tk/resource_classes/__init__.py +2 -0
- cognite_toolkit/_cdf_tk/resource_classes/infield_cdmv1.py +3 -1
- cognite_toolkit/_cdf_tk/storageio/_asset_centric.py +13 -0
- cognite_toolkit/_cdf_tk/utils/text.py +23 -0
- cognite_toolkit/_repo_files/GitHub/.github/workflows/deploy.yaml +1 -1
- cognite_toolkit/_repo_files/GitHub/.github/workflows/dry-run.yaml +1 -1
- cognite_toolkit/_resources/cdf.toml +1 -1
- cognite_toolkit/_version.py +1 -1
- {cognite_toolkit-0.6.90.dist-info → cognite_toolkit-0.6.92.dist-info}/METADATA +1 -1
- {cognite_toolkit-0.6.90.dist-info → cognite_toolkit-0.6.92.dist-info}/RECORD +32 -26
- {cognite_toolkit-0.6.90.dist-info → cognite_toolkit-0.6.92.dist-info}/WHEEL +0 -0
- {cognite_toolkit-0.6.90.dist-info → cognite_toolkit-0.6.92.dist-info}/entry_points.txt +0 -0
- {cognite_toolkit-0.6.90.dist-info → cognite_toolkit-0.6.92.dist-info}/licenses/LICENSE +0 -0
|
@@ -3,6 +3,8 @@ from typing import cast
|
|
|
3
3
|
from cognite.client import CogniteClient
|
|
4
4
|
from rich.console import Console
|
|
5
5
|
|
|
6
|
+
from cognite_toolkit._cdf_tk.utils.http_client import HTTPClient
|
|
7
|
+
|
|
6
8
|
from .api.canvas import CanvasAPI
|
|
7
9
|
from .api.charts import ChartsAPI
|
|
8
10
|
from .api.dml import DMLAPI
|
|
@@ -11,6 +13,7 @@ from .api.extended_files import ExtendedFileMetadataAPI
|
|
|
11
13
|
from .api.extended_functions import ExtendedFunctionsAPI
|
|
12
14
|
from .api.extended_raw import ExtendedRawAPI
|
|
13
15
|
from .api.extended_timeseries import ExtendedTimeSeriesAPI
|
|
16
|
+
from .api.infield import InfieldAPI
|
|
14
17
|
from .api.lookup import LookUpGroup
|
|
15
18
|
from .api.migration import MigrationAPI
|
|
16
19
|
from .api.project import ProjectAPI
|
|
@@ -24,6 +27,7 @@ from .config import ToolkitClientConfig
|
|
|
24
27
|
class ToolkitClient(CogniteClient):
|
|
25
28
|
def __init__(self, config: ToolkitClientConfig | None = None, enable_set_pending_ids: bool = False) -> None:
|
|
26
29
|
super().__init__(config=config)
|
|
30
|
+
http_client = HTTPClient(self.config)
|
|
27
31
|
toolkit_config = ToolkitClientConfig.from_client_config(self.config)
|
|
28
32
|
self.console = Console()
|
|
29
33
|
self.search = SearchAPI(self._config, self._API_VERSION, self)
|
|
@@ -42,6 +46,7 @@ class ToolkitClient(CogniteClient):
|
|
|
42
46
|
self.token = TokenAPI(self)
|
|
43
47
|
self.charts = ChartsAPI(self._config, self._API_VERSION, self)
|
|
44
48
|
self.project = ProjectAPI(config=toolkit_config, cognite_client=self)
|
|
49
|
+
self.infield = InfieldAPI(http_client, self.console)
|
|
45
50
|
|
|
46
51
|
@property
|
|
47
52
|
def config(self) -> ToolkitClientConfig:
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from typing import Any, cast
|
|
3
|
+
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
|
|
6
|
+
from cognite_toolkit._cdf_tk.client.data_classes.api_classes import PagedResponse, QueryResponse
|
|
7
|
+
from cognite_toolkit._cdf_tk.client.data_classes.infield import DataExplorationConfig, InfieldLocationConfig
|
|
8
|
+
from cognite_toolkit._cdf_tk.client.data_classes.instance_api import (
|
|
9
|
+
InstanceResponseItem,
|
|
10
|
+
InstanceResult,
|
|
11
|
+
NodeIdentifier,
|
|
12
|
+
)
|
|
13
|
+
from cognite_toolkit._cdf_tk.tk_warnings import HighSeverityWarning
|
|
14
|
+
from cognite_toolkit._cdf_tk.utils.http_client import HTTPClient, ItemsRequest, SimpleBodyRequest
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class InfieldConfigAPI:
|
|
18
|
+
ENDPOINT = "/models/instances"
|
|
19
|
+
LOCATION_REF = "locationConfig"
|
|
20
|
+
EXPLORATION_REF = "explorerConfig"
|
|
21
|
+
# We know that this key exists and it has alias set.
|
|
22
|
+
DATA_EXPLORATION_PROP_ID = cast(str, InfieldLocationConfig.model_fields["data_exploration_config"].alias)
|
|
23
|
+
|
|
24
|
+
def __init__(self, http_client: HTTPClient, console: Console) -> None:
|
|
25
|
+
self._http_client = http_client
|
|
26
|
+
self._console = console
|
|
27
|
+
self._config = http_client.config
|
|
28
|
+
|
|
29
|
+
def apply(self, items: Sequence[InfieldLocationConfig]) -> list[InstanceResult]:
|
|
30
|
+
if len(items) > 500:
|
|
31
|
+
raise ValueError("Cannot apply more than 500 InfieldLocationConfig items at once.")
|
|
32
|
+
|
|
33
|
+
request_items = (
|
|
34
|
+
[item.as_request_item()]
|
|
35
|
+
if item.data_exploration_config is None
|
|
36
|
+
else [item.as_request_item(), item.data_exploration_config.as_request_item()]
|
|
37
|
+
for item in items
|
|
38
|
+
)
|
|
39
|
+
responses = self._http_client.request_with_retries(
|
|
40
|
+
ItemsRequest(
|
|
41
|
+
endpoint_url=self._config.create_api_url(self.ENDPOINT),
|
|
42
|
+
method="POST",
|
|
43
|
+
items=[item for sublist in request_items for item in sublist],
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
responses.raise_for_status()
|
|
47
|
+
return PagedResponse[InstanceResult].model_validate(responses.get_first_body()).items
|
|
48
|
+
|
|
49
|
+
def retrieve(self, items: Sequence[NodeIdentifier]) -> list[InfieldLocationConfig]:
|
|
50
|
+
if len(items) > 100:
|
|
51
|
+
raise ValueError("Cannot retrieve more than 100 InfieldLocationConfig items at once.")
|
|
52
|
+
if not items:
|
|
53
|
+
return []
|
|
54
|
+
responses = self._http_client.request_with_retries(
|
|
55
|
+
SimpleBodyRequest(
|
|
56
|
+
# We use the query endpoint to be able to retrieve linked DataExplorationConfig items
|
|
57
|
+
endpoint_url=self._config.create_api_url(f"{self.ENDPOINT}/query"),
|
|
58
|
+
method="POST",
|
|
59
|
+
body_content=self._retrieve_query(items),
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
responses.raise_for_status()
|
|
63
|
+
parsed_response = QueryResponse[InstanceResponseItem].model_validate(responses.get_first_body())
|
|
64
|
+
return self._parse_retrieve_response(parsed_response)
|
|
65
|
+
|
|
66
|
+
def delete(self, items: Sequence[InfieldLocationConfig]) -> list[NodeIdentifier]:
|
|
67
|
+
if len(items) > 500:
|
|
68
|
+
raise ValueError("Cannot delete more than 500 InfieldLocationConfig items at once.")
|
|
69
|
+
|
|
70
|
+
identifiers = (
|
|
71
|
+
[item.as_id()]
|
|
72
|
+
if item.data_exploration_config is None
|
|
73
|
+
else [item.as_id(), item.data_exploration_config.as_id()]
|
|
74
|
+
for item in items
|
|
75
|
+
)
|
|
76
|
+
responses = self._http_client.request_with_retries(
|
|
77
|
+
ItemsRequest(
|
|
78
|
+
endpoint_url=self._config.create_api_url(f"{self.ENDPOINT}/delete"),
|
|
79
|
+
method="POST",
|
|
80
|
+
items=[identifier for sublist in identifiers for identifier in sublist],
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
responses.raise_for_status()
|
|
84
|
+
return PagedResponse[NodeIdentifier].model_validate(responses.get_first_body()).items
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def _retrieve_query(cls, items: Sequence[NodeIdentifier]) -> dict[str, Any]:
|
|
88
|
+
return {
|
|
89
|
+
"with": {
|
|
90
|
+
cls.LOCATION_REF: {
|
|
91
|
+
"limit": len(items),
|
|
92
|
+
"nodes": {
|
|
93
|
+
"filter": {
|
|
94
|
+
"instanceReferences": [
|
|
95
|
+
{"space": item.space, "externalId": item.external_id} for item in items
|
|
96
|
+
]
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
cls.EXPLORATION_REF: {
|
|
101
|
+
"nodes": {
|
|
102
|
+
"from": "locationConfig",
|
|
103
|
+
"direction": "outwards",
|
|
104
|
+
"through": {
|
|
105
|
+
"source": InfieldLocationConfig.VIEW_ID.dump(),
|
|
106
|
+
"identifier": cls.DATA_EXPLORATION_PROP_ID,
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
"select": {
|
|
112
|
+
cls.LOCATION_REF: {
|
|
113
|
+
"sources": [{"source": InfieldLocationConfig.VIEW_ID.dump(), "properties": ["*"]}],
|
|
114
|
+
},
|
|
115
|
+
cls.EXPLORATION_REF: {
|
|
116
|
+
"sources": [{"source": DataExplorationConfig.VIEW_ID.dump(), "properties": ["*"]}],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
def _parse_retrieve_response(
|
|
122
|
+
self, parsed_response: QueryResponse[InstanceResponseItem]
|
|
123
|
+
) -> list[InfieldLocationConfig]:
|
|
124
|
+
data_exploration_results = (
|
|
125
|
+
DataExplorationConfig.model_validate(
|
|
126
|
+
item.get_properties_for_source(DataExplorationConfig.VIEW_ID, include_identifier=True)
|
|
127
|
+
)
|
|
128
|
+
for item in parsed_response.items[self.EXPLORATION_REF]
|
|
129
|
+
)
|
|
130
|
+
data_exploration_config_map = {(dec.space, dec.external_id): dec for dec in data_exploration_results}
|
|
131
|
+
result: list[InfieldLocationConfig] = []
|
|
132
|
+
for item in parsed_response.items[self.LOCATION_REF]:
|
|
133
|
+
properties = item.get_properties_for_source(InfieldLocationConfig.VIEW_ID, include_identifier=True)
|
|
134
|
+
data_exploration = properties.pop(self.DATA_EXPLORATION_PROP_ID, None)
|
|
135
|
+
if isinstance(data_exploration, dict):
|
|
136
|
+
space = data_exploration["space"]
|
|
137
|
+
external_id = data_exploration["externalId"]
|
|
138
|
+
if (space, external_id) not in data_exploration_config_map:
|
|
139
|
+
HighSeverityWarning(
|
|
140
|
+
f"{self.DATA_EXPLORATION_PROP_ID} with space '{space}' and externalId '{external_id}' referenced in InfieldLocationConfig '{properties['externalId']}' was not found in the retrieved results."
|
|
141
|
+
).print_warning(console=self._console)
|
|
142
|
+
else:
|
|
143
|
+
# Pydantic allow already validated models to be assigned to fields
|
|
144
|
+
properties[self.DATA_EXPLORATION_PROP_ID] = data_exploration_config_map[(space, external_id)] # type: ignore[assignment,index]
|
|
145
|
+
else:
|
|
146
|
+
HighSeverityWarning(
|
|
147
|
+
f"InfieldLocationConfig '{properties['externalId']}' is missing a valid {self.DATA_EXPLORATION_PROP_ID} reference."
|
|
148
|
+
).print_warning(console=self._console)
|
|
149
|
+
result.append(InfieldLocationConfig.model_validate(properties))
|
|
150
|
+
return result
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class InfieldAPI:
|
|
154
|
+
def __init__(self, http_client: HTTPClient, console: Console) -> None:
|
|
155
|
+
self._http_client = http_client
|
|
156
|
+
self.config = InfieldConfigAPI(http_client, console)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import Generic, TypeVar
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field, JsonValue
|
|
4
|
+
|
|
5
|
+
T = TypeVar("T", bound=BaseModel)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PagedResponse(BaseModel, Generic[T]):
|
|
9
|
+
items: list[T]
|
|
10
|
+
next_cursor: str | None = Field(None, alias="nextCursor")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class QueryResponse(BaseModel, Generic[T]):
|
|
14
|
+
items: dict[str, list[T]]
|
|
15
|
+
typing: dict[str, JsonValue] | None = None
|
|
16
|
+
next_cursor: dict[str, str] = Field(alias="nextCursor")
|
|
17
|
+
debug: dict[str, JsonValue] | None = None
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from typing import Any, Generic, TypeVar
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict
|
|
6
|
+
from pydantic.alias_generators import to_camel
|
|
7
|
+
|
|
8
|
+
if sys.version_info >= (3, 11):
|
|
9
|
+
from typing import Self
|
|
10
|
+
else:
|
|
11
|
+
from typing_extensions import Self
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseModelObject(BaseModel):
|
|
15
|
+
"""Base class for all object. This includes resources and nested objects."""
|
|
16
|
+
|
|
17
|
+
# We allow extra fields to support forward compatibility.
|
|
18
|
+
model_config = ConfigDict(alias_generator=to_camel, extra="allow")
|
|
19
|
+
|
|
20
|
+
def dump(self, camel_case: bool = True) -> dict[str, Any]:
|
|
21
|
+
"""Dump the resource to a dictionary.
|
|
22
|
+
|
|
23
|
+
This is the default serialization method for request resources.
|
|
24
|
+
"""
|
|
25
|
+
return self.model_dump(mode="json", by_alias=camel_case)
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def _load(cls, resource: dict[str, Any]) -> "Self":
|
|
29
|
+
"""Load method to match CogniteResource signature."""
|
|
30
|
+
return cls.model_validate(resource)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class RequestResource(BaseModelObject): ...
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
T_RequestResource = TypeVar("T_RequestResource", bound=RequestResource)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ResponseResource(BaseModelObject, Generic[T_RequestResource], ABC):
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def as_request_resource(self) -> T_RequestResource:
|
|
42
|
+
"""Convert the response resource to a request resource."""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Identifier(BaseModel):
|
|
47
|
+
"""Base class for all identifier classes."""
|
|
48
|
+
|
|
49
|
+
model_config = ConfigDict(alias_generator=to_camel, extra="ignore", populate_by_name=True, frozen=True)
|
|
50
|
+
|
|
51
|
+
def dump(self, include_type: bool = True) -> dict[str, Any]:
|
|
52
|
+
"""Dump the identifier to a dictionary.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
include_type (bool): Whether to include the type of the identifier in the output.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
dict[str, Any]: The dumped identifier.
|
|
59
|
+
"""
|
|
60
|
+
return self.model_dump(mode="json", by_alias=True, exclude_defaults=not include_type)
|
|
61
|
+
|
|
62
|
+
def as_id(self) -> Self:
|
|
63
|
+
return self
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from collections import UserList
|
|
3
|
+
from typing import Any, ClassVar, Literal
|
|
4
|
+
|
|
5
|
+
from cognite.client import CogniteClient
|
|
6
|
+
from pydantic import JsonValue, field_validator
|
|
7
|
+
from pydantic_core.core_schema import ValidationInfo
|
|
8
|
+
|
|
9
|
+
from cognite_toolkit._cdf_tk.protocols import ResourceRequestListProtocol, ResourceResponseListProtocol
|
|
10
|
+
from cognite_toolkit._cdf_tk.utils.text import sanitize_instance_external_id
|
|
11
|
+
|
|
12
|
+
from .base import ResponseResource
|
|
13
|
+
from .instance_api import InstanceRequestResource, ViewReference
|
|
14
|
+
|
|
15
|
+
if sys.version_info >= (3, 11):
|
|
16
|
+
from typing import Self
|
|
17
|
+
else:
|
|
18
|
+
from typing_extensions import Self
|
|
19
|
+
|
|
20
|
+
INFIELD_LOCATION_CONFIG_VIEW_ID = ViewReference(space="cdf_infield", external_id="InFieldLocationConfig", version="v1")
|
|
21
|
+
DATA_EXPLORATION_CONFIG_VIEW_ID = ViewReference(space="cdf_infield", external_id="DataExplorationConfig", version="v1")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DataExplorationConfig(InstanceRequestResource):
|
|
25
|
+
"""Data Exploration Configuration resource class."""
|
|
26
|
+
|
|
27
|
+
VIEW_ID: ClassVar[ViewReference] = DATA_EXPLORATION_CONFIG_VIEW_ID
|
|
28
|
+
instance_type: Literal["node"] = "node"
|
|
29
|
+
|
|
30
|
+
observations: dict[str, JsonValue] | None = None
|
|
31
|
+
activities: dict[str, JsonValue] | None = None
|
|
32
|
+
documents: dict[str, JsonValue] | None = None
|
|
33
|
+
notifications: dict[str, JsonValue] | None = None
|
|
34
|
+
assets: dict[str, JsonValue] | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class InfieldLocationConfig(
|
|
38
|
+
ResponseResource["InfieldLocationConfig"],
|
|
39
|
+
InstanceRequestResource,
|
|
40
|
+
):
|
|
41
|
+
"""Infield Location Configuration resource class.
|
|
42
|
+
|
|
43
|
+
This class is used for both the response and request resource for Infield Location Configuration nodes.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
VIEW_ID: ClassVar[ViewReference] = INFIELD_LOCATION_CONFIG_VIEW_ID
|
|
47
|
+
instance_type: Literal["node"] = "node"
|
|
48
|
+
|
|
49
|
+
root_location_external_id: str | None = None
|
|
50
|
+
feature_toggles: dict[str, JsonValue] | None = None
|
|
51
|
+
app_instance_space: str | None = None
|
|
52
|
+
access_management: dict[str, JsonValue] | None = None
|
|
53
|
+
data_filters: dict[str, JsonValue] | None = None
|
|
54
|
+
data_exploration_config: DataExplorationConfig | None = None
|
|
55
|
+
|
|
56
|
+
def as_request_resource(self) -> "InfieldLocationConfig":
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
def as_write(self) -> Self:
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
@field_validator("data_exploration_config", mode="before")
|
|
63
|
+
@classmethod
|
|
64
|
+
def generate_identifier_if_missing(cls, value: Any, info: ValidationInfo) -> Any:
|
|
65
|
+
"""We do not require the user to specify the space and externalId for the data exploration config."""
|
|
66
|
+
if isinstance(value, dict):
|
|
67
|
+
if value.get("space") is None:
|
|
68
|
+
value["space"] = info.data["space"]
|
|
69
|
+
if value.get("externalId") is None:
|
|
70
|
+
external_id = info.data["external_id"]
|
|
71
|
+
candidate = f"{external_id}_data_exploration_config"
|
|
72
|
+
value["externalId"] = sanitize_instance_external_id(candidate)
|
|
73
|
+
return value
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class InfieldLocationConfigList(
|
|
77
|
+
UserList[InfieldLocationConfig],
|
|
78
|
+
ResourceResponseListProtocol,
|
|
79
|
+
ResourceRequestListProtocol,
|
|
80
|
+
):
|
|
81
|
+
"""A list of InfieldLocationConfig objects."""
|
|
82
|
+
|
|
83
|
+
_RESOURCE = InfieldLocationConfig
|
|
84
|
+
data: list[InfieldLocationConfig]
|
|
85
|
+
|
|
86
|
+
def __init__(self, initlist: list[InfieldLocationConfig] | None = None, **_: Any) -> None:
|
|
87
|
+
super().__init__(initlist or [])
|
|
88
|
+
|
|
89
|
+
def dump(self, camel_case: bool = True) -> list[dict[str, Any]]:
|
|
90
|
+
"""Serialize the list of InfieldLocationConfig objects to a list of dictionaries."""
|
|
91
|
+
return [item.dump(camel_case) for item in self.data]
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def load(
|
|
95
|
+
cls, data: list[dict[str, Any]], cognite_client: CogniteClient | None = None
|
|
96
|
+
) -> "InfieldLocationConfigList":
|
|
97
|
+
"""Deserialize a list of dictionaries to an InfieldLocationConfigList."""
|
|
98
|
+
items = [InfieldLocationConfig.model_validate(item) for item in data]
|
|
99
|
+
return cls(items)
|
|
100
|
+
|
|
101
|
+
def as_write(self) -> Self:
|
|
102
|
+
return self
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from typing import Any, ClassVar, Literal, TypeAlias
|
|
2
|
+
|
|
3
|
+
from pydantic import ConfigDict, JsonValue, model_serializer
|
|
4
|
+
|
|
5
|
+
from .base import BaseModelObject, Identifier, RequestResource
|
|
6
|
+
|
|
7
|
+
InstanceType: TypeAlias = Literal["node", "edge"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class InstanceIdentifier(Identifier):
|
|
11
|
+
"""Identifier for an Instance instance."""
|
|
12
|
+
|
|
13
|
+
instance_type: InstanceType
|
|
14
|
+
space: str
|
|
15
|
+
external_id: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class NodeIdentifier(InstanceIdentifier):
|
|
19
|
+
instance_type: Literal["node"] = "node"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class EdgeIdentifier(InstanceIdentifier):
|
|
23
|
+
instance_type: Literal["edge"] = "edge"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class InstanceResult(BaseModelObject):
|
|
27
|
+
instance_type: InstanceType
|
|
28
|
+
version: int
|
|
29
|
+
was_modified: bool
|
|
30
|
+
space: str
|
|
31
|
+
external_id: str
|
|
32
|
+
created_time: int
|
|
33
|
+
last_updated_time: int
|
|
34
|
+
|
|
35
|
+
def as_id(self) -> InstanceIdentifier:
|
|
36
|
+
return InstanceIdentifier(
|
|
37
|
+
instance_type=self.instance_type,
|
|
38
|
+
space=self.space,
|
|
39
|
+
external_id=self.external_id,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ViewReference(Identifier):
|
|
44
|
+
type: Literal["view"] = "view"
|
|
45
|
+
space: str
|
|
46
|
+
external_id: str
|
|
47
|
+
version: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
######################################################
|
|
51
|
+
# The classes below are helper classes for making instances request/responses.
|
|
52
|
+
# By using these, we can avoid having to include the instances specific classes in the DTO classes
|
|
53
|
+
# that are instance. Instead, these classes can now only have the properties they need to define.
|
|
54
|
+
#######################################################
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class InstanceRequestResource(RequestResource):
|
|
58
|
+
"""This is a base class for resources that are Instances."""
|
|
59
|
+
|
|
60
|
+
VIEW_ID: ClassVar[ViewReference]
|
|
61
|
+
instance_type: InstanceType
|
|
62
|
+
space: str
|
|
63
|
+
external_id: str
|
|
64
|
+
|
|
65
|
+
def as_id(self) -> InstanceIdentifier:
|
|
66
|
+
return InstanceIdentifier(
|
|
67
|
+
instance_type=self.instance_type,
|
|
68
|
+
space=self.space,
|
|
69
|
+
external_id=self.external_id,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def as_request_item(self) -> "InstanceRequestItem":
|
|
73
|
+
return InstanceRequestItem(
|
|
74
|
+
instance_type=self.instance_type,
|
|
75
|
+
space=self.space,
|
|
76
|
+
external_id=self.external_id,
|
|
77
|
+
sources=[InstanceSource(source=self.VIEW_ID, resource=self)],
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class InstanceSource(BaseModelObject):
|
|
82
|
+
source: ViewReference
|
|
83
|
+
resource: InstanceRequestResource
|
|
84
|
+
|
|
85
|
+
@model_serializer(mode="plain")
|
|
86
|
+
def serialize_resource(self) -> dict[str, Any]:
|
|
87
|
+
properties: dict[str, JsonValue] = {}
|
|
88
|
+
for field_id, field in type(self.resource).model_fields.items():
|
|
89
|
+
if field_id in InstanceRequestResource.model_fields:
|
|
90
|
+
# Skip space, external_id, instance_type
|
|
91
|
+
continue
|
|
92
|
+
key = field.alias or field_id
|
|
93
|
+
properties[key] = self._serialize_property(getattr(self.resource, field_id))
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
"source": self.source.model_dump(by_alias=True),
|
|
97
|
+
"properties": properties,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def _serialize_property(cls, value: Any) -> JsonValue:
|
|
102
|
+
"""Handles serialization of direct relations."""
|
|
103
|
+
if isinstance(value, InstanceRequestResource):
|
|
104
|
+
return {"space": value.space, "externalId": value.external_id}
|
|
105
|
+
elif isinstance(value, list):
|
|
106
|
+
return [cls._serialize_property(v) for v in value]
|
|
107
|
+
return value
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class InstanceRequestItem(BaseModelObject):
|
|
111
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
112
|
+
instance_type: InstanceType
|
|
113
|
+
space: str
|
|
114
|
+
external_id: str
|
|
115
|
+
existing_version: int | None = None
|
|
116
|
+
sources: list[InstanceSource] | None = None
|
|
117
|
+
|
|
118
|
+
def as_id(self) -> InstanceIdentifier:
|
|
119
|
+
return InstanceIdentifier(
|
|
120
|
+
instance_type=self.instance_type,
|
|
121
|
+
space=self.space,
|
|
122
|
+
external_id=self.external_id,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class InstanceResponseItem(BaseModelObject):
|
|
127
|
+
instance_type: InstanceType
|
|
128
|
+
space: str
|
|
129
|
+
external_id: str
|
|
130
|
+
version: int
|
|
131
|
+
type: InstanceIdentifier | None = None
|
|
132
|
+
created_time: int
|
|
133
|
+
last_updated_time: int
|
|
134
|
+
deleted_time: int | None = None
|
|
135
|
+
properties: dict[str, dict[str, dict[str, JsonValue]]] | None = None
|
|
136
|
+
|
|
137
|
+
def get_properties_for_source(
|
|
138
|
+
self, source: ViewReference, include_identifier: bool = False
|
|
139
|
+
) -> dict[str, JsonValue]:
|
|
140
|
+
output: dict[str, JsonValue] = (
|
|
141
|
+
{"space": self.space, "externalId": self.external_id} if include_identifier else {}
|
|
142
|
+
)
|
|
143
|
+
if not self.properties:
|
|
144
|
+
return output
|
|
145
|
+
if source.space not in self.properties:
|
|
146
|
+
return output
|
|
147
|
+
space_properties = self.properties[source.space]
|
|
148
|
+
view_version = f"{source.external_id}/{source.version}"
|
|
149
|
+
output.update(space_properties.get(view_version, {}))
|
|
150
|
+
return output
|
|
151
|
+
|
|
152
|
+
def as_id(self) -> InstanceIdentifier:
|
|
153
|
+
return InstanceIdentifier(
|
|
154
|
+
instance_type=self.instance_type,
|
|
155
|
+
space=self.space,
|
|
156
|
+
external_id=self.external_id,
|
|
157
|
+
)
|
|
@@ -21,6 +21,7 @@ from .api.extended_files import ExtendedFileMetadataAPI
|
|
|
21
21
|
from .api.extended_functions import ExtendedFunctionsAPI
|
|
22
22
|
from .api.extended_raw import ExtendedRawAPI
|
|
23
23
|
from .api.extended_timeseries import ExtendedTimeSeriesAPI
|
|
24
|
+
from .api.infield import InfieldAPI, InfieldConfigAPI
|
|
24
25
|
from .api.location_filters import LocationFiltersAPI
|
|
25
26
|
from .api.lookup import (
|
|
26
27
|
AssetLookUpAPI,
|
|
@@ -73,6 +74,8 @@ class ToolkitClientMock(CogniteClientMock):
|
|
|
73
74
|
self.functions = MagicMock(spec=ExtendedFunctionsAPI)
|
|
74
75
|
self.functions.calls = MagicMock(spec_set=FunctionCallsAPI)
|
|
75
76
|
self.functions.schedules = MagicMock(spec_set=FunctionSchedulesAPI)
|
|
77
|
+
self.infield = MagicMock(spec=InfieldAPI)
|
|
78
|
+
self.infield.config = MagicMock(spec_set=InfieldConfigAPI)
|
|
76
79
|
|
|
77
80
|
self.project = MagicMock(spec_set=ProjectAPI)
|
|
78
81
|
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
from cognite.client.data_classes._base import T_CogniteResourceList, T_WritableCogniteResource, T_WriteClass
|
|
2
1
|
from cognite.client.utils.useful_types import SequenceNotStr
|
|
3
2
|
|
|
4
|
-
from cognite_toolkit._cdf_tk.cruds import
|
|
5
|
-
ResourceCRUD,
|
|
6
|
-
)
|
|
7
|
-
from cognite_toolkit._cdf_tk.cruds._base_cruds import T_ID, T_WritableCogniteResourceList
|
|
3
|
+
from cognite_toolkit._cdf_tk.cruds._base_cruds import T_ID
|
|
8
4
|
|
|
9
5
|
|
|
10
6
|
def _print_ids_or_length(resource_ids: SequenceNotStr[T_ID], limit: int = 10) -> str:
|
|
@@ -14,22 +10,3 @@ def _print_ids_or_length(resource_ids: SequenceNotStr[T_ID], limit: int = 10) ->
|
|
|
14
10
|
return f"{resource_ids}"
|
|
15
11
|
else:
|
|
16
12
|
return f"{len(resource_ids)} items"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _remove_duplicates(
|
|
20
|
-
loaded_resources: T_CogniteResourceList,
|
|
21
|
-
loader: ResourceCRUD[
|
|
22
|
-
T_ID, T_WriteClass, T_WritableCogniteResource, T_CogniteResourceList, T_WritableCogniteResourceList
|
|
23
|
-
],
|
|
24
|
-
) -> tuple[T_CogniteResourceList, list[T_ID]]:
|
|
25
|
-
seen: set[T_ID] = set()
|
|
26
|
-
output = loader.list_write_cls([])
|
|
27
|
-
duplicates: list[T_ID] = []
|
|
28
|
-
for item in loaded_resources:
|
|
29
|
-
identifier = loader.get_id(item)
|
|
30
|
-
if identifier not in seen:
|
|
31
|
-
output.append(item)
|
|
32
|
-
seen.add(identifier)
|
|
33
|
-
else:
|
|
34
|
-
duplicates.append(identifier)
|
|
35
|
-
return output, duplicates
|
|
@@ -2,7 +2,6 @@ import traceback
|
|
|
2
2
|
from graphlib import TopologicalSorter
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
-
from cognite.client.data_classes._base import T_CogniteResourceList, T_WritableCogniteResource, T_WriteClass
|
|
6
5
|
from cognite.client.exceptions import CogniteAPIError, CogniteNotFoundError
|
|
7
6
|
from cognite.client.utils.useful_types import SequenceNotStr
|
|
8
7
|
from rich import print
|
|
@@ -24,7 +23,14 @@ from cognite_toolkit._cdf_tk.cruds import (
|
|
|
24
23
|
ResourceCRUD,
|
|
25
24
|
ResourceWorker,
|
|
26
25
|
)
|
|
27
|
-
from cognite_toolkit._cdf_tk.cruds._base_cruds import
|
|
26
|
+
from cognite_toolkit._cdf_tk.cruds._base_cruds import (
|
|
27
|
+
T_ID,
|
|
28
|
+
Loader,
|
|
29
|
+
T_ResourceRequest,
|
|
30
|
+
T_ResourceRequestList,
|
|
31
|
+
T_ResourceResponse,
|
|
32
|
+
T_ResourceResponseList,
|
|
33
|
+
)
|
|
28
34
|
from cognite_toolkit._cdf_tk.data_classes import (
|
|
29
35
|
BuildEnvironment,
|
|
30
36
|
DeployResults,
|
|
@@ -58,7 +64,7 @@ class CleanCommand(ToolkitCommand):
|
|
|
58
64
|
def clean_resources(
|
|
59
65
|
self,
|
|
60
66
|
loader: ResourceCRUD[
|
|
61
|
-
T_ID,
|
|
67
|
+
T_ID, T_ResourceRequest, T_ResourceResponse, T_ResourceRequestList, T_ResourceResponseList
|
|
62
68
|
],
|
|
63
69
|
env_vars: EnvironmentVariables,
|
|
64
70
|
read_modules: list[ReadModule],
|
|
@@ -130,7 +136,7 @@ class CleanCommand(ToolkitCommand):
|
|
|
130
136
|
return ResourceDeployResult(name=loader.display_name)
|
|
131
137
|
|
|
132
138
|
def _delete_resources(
|
|
133
|
-
self, loaded_resources:
|
|
139
|
+
self, loaded_resources: T_ResourceResponseList, loader: ResourceCRUD, dry_run: bool, verbose: bool
|
|
134
140
|
) -> int:
|
|
135
141
|
nr_of_deleted = 0
|
|
136
142
|
resource_ids = loader.get_ids(loaded_resources)
|
|
@@ -155,7 +161,7 @@ class CleanCommand(ToolkitCommand):
|
|
|
155
161
|
return nr_of_deleted
|
|
156
162
|
|
|
157
163
|
def _drop_data(
|
|
158
|
-
self, loaded_resources:
|
|
164
|
+
self, loaded_resources: T_ResourceResponseList, loader: ResourceContainerCRUD, dry_run: bool, verbose: bool
|
|
159
165
|
) -> int:
|
|
160
166
|
nr_of_dropped = 0
|
|
161
167
|
resource_ids = loader.get_ids(loaded_resources)
|