cognite-neat 1.0.25__py3-none-any.whl → 1.0.26__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.
@@ -1,8 +1,178 @@
1
+ from abc import ABC, abstractmethod
2
+ from collections.abc import Iterable, Sequence
3
+ from dataclasses import dataclass
4
+ from typing import Any, Generic, Literal, TypeAlias, TypeVar
5
+
6
+ from pydantic import BaseModel, JsonValue, TypeAdapter
7
+
1
8
  from cognite.neat._client.config import NeatClientConfig
2
- from cognite.neat._utils.http_client import HTTPClient
9
+ from cognite.neat._data_model.models.dms._base import T_Resource, T_Response
10
+ from cognite.neat._utils.collection import chunker_sequence
11
+ from cognite.neat._utils.http_client import HTTPClient, ParametersRequest, SimpleBodyRequest, SuccessResponse
12
+ from cognite.neat._utils.useful_types import T_Reference
13
+
14
+ from .data_classes import PagedResponse
15
+
16
+ _T_BaseModel = TypeVar("_T_BaseModel", bound=BaseModel)
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class Endpoint:
21
+ method: Literal["GET", "POST"]
22
+ path: str
23
+ item_limit: int
3
24
 
4
25
 
5
- class NeatAPI:
6
- def __init__(self, neat_config: NeatClientConfig, http_client: HTTPClient) -> None:
26
+ APIMethod: TypeAlias = Literal["apply", "retrieve", "delete", "list"]
27
+
28
+
29
+ class NeatAPI(Generic[T_Reference, T_Resource, T_Response], ABC):
30
+ def __init__(
31
+ self, neat_config: NeatClientConfig, http_client: HTTPClient, endpoint_map: dict[APIMethod, Endpoint]
32
+ ) -> None:
7
33
  self._config = neat_config
8
34
  self._http_client = http_client
35
+ self._method_endpoint_map = endpoint_map
36
+
37
+ @abstractmethod
38
+ def _validate_page_response(self, response: SuccessResponse) -> PagedResponse[T_Response]:
39
+ """Parse a single item response."""
40
+ raise NotImplementedError()
41
+
42
+ @abstractmethod
43
+ def _validate_id_response(self, response: SuccessResponse) -> list[T_Reference]:
44
+ """Parse a single item response."""
45
+ raise NotImplementedError()
46
+
47
+ def _make_url(self, path: str = "") -> str:
48
+ """Create the full URL for this resource endpoint."""
49
+ return self._config.create_api_url(path)
50
+
51
+ def _request_item_response(
52
+ self,
53
+ items: Sequence[BaseModel],
54
+ method: APIMethod,
55
+ extra_body: dict[str, Any] | None = None,
56
+ ) -> list[T_Response]:
57
+ response_items: list[T_Response] = []
58
+ for response in self._chunk_requests(items, method, extra_body):
59
+ response_items.extend(self._validate_page_response(response).items)
60
+ return response_items
61
+
62
+ def _request_id_response(
63
+ self,
64
+ items: Sequence[BaseModel],
65
+ method: APIMethod,
66
+ extra_body: dict[str, Any] | None = None,
67
+ ) -> list[T_Reference]:
68
+ response_items: list[T_Reference] = []
69
+ for response in self._chunk_requests(items, method, extra_body):
70
+ response_items.extend(self._validate_id_response(response))
71
+ return response_items
72
+
73
+ def _chunk_requests(
74
+ self,
75
+ items: Sequence[_T_BaseModel],
76
+ method: APIMethod,
77
+ extra_body: dict[str, Any] | None = None,
78
+ ) -> Iterable[SuccessResponse]:
79
+ """Send requests in chunks and yield responses.
80
+
81
+ Args:
82
+ items: The items to process.
83
+ method: The API method to use. This is used ot look the up the endpoint.
84
+ extra_body: Optional extra body content to include in the request.
85
+
86
+ Yields:
87
+ The successful responses from the API.
88
+ """
89
+ # Filter out None
90
+ endpoint = self._method_endpoint_map[method]
91
+
92
+ for chunk in chunker_sequence(items, endpoint.item_limit):
93
+ request = SimpleBodyRequest(
94
+ endpoint_url=self._make_url(endpoint.path),
95
+ method=endpoint.method,
96
+ body=TypeAdapter(dict[str, JsonValue]).dump_json(
97
+ {
98
+ "items": [item.model_dump(by_alias=True) for item in chunk],
99
+ **(extra_body or {}),
100
+ },
101
+ ),
102
+ )
103
+ response = self._http_client.request_with_retries(request)
104
+ response.raise_for_status()
105
+ yield response.success_response
106
+
107
+ @classmethod
108
+ def _filter_out_none_values(cls, params: dict[str, Any] | None) -> dict[str, Any] | None:
109
+ request_params: dict[str, Any] | None = None
110
+ if params:
111
+ request_params = {k: v for k, v in params.items() if v is not None}
112
+ return request_params
113
+
114
+ def _paginate(
115
+ self,
116
+ limit: int,
117
+ cursor: str | None = None,
118
+ params: dict[str, Any] | None = None,
119
+ ) -> PagedResponse[T_Response]:
120
+ """Fetch a single page of resources.
121
+
122
+ Args:
123
+ params: Query parameters for the request. Supported parameters depend on
124
+ the resource type but typically include:
125
+ - cursor: Cursor for pagination
126
+ - limit: Maximum number of items (defaults to list limit)
127
+ - space: Filter by space
128
+ - includeGlobal: Whether to include global resources
129
+ limit: Maximum number of items to return in the page.
130
+ cursor: Cursor for pagination.
131
+
132
+ Returns:
133
+ A Page containing the items and the cursor for the next page.
134
+ """
135
+ endpoint = self._method_endpoint_map["list"]
136
+ if not (0 < limit <= endpoint.item_limit):
137
+ raise ValueError(f"Limit must be between 1 and {endpoint.item_limit}, got {limit}.")
138
+ if endpoint.method != "GET":
139
+ raise NotImplementedError(f"Pagination not implemented for method {endpoint.method}.")
140
+ request_params = self._filter_out_none_values(params) or {}
141
+ request_params["limit"] = limit
142
+ if cursor is not None:
143
+ request_params["cursor"] = cursor
144
+ request = ParametersRequest(
145
+ endpoint_url=self._make_url(endpoint.path),
146
+ method=endpoint.method,
147
+ parameters=request_params,
148
+ )
149
+ result = self._http_client.request_with_retries(request)
150
+ result.raise_for_status()
151
+ return self._validate_page_response(result.success_response)
152
+
153
+ def _iterate(
154
+ self,
155
+ limit: int | None = None,
156
+ cursor: str | None = None,
157
+ params: dict[str, Any] | None = None,
158
+ ) -> Iterable[list[T_Response]]:
159
+ """Iterate over all resources, handling pagination automatically."""
160
+ next_cursor = cursor
161
+ total = 0
162
+ endpoint = self._method_endpoint_map["list"]
163
+ while True:
164
+ page_limit = endpoint.item_limit if limit is None else min(limit - total, endpoint.item_limit)
165
+ page = self._paginate(limit=page_limit, cursor=next_cursor, params=params)
166
+ yield page.items
167
+ total += len(page.items)
168
+ if page.next_cursor is None or (limit is not None and total >= limit):
169
+ break
170
+ next_cursor = page.next_cursor
171
+
172
+ def _list(
173
+ self,
174
+ limit: int | None = None,
175
+ params: dict[str, Any] | None = None,
176
+ ) -> list[T_Response]:
177
+ """List all resources, handling pagination automatically."""
178
+ return [item for batch in self._iterate(limit=limit, params=params) for item in batch]
@@ -1,18 +1,32 @@
1
- from __future__ import annotations
2
-
3
1
  from collections.abc import Sequence
