Skip to content

Commit 3f3f875

Browse files
committed
add /models/statistics
1 parent 2a276bd commit 3f3f875

File tree

3 files changed

+183
-0
lines changed

3 files changed

+183
-0
lines changed

cognite/client/_api/data_modeling/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from cognite.client._api.data_modeling.graphql import DataModelingGraphQLAPI
88
from cognite.client._api.data_modeling.instances import InstancesAPI
99
from cognite.client._api.data_modeling.spaces import SpacesAPI
10+
from cognite.client._api.data_modeling.statistics import StatisticsAPI
1011
from cognite.client._api.data_modeling.views import ViewsAPI
1112
from cognite.client._api_client import APIClient
1213

@@ -24,3 +25,4 @@ def __init__(self, config: ClientConfig, api_version: str | None, cognite_client
2425
self.views = ViewsAPI(config, api_version, cognite_client)
2526
self.instances = InstancesAPI(config, api_version, cognite_client)
2627
self.graphql = DataModelingGraphQLAPI(config, api_version, cognite_client)
28+
self.statistics = StatisticsAPI(config, api_version, cognite_client)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from __future__ import annotations
2+
3+
import itertools
4+
from collections.abc import Sequence
5+
from typing import TYPE_CHECKING
6+
7+
from cognite.client._api_client import APIClient
8+
from cognite.client.data_classes.data_modeling.ids import _load_space_identifier
9+
from cognite.client.data_classes.data_modeling.statistics import InstanceStatsList, ProjectStatsAndLimits
10+
11+
if TYPE_CHECKING:
12+
from cognite.client._cognite_client import CogniteClient
13+
from cognite.client.config import ClientConfig
14+
15+
16+
class StatisticsAPI(APIClient):
17+
_RESOURCE_PATH = "/models/statistics"
18+
19+
def __init__(self, config: ClientConfig, api_version: str | None, cognite_client: CogniteClient) -> None:
20+
super().__init__(config, api_version, cognite_client)
21+
self._RETRIEVE_LIMIT = 100 # may need to be renamed, but fine for now
22+
23+
def project(self) -> ProjectStatsAndLimits:
24+
"""Usage data and limits for a project's data modelling usage, including data model schemas and graph instances"""
25+
return ProjectStatsAndLimits._load(
26+
self._get(self._RESOURCE_PATH).json(), project=self._cognite_client._config.project
27+
)
28+
29+
def per_space(self, space: str | Sequence[str] | None = None, return_all: bool = False) -> InstanceStatsList:
30+
"""Statistics and limits for all spaces in the project, or for a select subset"""
31+
if return_all:
32+
return InstanceStatsList._load(self._get(self._RESOURCE_PATH + "/spaces").json()["items"])
33+
34+
elif space is not None:
35+
# Statistics and limits for spaces by their space identifiers
36+
ids = _load_space_identifier(space)
37+
return InstanceStatsList._load(
38+
itertools.chain.from_iterable(
39+
self._post(self._RESOURCE_PATH + "/spaces/byids", json={"items": chunk.as_dicts()}).json()["items"]
40+
for chunk in ids.chunked(self._RETRIEVE_LIMIT)
41+
)
42+
)
43+
raise ValueError("Either 'space' or 'return_all' must be specified")
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
from __future__ import annotations
2+
3+
from collections import UserList
4+
from collections.abc import Iterable
5+
from dataclasses import asdict, dataclass
6+
from typing import TYPE_CHECKING, Any
7+
8+
from typing_extensions import Self
9+
10+
from cognite.client.utils._importing import local_import
11+
from cognite.client.utils._pandas_helpers import notebook_display_with_fallback
12+
13+
if TYPE_CHECKING:
14+
import pandas as pd
15+
16+
17+
@dataclass
18+
class InstanceStatsPerSpace:
19+
space: str
20+
nodes: int
21+
edges: int
22+
soft_deleted_nodes: int
23+
soft_deleted_edges: int
24+
25+
@classmethod
26+
def _load(cls, data: dict[str, Any]) -> Self:
27+
return cls(
28+
space=data["space"],
29+
nodes=data["nodes"],
30+
edges=data["edges"],
31+
soft_deleted_nodes=data["softDeletedNodes"],
32+
soft_deleted_edges=data["softDeletedEdges"],
33+
)
34+
35+
def _repr_html_(self) -> str:
36+
return notebook_display_with_fallback(self)
37+
38+
def to_pandas(self) -> pd.DataFrame:
39+
pd = local_import("pandas")
40+
space = (dumped := asdict(self)).pop("space")
41+
return pd.Series(dumped).to_frame(name=space)
42+
43+
44+
class InstanceStatsList(UserList):
45+
def __init__(self, items: list[InstanceStatsPerSpace]):
46+
super().__init__(items)
47+
48+
@classmethod
49+
def _load(cls, data: Iterable[dict[str, Any]]) -> Self:
50+
return cls([InstanceStatsPerSpace._load(item) for item in data])
51+
52+
def _repr_html_(self) -> str:
53+
return notebook_display_with_fallback(self)
54+
55+
def to_pandas(self) -> pd.DataFrame:
56+
pd = local_import("pandas")
57+
df = pd.DataFrame([asdict(item) for item in self]).set_index("space")
58+
order_by_total = (df["nodes"] + df["edges"]).sort_values(ascending=False).index
59+
return df.loc[order_by_total]
60+
61+
62+
@dataclass
63+
class CountLimit:
64+
count: int
65+
limit: int
66+
67+
@classmethod
68+
def _load(cls, data: dict[str, Any]) -> Self:
69+
return cls(count=data["count"], limit=data["limit"])
70+
71+
72+
@dataclass
73+
class InstanceStatsAndLimits:
74+
nodes: int
75+
edges: int
76+
instances: int
77+
instances_limit: int
78+
soft_deleted_nodes: int
79+
soft_deleted_edges: int
80+
soft_deleted_instances: int
81+
soft_deleted_instances_limit: int
82+
83+
@classmethod
84+
def _load(cls, data: dict[str, Any]) -> Self:
85+
return cls(
86+
nodes=data["nodes"],
87+
edges=data["edges"],
88+
instances=data["instances"],
89+
instances_limit=data["instancesLimit"],
90+
soft_deleted_nodes=data["softDeletedNodes"],
91+
soft_deleted_edges=data["softDeletedEdges"],
92+
soft_deleted_instances=data["softDeletedInstances"],
93+
soft_deleted_instances_limit=data["softDeletedInstancesLimit"],
94+
)
95+
96+
def _repr_html_(self) -> str:
97+
return notebook_display_with_fallback(self)
98+
99+
def to_pandas(self) -> pd.DataFrame:
100+
pd = local_import("pandas")
101+
return pd.Series(asdict(self)).to_frame()
102+
103+
104+
@dataclass
105+
class ProjectStatsAndLimits:
106+
project: str
107+
spaces: CountLimit
108+
containers: CountLimit
109+
views: CountLimit
110+
data_models: CountLimit
111+
container_properties: CountLimit
112+
instances: InstanceStatsAndLimits
113+
concurrent_read_limit: int
114+
concurrent_write_limit: int
115+
concurrent_delete_limit: int
116+
117+
@classmethod
118+
def _load(cls, data: dict[str, Any], project: str) -> Self:
119+
return cls(
120+
project=project,
121+
spaces=CountLimit._load(data["spaces"]),
122+
containers=CountLimit._load(data["containers"]),
123+
views=CountLimit._load(data["views"]),
124+
data_models=CountLimit._load(data["dataModels"]),
125+
container_properties=CountLimit._load(data["containerProperties"]),
126+
instances=InstanceStatsAndLimits._load(data["instances"]),
127+
concurrent_read_limit=data["concurrentReadLimit"],
128+
concurrent_write_limit=data["concurrentWriteLimit"],
129+
concurrent_delete_limit=data["concurrentDeleteLimit"],
130+
)
131+
132+
def _repr_html_(self) -> str:
133+
return notebook_display_with_fallback(self)
134+
135+
def to_pandas(self) -> pd.DataFrame:
136+
pd = local_import("pandas")
137+
project = (dumped := asdict(self)).pop("project")
138+
return pd.Series(dumped).to_frame(name=project)

0 commit comments

Comments
 (0)