4
2
 
5
- from cognite.neat._data_model.models.dms import ContainerReference, ContainerRequest, ContainerResponse, DataModelBody
6
- from cognite.neat._utils.http_client import ItemIDBody, ItemsRequest, ParametersRequest
7
- from cognite.neat._utils.useful_types import PrimitiveType
3
+ from cognite.neat._data_model.models.dms import ContainerReference, ContainerRequest, ContainerResponse
4
+ from cognite.neat._utils.http_client import HTTPClient, SuccessResponse
8
5
 
9
- from .api import NeatAPI
6
+ from .api import Endpoint, NeatAPI
7
+ from .config import NeatClientConfig
10
8
  from .data_classes import PagedResponse
9
+ from .filters import ContainerFilter
11
10
 
12
11
 
13
12
  class ContainersAPI(NeatAPI):
14
- ENDPOINT = "/models/containers"
15
- LIST_REQUEST_LIMIT = 1000
13
+ def __init__(self, neat_config: NeatClientConfig, http_client: HTTPClient) -> None:
14
+ super().__init__(
15
+ neat_config,
16
+ http_client,
17
+ endpoint_map={
18
+ "apply": Endpoint("POST", "models/containers", item_limit=100),
19
+ "retrieve": Endpoint("POST", "models/containers/byids", item_limit=100),
20
+ "delete": Endpoint("POST", "models/containers/delete", item_limit=100),
21
+ "list": Endpoint("GET", "models/containers", item_limit=1000),
22
+ },
23
+ )
24
+
25
+ def _validate_page_response(self, response: SuccessResponse) -> PagedResponse[ContainerResponse]:
26
+ return PagedResponse[ContainerResponse].model_validate_json(response.body)
27
+
28
+ def _validate_id_response(self, response: SuccessResponse) -> list[ContainerReference]:
29
+ return PagedResponse[ContainerReference].model_validate_json(response.body).items
16
30
 
17
31
  def apply(self, items: Sequence[ContainerRequest]) -> list[ContainerResponse]:
18
32
  """Apply (create or update) containers in CDF.
@@ -22,25 +36,9 @@ class ContainersAPI(NeatAPI):
22
36
  Returns:
23
37
  List of ContainerResponse objects.
24
38
  """
25
- if not items:
26
- return []
27
- if len(items) > 100:
28
- raise ValueError("Cannot apply more than 100 containers at once.")
29
- result = self._http_client.request_with_retries(
30
- ItemsRequest(
31
- endpoint_url=self._config.create_api_url(self.ENDPOINT),
32
- method="POST",
33
- body=DataModelBody(items=items),
34
- )
35
- )
36
- result.raise_for_status()
37
- result = PagedResponse[ContainerResponse].model_validate_json(result.success_response.body)
38
- return result.items
39
+ return self._request_item_response(items, "apply")
39
40
 
40
- def retrieve(
41
- self,
42
- items: list[ContainerReference],
43
- ) -> list[ContainerResponse]:
41
+ def retrieve(self, items: list[ContainerReference]) -> list[ContainerResponse]:
44
42
  """Retrieve containers by their identifiers.
45
43
 
46
44
  Args:
@@ -49,21 +47,7 @@ class ContainersAPI(NeatAPI):
49
47
  Returns:
50
48
  List of ContainerResponse objects.
51
49
  """
52
- if not items:
53
- return []
54
- if len(items) > 1000:
55
- raise ValueError("Cannot retrieve more than 1000 containers at once.")
56
-
57
- result = self._http_client.request_with_retries(
58
- ItemsRequest(
59
- endpoint_url=self._config.create_api_url(f"{self.ENDPOINT}/byids"),
60
- method="POST",
61
- body=ItemIDBody(items=items),
62
- )
63
- )
64
- result.raise_for_status()
65
- result = PagedResponse[ContainerResponse].model_validate_json(result.success_response.body)
66
- return result.items
50
+ return self._request_item_response(items, "retrieve")
67
51
 
68
52
  def delete(self, items: list[ContainerReference]) -> list[ContainerReference]:
69
53
  """Delete containers by their identifiers.
@@ -74,27 +58,10 @@ class ContainersAPI(NeatAPI):
74
58
  Returns:
75
59
  List of ContainerReference objects representing the deleted containers.
76
60
  """
77
- if not items:
78
- return []
79
- if len(items) > 100:
80
- raise ValueError("Cannot delete more than 100 containers at once.")
81
-
82
- result = self._http_client.request_with_retries(
83
- ItemsRequest(
84
- endpoint_url=self._config.create_api_url(f"{self.ENDPOINT}/delete"),
85
- method="POST",
86
- body=ItemIDBody(items=items),
87
- )
88
- )
89
- result.raise_for_status()
90
- result = PagedResponse[ContainerReference].model_validate_json(result.success_response.body)
91
- return result.items
61
+ return self._request_id_response(items, "delete")
92
62
 
93
63
  def list(
94
- self,
95
- space: str | None = None,
96
- include_global: bool = False,
97
- limit: int | None = 10,
64
+ self, space: str | None = None, include_global: bool = False, limit: int | None = 10
98
65
  ) -> list[ContainerResponse]:
99
66
  """List containers in CDF Project.
100
67
 
@@ -106,33 +73,5 @@ class ContainersAPI(NeatAPI):
106
73
  Returns:
107
74
  List of ContainerResponse objects.
108
75
  """
109
- if limit is not None and limit < 0:
110
- raise ValueError("Limit must be non-negative.")
111
- elif limit is not None and limit == 0:
112
- return []
113
- parameters: dict[str, PrimitiveType] = {"includeGlobal": include_global}
114
- if space is not None:
115
- parameters["space"] = space
116
- cursor: str | None = None
117
- container_responses: list[ContainerResponse] = []
118
- while True:
119
- if cursor is not None:
120
- parameters["cursor"] = cursor
121
- if limit is None:
122
- parameters["limit"] = self.LIST_REQUEST_LIMIT
123
- else:
124
- parameters["limit"] = min(self.LIST_REQUEST_LIMIT, limit - len(container_responses))
125
- result = self._http_client.request_with_retries(
126
- ParametersRequest(
127
- endpoint_url=self._config.create_api_url(self.ENDPOINT),
128
- method="GET",
129
- parameters=parameters,
130
- )
131
- )
132
- result.raise_for_status()
133
- result = PagedResponse[ContainerResponse].model_validate_json(result.success_response.body)
134
- container_responses.extend(result.items)
135
- cursor = result.next_cursor
136
- if cursor is None or (limit is not None and len(container_responses) >= limit):
137
- break
138
- return container_responses
76
+ filter = ContainerFilter(space=space, include_global=include_global)
77
+ return self._list(limit=limit, params=filter.dump())
@@ -1,15 +1,32 @@
1
1
  from collections.abc import Sequence
2
2
 
3
- from cognite.neat._data_model.models.dms import DataModelBody, DataModelReference, DataModelRequest, DataModelResponse
4
- from cognite.neat._utils.http_client import ItemIDBody, ItemsRequest, ParametersRequest
5
- from cognite.neat._utils.useful_types import PrimitiveType
3
+ from cognite.neat._data_model.models.dms import DataModelReference, DataModelRequest, DataModelResponse
4
+ from cognite.neat._utils.http_client import HTTPClient, SuccessResponse
6
5
 
7
- from .api import NeatAPI
6
+ from .api import Endpoint, NeatAPI
7
+ from .config import NeatClientConfig
8
8
  from .data_classes import PagedResponse
9
+ from .filters import DataModelFilter
9
10
 
10
11
 
11
12
  class DataModelsAPI(NeatAPI):
12
- ENDPOINT = "/models/datamodels"
13
+ def __init__(self, neat_config: NeatClientConfig, http_client: HTTPClient) -> None:
14
+ super().__init__(
15
+ neat_config,
16
+ http_client,
17
+ endpoint_map={
18
+ "apply": Endpoint("POST", "/models/datamodels", item_limit=100),
19
+ "retrieve": Endpoint("POST", "/models/datamodels/byids", item_limit=100),
20
+ "delete": Endpoint("POST", "/models/datamodels/delete", item_limit=100),
21
+ "list": Endpoint("GET", "/models/datamodels", item_limit=1000),
22
+ },
23
+ )
24
+
25
+ def _validate_page_response(self, response: SuccessResponse) -> PagedResponse[DataModelResponse]:
26
+ return PagedResponse[DataModelResponse].model_validate_json(response.body)
27
+
28
+ def _validate_id_response(self, response: SuccessResponse) -> list[DataModelReference]:
29
+ return PagedResponse[DataModelReference].model_validate_json(response.body).items
13
30
 
14
31
  def apply(self, data_models: Sequence[DataModelRequest]) -> list[DataModelResponse]:
15
32
  """Apply (create or update) data models in CDF.
@@ -19,26 +36,9 @@ class DataModelsAPI(NeatAPI):
19
36
  Returns:
20
37
  List of DataModelResponse objects.
21
38
  """
22
- if not data_models:
23
- return []
24
- if len(data_models) > 100:
25
- raise ValueError("Cannot apply more than 100 data models at once.")
26
-
27
- result = self._http_client.request_with_retries(
28
- ItemsRequest(
29
- endpoint_url=self._config.create_api_url(self.ENDPOINT),
30
- method="POST",
31
- body=DataModelBody(items=data_models),
32
- )
33
- )
34
- result.raise_for_status()
35
- result = PagedResponse[DataModelResponse].model_validate_json(result.success_response.body)
36
- return result.items
39
+ return self._request_item_response(data_models, "apply")
37
40
 
38
- def retrieve(
39
- self,
40
- items: list[DataModelReference],
41
- ) -> list[DataModelResponse]:
41
+ def retrieve(self, items: list[DataModelReference]) -> list[DataModelResponse]:
42
42
  """Retrieve data models by their identifiers.
43
43
 
44
44
  Args:
@@ -46,21 +46,7 @@ class DataModelsAPI(NeatAPI):
46
46
  Returns:
47
47
  List of DataModelResponse objects.
48
48
  """
49
- if not items:
50
- return []
51
- if len(items) > 100:
52
- raise ValueError("Cannot retrieve more than 1000 containers at once.")
53
-
54
- result = self._http_client.request_with_retries(
55
- ItemsRequest(
56
- endpoint_url=self._config.create_api_url(f"{self.ENDPOINT}/byids"),
57
- method="POST",
58
- body=ItemIDBody(items=items),
59
- )
60
- )
61
- result.raise_for_status()
62
- result = PagedResponse[DataModelResponse].model_validate_json(result.success_response.body)
63
- return result.items
49
+ return self._request_item_response(items, "retrieve")
64
50
 
65
51
  def delete(self, items: list[DataModelReference]) -> list[DataModelReference]:
66
52
  """Delete data models by their identifiers.
@@ -70,46 +56,32 @@ class DataModelsAPI(NeatAPI):
70
56
  Returns:
71
57
  List of DataModelReference objects representing the deleted data models.
72
58
  """
73
- if not items:
74
- return []
75
- if len(items) > 100:
76
- raise ValueError("Cannot delete more than 100 data models at once.")
77
-
78
- result = self._http_client.request_with_retries(
79
- ItemsRequest(
80
- endpoint_url=self._config.create_api_url(f"{self.ENDPOINT}/delete"),
81
- method="POST",
82
- body=ItemIDBody(items=items),
83
- )
84
- )
85
- result.raise_for_status()
86
- result = PagedResponse[DataModelReference].model_validate_json(result.success_response.body)
87
- return result.items
59
+ return self._request_id_response(items, "delete")
88
60
 
89
61
  def list(
90
62
  self,
91
63
  space: str | None = None,
92
64
  all_versions: bool = False,
65
+ inline_views: bool = False,
93
66
  include_global: bool = False,
94
- limit: int = 10,
67
+ limit: int | None = 10,
95
68
  ) -> list[DataModelResponse]:
96
- """List data models in CDF Project."""
97
- if limit > 1000:
98
- raise ValueError("Pagination is not (yet) supported for listing data models. The maximum limit is 1000.")
99
- parameters: dict[str, PrimitiveType] = {
100
- "allVersions": all_versions,
101
- "includeGlobal": include_global,
102
- "limit": limit,
103
- }
104
- if space is not None:
105
- parameters["space"] = space
106
- result = self._http_client.request_with_retries(
107
- ParametersRequest(
108
- endpoint_url=self._config.create_api_url(self.ENDPOINT),
109
- method="GET",
110
- parameters=parameters,
111
- )
69
+ """List data models in CDF Project.
70
+
71
+ Args:
72
+ space: If specified, only data models in this space are returned.
73
+ all_versions: If True, return all versions. If False, only return the latest version.
74
+ inline_views: If True, include views inline in the response.
75
+ include_global: If True, include global data models.
76
+ limit: Maximum number of data models to return. If None, return all data models.
77
+
78
+ Returns:
79
+ List of DataModelResponse objects.
80
+ """
81
+ filter = DataModelFilter(
82
+ space=space,
83
+ all_versions=all_versions,
84
+ inline_views=inline_views,
85
+ include_global=include_global,
112
86
  )
113
- result.raise_for_status()
114
- result = PagedResponse[DataModelResponse].model_validate_json(result.success_response.body)
115
- return result.items
87
+ return self._list(limit=limit, params=filter.dump())
@@ -0,0 +1,40 @@
1
+ from typing import Any, Literal
2
+
3
+ from pydantic import BaseModel, ConfigDict
4
+ from pydantic.alias_generators import to_camel
5
+
6
+
7
+ class BaseModelRequest(BaseModel):
8
+ """Base class for all object. This includes resources and nested objects."""
9
+
10
+
11
+ class Filter(BaseModel):
12
+ model_config = ConfigDict(alias_generator=to_camel, extra="ignore", populate_by_name=True)
13
+
14
+ def dump(self, camel_case: bool = True) -> dict[str, Any]:
15
+ """Dump the object to a dictionary.
16
+
17
+ Args:
18
+ camel_case (bool): Whether to use camelCase for the keys. Default is True.
19
+
20
+ """
21
+ return self.model_dump(mode="json", by_alias=camel_case, exclude_unset=True)
22
+
23
+
24
+ class DataModelingFilter(Filter):
25
+ space: str | None = None
26
+ include_global: bool | None = None
27
+
28
+
29
+ class ContainerFilter(DataModelingFilter):
30
+ used_for: Literal["node", "edge", "record", "all"] | None = None
31
+
32
+
33
+ class ViewFilter(DataModelingFilter):
34
+ include_inherited_properties: bool | None = None
35
+ all_versions: bool | None = None
36
+
37
+
38
+ class DataModelFilter(DataModelingFilter):
39
+ inline_views: bool | None = None
40
+ all_versions: bool | None = None
@@ -1,18 +1,37 @@
1
1
  from __future__ import annotations
2
2
 
3
- from cognite.neat._data_model.models.dms import DataModelBody, SpaceRequest, SpaceResponse
3
+ from collections.abc import Sequence
4
+
5
+ from cognite.neat._data_model.models.dms import SpaceRequest, SpaceResponse
4
6
  from cognite.neat._data_model.models.dms._references import SpaceReference
5
- from cognite.neat._utils.http_client import ItemIDBody, ItemsRequest, ParametersRequest
6
- from cognite.neat._utils.useful_types import PrimitiveType
7
+ from cognite.neat._utils.http_client import HTTPClient, SuccessResponse
7
8
 
8
- from .api import NeatAPI
9
+ from .api import Endpoint, NeatAPI
10
+ from .config import NeatClientConfig
9
11
  from .data_classes import PagedResponse
12
+ from .filters import DataModelingFilter
10
13
 
11
14
 
12
15
  class SpacesAPI(NeatAPI):
13
- ENDPOINT = "/models/spaces"
16
+ def __init__(self, neat_config: NeatClientConfig, http_client: HTTPClient) -> None:
17
+ super().__init__(
18
+ neat_config,
19
+ http_client,
20
+ endpoint_map={
21
+ "apply": Endpoint("POST", "/models/spaces", item_limit=100),
22
+ "retrieve": Endpoint("POST", "/models/spaces/byids", item_limit=100),
23
+ "delete": Endpoint("POST", "/models/spaces/delete", item_limit=100),
24
+ "list": Endpoint("GET", "/models/spaces", item_limit=1000),
25
+ },
26
+ )
27
+
28
+ def _validate_page_response(self, response: SuccessResponse) -> PagedResponse[SpaceResponse]:
29
+ return PagedResponse[SpaceResponse].model_validate_json(response.body)
30
+
31
+ def _validate_id_response(self, response: SuccessResponse) -> list[SpaceReference]:
32
+ return PagedResponse[SpaceReference].model_validate_json(response.body).items
14
33
 
15
- def apply(self, spaces: list[SpaceRequest]) -> list[SpaceResponse]:
34
+ def apply(self, spaces: Sequence[SpaceRequest]) -> list[SpaceResponse]:
16
35
  """Apply (create or update) spaces in CDF.
17
36
 
18
37
  Args:
@@ -20,20 +39,7 @@ class SpacesAPI(NeatAPI):
20
39
  Returns:
21
40
  List of SpaceResponse objects.
22
41
  """
23
- if not spaces:
24
- return []
25
- if len(spaces) > 100:
26
- raise ValueError("Cannot apply more than 100 spaces at once.")
27
- result = self._http_client.request_with_retries(
28
- ItemsRequest(
29
- endpoint_url=self._config.create_api_url(self.ENDPOINT),
30
- method="POST",
31
- body=DataModelBody(items=spaces),
32
- )
33
- )
34
- result.raise_for_status()
35
- result = PagedResponse[SpaceResponse].model_validate_json(result.success_response.body)
36
- return result.items
42
+ return self._request_item_response(spaces, "apply")
37
43
 
38
44
  def retrieve(self, spaces: list[SpaceReference]) -> list[SpaceResponse]:
39
45
  """Retrieve spaces by their identifiers.
@@ -44,21 +50,7 @@ class SpacesAPI(NeatAPI):
44
50
  Returns:
45
51
  List of SpaceResponse objects.
46
52
  """
47
- if not spaces:
48
- return []
49
- if len(spaces) > 1000:
50
- raise ValueError("Cannot retrieve more than 1000 spaces at once.")
51
-
52
- result = self._http_client.request_with_retries(
53
- ItemsRequest(
54
- endpoint_url=self._config.create_api_url(f"{self.ENDPOINT}/byids"),
55
- method="POST",
56
- body=ItemIDBody(items=spaces),
57
- )
58
- )
59
- result.raise_for_status()
60
- result = PagedResponse[SpaceResponse].model_validate_json(result.success_response.body)
61
- return result.items
53
+ return self._request_item_response(spaces, "retrieve")
62
54
 
63
55
  def delete(self, spaces: list[SpaceReference]) -> list[SpaceReference]:
64
56
  """Delete spaces by their identifiers.
@@ -68,48 +60,21 @@ class SpacesAPI(NeatAPI):
68
60
  Returns:
69
61
  List of SpaceReference objects representing the deleted spaces.
70
62
  """
71
- if not spaces:
72
- return []
73
- if len(spaces) > 100:
74
- raise ValueError("Cannot delete more than 100 spaces at once.")
75
- result = self._http_client.request_with_retries(
76
- ItemsRequest(
77
- endpoint_url=self._config.create_api_url(f"{self.ENDPOINT}/delete"),
78
- method="POST",
79
- body=ItemIDBody(items=spaces),
80
- )
81
- )
82
- result.raise_for_status()
83
- result = PagedResponse[SpaceReference].model_validate_json(result.success_response.body)
84
- return result.items
63
+ return self._request_id_response(spaces, "delete")
85
64
 
86
65
  def list(
87
66
  self,
88
67
  include_global: bool = False,
89
- limit: int = 10,
68
+ limit: int | None = 10,
90
69
  ) -> list[SpaceResponse]:
91
70
  """List spaces in CDF Project.
92
71
 
93
72
  Args:
94
73
  include_global: If True, include global spaces.
95
- limit: Maximum number of spaces to return. Max is 1000.
74
+ limit: Maximum number of spaces to return. If None, return all spaces.
96
75
 
97
76
  Returns:
98
77
  List of SpaceResponse objects.
99
78
  """
100
- if limit > 1000:
101
- raise ValueError("Pagination is not (yet) supported for listing spaces. The maximum limit is 1000.")
102
- parameters: dict[str, PrimitiveType] = {
103
- "includeGlobal": include_global,
104
- "limit": limit,
105
- }
106
- result = self._http_client.request_with_retries(
107
- ParametersRequest(
108
- endpoint_url=self._config.create_api_url(self.ENDPOINT),
109
- method="GET",
110
- parameters=parameters,
111
- )
112
- )
113
- result.raise_for_status()
114
- result = PagedResponse[SpaceResponse].model_validate_json(result.success_response.body)
115
- return result.items
79
+ filter = DataModelingFilter(include_global=include_global)
80
+ return self._list(limit=limit, params=filter.dump())
@@ -1,9 +1,14 @@
1
- from cognite.neat._client.api import NeatAPI
2
- from cognite.neat._client.data_classes import StatisticsResponse
3
- from cognite.neat._utils.http_client import ParametersRequest
1
+ from cognite.neat._utils.http_client import HTTPClient, ParametersRequest
4
2
 
3
+ from .config import NeatClientConfig
4
+ from .data_classes import StatisticsResponse
5
+
6
+
7
+ class StatisticsAPI:
8
+ def __init__(self, neat_config: NeatClientConfig, http_client: HTTPClient) -> None:
9
+ self._config = neat_config
10
+ self._http_client = http_client
5
11
 
6
- class StatisticsAPI(NeatAPI):
7
12
  def project(self) -> StatisticsResponse:
8
13
  """Retrieve project-wide usage data and limits.
9
14
 
@@ -2,46 +2,45 @@ from __future__ import annotations
2
2
 
3
3
  from collections.abc import Sequence
4
4
 
5
- from cognite.neat._data_model.models.dms import DataModelBody, ViewReference, ViewRequest, ViewResponse
6
- from cognite.neat._utils.collection import chunker_sequence
7
- from cognite.neat._utils.http_client import ItemIDBody, ItemsRequest, ParametersRequest
8
- from cognite.neat._utils.useful_types import PrimitiveType
5
+ from cognite.neat._data_model.models.dms import ViewReference, ViewRequest, ViewResponse
6
+ from cognite.neat._utils.http_client import HTTPClient, SuccessResponse
9
7
 
10
- from .api import NeatAPI
8
+ from .api import Endpoint, NeatAPI
9
+ from .config import NeatClientConfig
11
10
  from .data_classes import PagedResponse
11
+ from .filters import ViewFilter
12
12
 
13
13
 
14
14
  class ViewsAPI(NeatAPI):
15
- ENDPOINT = "/models/views"
16
- LIST_REQUEST_LIMIT = 1000
15
+ def __init__(self, neat_config: NeatClientConfig, http_client: HTTPClient) -> None:
16
+ super().__init__(
17
+ neat_config,
18
+ http_client,
19
+ endpoint_map={
20
+ "apply": Endpoint("POST", "/models/views", item_limit=100),
21
+ "retrieve": Endpoint("POST", "/models/views/byids", item_limit=100),
22
+ "delete": Endpoint("POST", "/models/views/delete", item_limit=100),
23
+ "list": Endpoint("GET", "/models/views", item_limit=1000),
24
+ },
25
+ )
26
+
27
+ def _validate_page_response(self, response: SuccessResponse) -> PagedResponse[ViewResponse]:
28
+ return PagedResponse[ViewResponse].model_validate_json(response.body)
29
+
30
+ def _validate_id_response(self, response: SuccessResponse) -> list[ViewReference]:
31
+ return PagedResponse[ViewReference].model_validate_json(response.body).items
17
32
 
18
33
  def apply(self, items: Sequence[ViewRequest]) -> list[ViewResponse]:
19
34
  """Create or update views in CDF Project.
35
+
20
36
  Args:
21
37
  items: List of ViewRequest objects to create or update.
22
38
  Returns:
23
39
  List of ViewResponse objects.
24
40
  """
25
- if not items:
26
- return []
27
- if len(items) > 100:
28
- raise ValueError("Cannot apply more than 100 views at once.")
29
- result = self._http_client.request_with_retries(
30
- ItemsRequest(
31
- endpoint_url=self._config.create_api_url(self.ENDPOINT),
32
- method="POST",
33
- body=DataModelBody(items=items),
34
- )
35
- )
36
- result.raise_for_status()
37
- result = PagedResponse[ViewResponse].model_validate_json(result.success_response.body)
38
- return result.items
41
+ return self._request_item_response(items, "apply")
39
42
 
40
- def retrieve(
41
- self,
42
- items: list[ViewReference],
43
- include_inherited_properties: bool = True,
44
- ) -> list[ViewResponse]:
43
+ def retrieve(self, items: list[ViewReference], include_inherited_properties: bool = True) -> list[ViewResponse]:
45
44
  """Retrieve views by their identifiers.
46
45
 
47
46
  Args:
@@ -51,42 +50,20 @@ class ViewsAPI(NeatAPI):
51
50
  Returns:
52
51
  List of ViewResponse objects.
53
52
  """
54
- results: list[ViewResponse] = []
55
- for chunk in chunker_sequence(items, 100):
56
- batch = self._http_client.request_with_retries(
57
- ItemsRequest(
58
- endpoint_url=self._config.create_api_url(f"{self.ENDPOINT}/byids"),
59
- method="POST",
60
- body=ItemIDBody(items=chunk),
61
- parameters={"includeInheritedProperties": include_inherited_properties},
62
- )
63
- )
64
- batch.raise_for_status()
65
- result = PagedResponse[ViewResponse].model_validate_json(batch.success_response.body)
66
- results.extend(result.items)
67
- return results
53
+ return self._request_item_response(
54
+ items, "retrieve", extra_body={"includeInheritedProperties": include_inherited_properties}
55
+ )
68
56
 
69
57
  def delete(self, items: list[ViewReference]) -> list[ViewReference]:
70
58
  """Delete views by their identifiers.
71
59
 
72
60
  Args:
73
61
  items: List of (space, external_id, version) tuples identifying the views to delete.
62
+
63
+ Returns:
64
+ List of ViewReference objects representing the deleted views.
74
65
  """
75
- if not items:
76
- return []
77
- if len(items) > 100:
78
- raise ValueError("Cannot delete more than 100 views at once.")
79
-
80
- result = self._http_client.request_with_retries(
81
- ItemsRequest(
82
- endpoint_url=self._config.create_api_url(f"{self.ENDPOINT}/delete"),
83
- method="POST",
84
- body=ItemIDBody(items=items),
85
- )
86
- )
87
- result.raise_for_status()
88
- result = PagedResponse[ViewReference].model_validate_json(result.success_response.body)
89
- return result.items
66
+ return self._request_id_response(items, "delete")
90
67
 
91
68
  def list(
92
69
  self,
@@ -108,37 +85,10 @@ class ViewsAPI(NeatAPI):
108
85
  Returns:
109
86
  List of ViewResponse objects.
110
87
  """
111
- if limit is not None and limit < 0:
112
- raise ValueError("Limit must be non-negative.")
113
- elif limit is not None and limit == 0:
114
- return []
115
- parameters: dict[str, PrimitiveType] = {
116
- "allVersions": all_versions,
117
- "includeInheritedProperties": include_inherited_properties,
118
- "includeGlobal": include_global,
119
- }
120
- if space is not None:
121
- parameters["space"] = space
122
- cursor: str | None = None
123
- view_responses: list[ViewResponse] = []
124
- while True:
125
- if cursor is not None:
126
- parameters["cursor"] = cursor
127
- if limit is None:
128
- parameters["limit"] = self.LIST_REQUEST_LIMIT
129
- else:
130
- parameters["limit"] = min(self.LIST_REQUEST_LIMIT, limit - len(view_responses))
131
- result = self._http_client.request_with_retries(
132
- ParametersRequest(
133
- endpoint_url=self._config.create_api_url(self.ENDPOINT),
134
- method="GET",
135
- parameters=parameters,
136
- )
137
- )
138
- result.raise_for_status()
139
- result = PagedResponse[ViewResponse].model_validate_json(result.success_response.body)
140
- view_responses.extend(result.items)
141
- cursor = result.next_cursor
142
- if cursor is None or (limit is not None and len(view_responses) >= limit):
143
- break
144
- return view_responses
88
+ filter = ViewFilter(
89
+ space=space,
90
+ all_versions=all_versions,
91
+ include_inherited_properties=include_inherited_properties,
92
+ include_global=include_global,
93
+ )
94
+ return self._list(limit=limit, params=filter.dump())
@@ -22,6 +22,9 @@ class WriteableResource(Resource, Generic[T_Resource], ABC):
22
22
  raise NotImplementedError()
23
23
 
24
24
 
25
+ T_Response = TypeVar("T_Response", bound=WriteableResource)
26
+
27
+
25
28
  class APIResource(Generic[T_Reference], ABC):
26
29
  """Base class for all API data modeling resources."""
27
30
 
@@ -167,7 +167,9 @@ class HTTPClient:
167
167
  if isinstance(item, BodyRequest):
168
168
  data = item.data()
169
169
  if not global_config.disable_gzip:
170
- data = gzip.compress(data.encode("utf-8"))
170
+ if isinstance(data, str):
171
+ data = data.encode("utf-8")
172
+ data = gzip.compress(data)
171
173
  return self.session.request(
172
174
  method=item.method,
173
175
  url=item.endpoint_url,
@@ -118,14 +118,14 @@ class BodyRequest(ParametersRequest, ABC):
118
118
  """Base class for HTTP request messages with a body"""
119
119
 
120
120
  @abstractmethod
121
- def data(self) -> str:
121
+ def data(self) -> str | bytes:
122
122
  raise NotImplementedError()
123
123
 
124
124
 
125
125
  class SimpleBodyRequest(BodyRequest):
126
- body: str
126
+ body: str | bytes
127
127
 
128
- def data(self) -> str:
128
+ def data(self) -> str | bytes:
129
129
  return self.body
130
130
 
131
131
 
cognite/neat/_version.py CHANGED
@@ -1,2 +1,2 @@
1
- __version__ = "1.0.25"
1
+ __version__ = "1.0.26"
2
2
  __engine__ = "^2.0.4"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cognite-neat
3
- Version: 1.0.25
3
+ Version: 1.0.26
4
4
  Summary: Knowledge graph transformation
5
5
  Author: Nikola Vasiljevic, Anders Albert
6
6
  Author-email: Nikola Vasiljevic <nikola.vasiljevic@cognite.com>, Anders Albert <anders.albert@cognite.com>
@@ -1,19 +1,20 @@
1
1
  cognite/neat/__init__.py,sha256=YU2PPtzByiyLcUeWePkckaL9v-ZxoR6OF6wiq-2XVwM,297
2
2
  cognite/neat/_client/__init__.py,sha256=DIHMeggZ81rVJwO97E5PVP8AwVAhhLpjBQu7Ns3_xdc,178
3
- cognite/neat/_client/api.py,sha256=nbxCdWBXcTVM6MrQeT_VpB6ehfoI544JHPFq-ejQKCY,292
3
+ cognite/neat/_client/api.py,sha256=VOtXyr9-Ouxkl3nWUj6fffr-9VG6wzVRGWSr_QqR5d8,6915
4
4
  cognite/neat/_client/client.py,sha256=h0HELAHiBFxMNInkDu4AzbgfEIXqeM0BqqnMBmXjgi0,903
5
5
  cognite/neat/_client/config.py,sha256=eIIdWaA13yncRP6X7vTYsTpmXmVcmkhZPv5oPnLUEVc,1484
6
- cognite/neat/_client/containers_api.py,sha256=7bVIlL5PwoAG5Bks1ortW_bCG8iTkFqFVyL05pdJ3Pw,5176
6
+ cognite/neat/_client/containers_api.py,sha256=zFHb9SwfwqAxUkrEStLBV8BDSvRWvBBOY8mVuCIOWQU,3116
7
7
  cognite/neat/_client/data_classes.py,sha256=HYPsrAJGVCUmlWTSIxJgAnIHAOzcyDveMM6Z-cuA92M,1404
8
- cognite/neat/_client/data_model_api.py,sha256=ogVHOabQ3HTqWaaoiGClmbtYdP-pl6DPN2zmPdH5LWY,4253
8
+ cognite/neat/_client/data_model_api.py,sha256=fxJ77RbKd8UAsm4jfDH3P16Jp-GwKWPrxx2jyA_xCnk,3518
9
+ cognite/neat/_client/filters.py,sha256=1tPpbp_GFAh7Jq5tdwUvZz6nOoWB36zIxa_ZO-1UER0,1138
9
10
  cognite/neat/_client/init/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
11
  cognite/neat/_client/init/credentials.py,sha256=8PCKk-rc3rxG3l9vNiY3DklIziJgJpBr9nleozS7BZw,2385
11
12
  cognite/neat/_client/init/env_vars.py,sha256=9nOEVABp4rI2TtFHCMItLouQ5MXFL0zzL4PTclT9m-c,5469
12
13
  cognite/neat/_client/init/interactive.py,sha256=YVnsvRblQJPojf24dA050gVlrrqADK8afSQ6hkXn7Cs,4801
13
14
  cognite/neat/_client/init/main.py,sha256=D-9bsor07T84iFnTZLnhIFA1Xey9zsooHaDhXVeNdos,2508
14
- cognite/neat/_client/spaces_api.py,sha256=xHtSMt_2k2YwZ5_8kH2dfa7fWxQQrky7wra4Ar2jwqs,4111
15
- cognite/neat/_client/statistics_api.py,sha256=HcYb2nNC9M_iaI1xyjjLn2Cz1tcyu7BJeaqVps79tg4,773
16
- cognite/neat/_client/views_api.py,sha256=Qzk_wiLtaWszxCQFDBoWCH1yDc4GOEJsVOcL061rcK0,5639
15
+ cognite/neat/_client/spaces_api.py,sha256=yb26ju3sKbVgiduMEbN4LDGan6vsC3z_JaW3p4FUdr0,2929
16
+ cognite/neat/_client/statistics_api.py,sha256=5meeh0v5mxC2SMB7xGdOwMh4KkaX3DtpUjbGEPhNBJo,913
17
+ cognite/neat/_client/views_api.py,sha256=YMaw7IaxU4gmixpd_t1u9JK9BHfNerf5DMNinGPCAa0,3692
17
18
  cognite/neat/_config.py,sha256=ZvCkcaRVAvH4-ClvinoWaLWhRJpRByqdvncGFsf5gLk,9886
18
19
  cognite/neat/_data_model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
20
  cognite/neat/_data_model/_analysis.py,sha256=dN-udKm_5oD3217O4B_QIps2Hx4v50-Pu2fR0bQNQg0,23504
@@ -52,7 +53,7 @@ cognite/neat/_data_model/models/conceptual/_data_model.py,sha256=zvJZi0OqOFMviwY
52
53
  cognite/neat/_data_model/models/conceptual/_properties.py,sha256=CpF37vJYBTLT4DH4ZOu2U-JyWtkb_27V8fw52qiaE_k,4007
53
54
  cognite/neat/_data_model/models/conceptual/_property.py,sha256=blSZQxX52zaILAtjUkldPzPeysz7wnG-UGSNU5tacI8,4138
54
55
  cognite/neat/_data_model/models/dms/__init__.py,sha256=CW5NPMRrMyY4iyZgqYb8eZkRuwbbXUDSVNMWep3zEPI,5326
55
- cognite/neat/_data_model/models/dms/_base.py,sha256=931ODXnhrBrzf6vkjqu2IaFz8r2gGxuapn85yN_jkgg,889
56
+ cognite/neat/_data_model/models/dms/_base.py,sha256=gCjmDca_z0ZB_NHHGOkB9CCLKNxrI_npz-4mWU2yFI0,951
56
57
  cognite/neat/_data_model/models/dms/_constants.py,sha256=TaoE9kmNVEaTl_dDrZQL7YzgP4K13ff0Rc7nr4zbIgg,1384
57
58
  cognite/neat/_data_model/models/dms/_constraints.py,sha256=cyGgDlByXAuSMWJg7Oc25fkp33LsA61M927bCzTWlbo,1458
58
59
  cognite/neat/_data_model/models/dms/_container.py,sha256=wtQbNUwtpymltT1jav8wD4kIfjaIYnvhhz1KS0ffAbo,6044
@@ -124,9 +125,9 @@ cognite/neat/_utils/_reader.py,sha256=9dXrODNNqWU0Gx1zXjRTOiiByFuDZlpQkQEzx3HAxY
124
125
  cognite/neat/_utils/auxiliary.py,sha256=YQMpqCxccex_slmLYrR5icVX9aeLbD793ou7IrbNTFs,1654
125
126
  cognite/neat/_utils/collection.py,sha256=BIwRrFbUXNPvHhEVujLHgVoDJXzdPEMScrbSBhyCibk,446
126
127
  cognite/neat/_utils/http_client/__init__.py,sha256=qaCLLLhi7H3b_cmbknX0S66KILT7JSKX1YSgZjNdd1U,786
127
- cognite/neat/_utils/http_client/_client.py,sha256=TO9C77LcsqX0R3Fu-mP560nnV6rP5oRXki9kxRYtBlg,9658
128
+ cognite/neat/_utils/http_client/_client.py,sha256=4aj7gmOikLr2sLlmqyMaWonJGGy0IuGvCbJ5hIIuSPw,9732
128
129
  cognite/neat/_utils/http_client/_config.py,sha256=C8IF1JoijmVMjA_FEMgAkiD1buEV1cY5Og3t-Ecyfmk,756
129
- cognite/neat/_utils/http_client/_data_classes.py,sha256=McyCQZUcwkbOXdBmc99DdNsO6mQz8dNVsepDvXmaINA,10138
130
+ cognite/neat/_utils/http_client/_data_classes.py,sha256=aPF9B8Tw6_egBulUtSXHoaUhNMCg4iLaOSu5tx1cNQs,10162
130
131
  cognite/neat/_utils/http_client/_tracker.py,sha256=EBBnd-JZ7nc_jYNFJokCHN2UZ9sx0McFLZvlceUYYic,1215
131
132
  cognite/neat/_utils/repo.py,sha256=Ex_lW7qd4fi0YLaOJaLRwSyrN46kfMXbxecVE4mSs2I,598
132
133
  cognite/neat/_utils/text.py,sha256=-ujNaG_hLkdurKsUmZB9ZI_kJkddlCKEf8g-g_XCk10,2010
@@ -322,9 +323,9 @@ cognite/neat/_v0/session/_template.py,sha256=BNcvrW5y7LWzRM1XFxZkfR1Nc7e8UgjBClH
322
323
  cognite/neat/_v0/session/_to.py,sha256=AnsRSDDdfFyYwSgi0Z-904X7WdLtPfLlR0x1xsu_jAo,19447
323
324
  cognite/neat/_v0/session/_wizard.py,sha256=baPJgXAAF3d1bn4nbIzon1gWfJOeS5T43UXRDJEnD3c,1490
324
325
  cognite/neat/_v0/session/exceptions.py,sha256=jv52D-SjxGfgqaHR8vnpzo0SOJETIuwbyffSWAxSDJw,3495
325
- cognite/neat/_version.py,sha256=kEQ71HVQEACqgtyau8hgTcILi0EwbWj3B7RI2KWSEHg,45
326
+ cognite/neat/_version.py,sha256=lZxKXnsRmb2ksFvFfpJkeZtX4W01ZenoMN457_FWsB0,45
326
327
  cognite/neat/legacy.py,sha256=eI2ecxOV8ilGHyLZlN54ve_abtoK34oXognkFv3yvF0,219
327
328
  cognite/neat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
328
- cognite_neat-1.0.25.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
329
- cognite_neat-1.0.25.dist-info/METADATA,sha256=Puzp0pIIcNYZAvb2M1YswRXTzr6pvtqk4cfBT-oeKBg,6689
330
- cognite_neat-1.0.25.dist-info/RECORD,,
329
+ cognite_neat-1.0.26.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
330
+ cognite_neat-1.0.26.dist-info/METADATA,sha256=jISdpgKdsL7q8pj6OfhnOzfV6VI40aR-iYoLlnTjAP0,6689
331
+ cognite_neat-1.0.26.dist-info/RECORD,,