cognite-neat 1.0.25__py3-none-any.whl → 1.0.27__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/neat/_client/api.py +173 -3
- cognite/neat/_client/containers_api.py +29 -90
- cognite/neat/_client/data_model_api.py +46 -74
- cognite/neat/_client/filters.py +40 -0
- cognite/neat/_client/spaces_api.py +32 -67
- cognite/neat/_client/statistics_api.py +9 -4
- cognite/neat/_client/views_api.py +39 -89
- cognite/neat/_config.py +1 -0
- cognite/neat/_data_model/models/dms/_base.py +3 -0
- cognite/neat/_data_model/models/dms/_limits.py +4 -0
- cognite/neat/_session/_cdf.py +36 -0
- cognite/neat/_session/_html/_render.py +8 -3
- cognite/neat/_session/_html/static/__init__.py +3 -0
- cognite/neat/_session/_html/static/statistics.css +163 -0
- cognite/neat/_session/_html/static/statistics.js +108 -0
- cognite/neat/_session/_html/templates/__init__.py +1 -0
- cognite/neat/_session/_html/templates/statistics.html +26 -0
- cognite/neat/_session/_session.py +4 -0
- cognite/neat/_utils/_reader.py +7 -7
- cognite/neat/_utils/http_client/_client.py +3 -1
- cognite/neat/_utils/http_client/_data_classes.py +3 -3
- cognite/neat/_version.py +1 -1
- {cognite_neat-1.0.25.dist-info → cognite_neat-1.0.27.dist-info}/METADATA +1 -1
- {cognite_neat-1.0.25.dist-info → cognite_neat-1.0.27.dist-info}/RECORD +25 -20
- {cognite_neat-1.0.25.dist-info → cognite_neat-1.0.27.dist-info}/WHEEL +0 -0
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
from cognite.neat.
|
|
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
|
|
6
|
-
from cognite.neat._utils.
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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())
|
cognite/neat/_config.py
CHANGED
|
@@ -105,6 +105,7 @@ class AlphaFlagConfig(ConfigModel):
|
|
|
105
105
|
default=False,
|
|
106
106
|
description="If enabled, Neat will run experimental validators that are still in alpha stage.",
|
|
107
107
|
)
|
|
108
|
+
enable_cdf_analysis: bool = Field(default=False, description="If enabled, neat.cdf endpoint will be available.")
|
|
108
109
|
|
|
109
110
|
def __setattr__(self, key: str, value: Any) -> None:
|
|
110
111
|
"""Set attribute value or raise AttributeError."""
|
|
@@ -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
|
|
|
@@ -9,6 +9,7 @@ class SpaceLimit(BaseModel):
|
|
|
9
9
|
"""Limits for spaces."""
|
|
10
10
|
|
|
11
11
|
limit: int = 100
|
|
12
|
+
count: int = 0
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class ListablePropertyLimits(BaseModel):
|
|
@@ -51,6 +52,7 @@ class ContainerLimits(BaseModel):
|
|
|
51
52
|
"""Limits for containers."""
|
|
52
53
|
|
|
53
54
|
limit: int = 1_000
|
|
55
|
+
count: int = 0
|
|
54
56
|
properties: ContainerPropertyLimits = Field(default_factory=ContainerPropertyLimits)
|
|
55
57
|
|
|
56
58
|
|
|
@@ -58,6 +60,7 @@ class ViewLimits(BaseModel):
|
|
|
58
60
|
"""Limits for views."""
|
|
59
61
|
|
|
60
62
|
limit: int = 2_000
|
|
63
|
+
count: int = 0
|
|
61
64
|
versions: int = 100
|
|
62
65
|
properties: int = 300
|
|
63
66
|
implements: int = 10
|
|
@@ -68,6 +71,7 @@ class DataModelLimits(BaseModel):
|
|
|
68
71
|
"""Limits for data models."""
|
|
69
72
|
|
|
70
73
|
limit: int = 500
|
|
74
|
+
count: int = 0
|
|
71
75
|
versions: int = Field(100, description="Limit of versions per data model.")
|
|
72
76
|
views: int = Field(100, description="Limit of views per data model.")
|
|
73
77
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# /Users/nikola/repos/neat/cognite/neat/_session/_cdf.py
|
|
2
|
+
import uuid
|
|
3
|
+
|
|
4
|
+
from cognite.neat._client import NeatClient
|
|
5
|
+
from cognite.neat._config import NeatConfig
|
|
6
|
+
from cognite.neat._store._store import NeatStore
|
|
7
|
+
|
|
8
|
+
from ._html._render import render
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CDF:
|
|
12
|
+
"""Read from a data source into NeatSession graph store."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, store: NeatStore, client: NeatClient, config: NeatConfig) -> None:
|
|
15
|
+
self._store = store
|
|
16
|
+
self._client = client
|
|
17
|
+
self._config = config
|
|
18
|
+
|
|
19
|
+
def _repr_html_(self) -> str:
|
|
20
|
+
"""Generate HTML representation of CDF schema statistics."""
|
|
21
|
+
unique_id = str(uuid.uuid4())[:8]
|
|
22
|
+
|
|
23
|
+
return render(
|
|
24
|
+
"statistics",
|
|
25
|
+
{
|
|
26
|
+
"unique_id": unique_id,
|
|
27
|
+
"spaces_current": self._store.cdf_limits.spaces.count,
|
|
28
|
+
"spaces_limit": self._store.cdf_limits.spaces.limit,
|
|
29
|
+
"containers_current": self._store.cdf_limits.containers.count,
|
|
30
|
+
"containers_limit": self._store.cdf_limits.containers.limit,
|
|
31
|
+
"views_current": self._store.cdf_limits.views.count,
|
|
32
|
+
"views_limit": self._store.cdf_limits.views.limit,
|
|
33
|
+
"data_models_current": self._store.cdf_limits.data_models.count,
|
|
34
|
+
"data_models_limit": self._store.cdf_limits.data_models.limit,
|
|
35
|
+
},
|
|
36
|
+
)
|
|
@@ -4,13 +4,13 @@ from . import static, templates
|
|
|
4
4
|
|
|
5
5
|
ENCODING = "utf-8"
|
|
6
6
|
|
|
7
|
-
Template: TypeAlias = Literal["issues", "deployment"]
|
|
7
|
+
Template: TypeAlias = Literal["issues", "deployment", "statistics"]
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
def render(template_name: Literal["issues", "deployment"], variables: dict[str, Any]) -> str:
|
|
10
|
+
def render(template_name: Literal["issues", "deployment", "statistics"], variables: dict[str, Any]) -> str:
|
|
11
11
|
"""Generate HTML content from a template and variables."""
|
|
12
12
|
|
|
13
|
-
if template_name not in ["issues", "deployment"]:
|
|
13
|
+
if template_name not in ["issues", "deployment", "statistics"]:
|
|
14
14
|
raise ValueError(f"Unknown template name: {template_name}")
|
|
15
15
|
|
|
16
16
|
variables["SHARED_CSS"] = static.shared_style.read_text(encoding=ENCODING)
|
|
@@ -25,6 +25,11 @@ def render(template_name: Literal["issues", "deployment"], variables: dict[str,
|
|
|
25
25
|
variables["SCRIPTS"] = static.deployment_scripts.read_text(encoding=ENCODING)
|
|
26
26
|
variables["SPECIFIC_CSS"] = static.deployment_style.read_text(encoding=ENCODING)
|
|
27
27
|
|
|
28
|
+
elif template_name == "statistics":
|
|
29
|
+
template = templates.statistics.read_text(encoding=ENCODING)
|
|
30
|
+
variables["SCRIPTS"] = static.statistics_scripts.read_text(encoding=ENCODING)
|
|
31
|
+
variables["SPECIFIC_CSS"] = static.statistics_style.read_text(encoding=ENCODING)
|
|
32
|
+
|
|
28
33
|
for key, value in variables.items():
|
|
29
34
|
template = template.replace(f"{{{{{key}}}}}", str(value))
|
|
30
35
|
return template
|
|
@@ -6,3 +6,6 @@ issues_scripts = Path(__file__).parent / "issues.js"
|
|
|
6
6
|
|
|
7
7
|
deployment_style = Path(__file__).parent / "deployment.css"
|
|
8
8
|
deployment_scripts = Path(__file__).parent / "deployment.js"
|
|
9
|
+
|
|
10
|
+
statistics_style = Path(__file__).parent / "statistics.css"
|
|
11
|
+
statistics_scripts = Path(__file__).parent / "statistics.js"
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
.statistics-container {
|
|
2
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
3
|
+
padding: 20px;
|
|
4
|
+
background: var(--bg-secondary);
|
|
5
|
+
border-radius: 8px;
|
|
6
|
+
color: var(--text-primary);
|
|
7
|
+
transition: all 0.3s ease;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.statistics-header {
|
|
11
|
+
margin-bottom: 24px;
|
|
12
|
+
position: relative;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.statistics-title {
|
|
16
|
+
margin: 0 0 8px 0;
|
|
17
|
+
font-size: 24px;
|
|
18
|
+
font-weight: 600;
|
|
19
|
+
color: var(--text-primary);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.statistics-subtitle {
|
|
23
|
+
margin: 0;
|
|
24
|
+
font-size: 14px;
|
|
25
|
+
color: var(--text-secondary);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.theme-toggle {
|
|
29
|
+
position: absolute;
|
|
30
|
+
top: 0;
|
|
31
|
+
right: 0;
|
|
32
|
+
padding: 8px 12px;
|
|
33
|
+
background: var(--bg-primary);
|
|
34
|
+
border: 2px solid var(--border-color);
|
|
35
|
+
border-radius: 6px;
|
|
36
|
+
color: var(--text-primary);
|
|
37
|
+
cursor: pointer;
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
gap: 6px;
|
|
41
|
+
font-size: 13px;
|
|
42
|
+
font-weight: 500;
|
|
43
|
+
transition: all 0.2s;
|
|
44
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.theme-toggle:hover {
|
|
48
|
+
background: var(--hover-bg);
|
|
49
|
+
border-color: var(--text-secondary);
|
|
50
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.theme-toggle:active {
|
|
54
|
+
transform: scale(0.98);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.statistics-grid {
|
|
58
|
+
display: grid;
|
|
59
|
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
60
|
+
gap: 16px;
|
|
61
|
+
margin-bottom: 20px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.stat-card {
|
|
65
|
+
background: var(--bg-primary);
|
|
66
|
+
padding: 16px;
|
|
67
|
+
border-radius: 6px;
|
|
68
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
69
|
+
border: 1px solid var(--border-light);
|
|
70
|
+
transition: all 0.3s ease;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.stat-label {
|
|
74
|
+
margin: 0 0 12px 0;
|
|
75
|
+
font-size: 14px;
|
|
76
|
+
color: var(--text-secondary);
|
|
77
|
+
text-transform: uppercase;
|
|
78
|
+
letter-spacing: 0.5px;
|
|
79
|
+
font-weight: 600;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.stat-value {
|
|
83
|
+
display: flex;
|
|
84
|
+
align-items: baseline;
|
|
85
|
+
margin-bottom: 12px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.stat-current {
|
|
89
|
+
font-size: 28px;
|
|
90
|
+
font-weight: bold;
|
|
91
|
+
color: var(--text-primary);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.stat-limit {
|
|
95
|
+
color: var(--text-muted);
|
|
96
|
+
margin-left: 8px;
|
|
97
|
+
font-size: 14px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.stat-progress-bg {
|
|
101
|
+
background: var(--border-light);
|
|
102
|
+
border-radius: 4px;
|
|
103
|
+
height: 8px;
|
|
104
|
+
overflow: hidden;
|
|
105
|
+
margin-bottom: 8px;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.stat-progress-bar {
|
|
109
|
+
height: 100%;
|
|
110
|
+
transition: width 0.3s ease, background 0.3s ease;
|
|
111
|
+
background: #10b981;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.stat-usage {
|
|
115
|
+
font-size: 12px;
|
|
116
|
+
font-weight: 500;
|
|
117
|
+
color: #10b981;
|
|
118
|
+
transition: color 0.3s ease;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.statistics-additional {
|
|
122
|
+
background: var(--bg-primary);
|
|
123
|
+
padding: 16px;
|
|
124
|
+
border-radius: 6px;
|
|
125
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
126
|
+
border: 1px solid var(--border-light);
|
|
127
|
+
transition: all 0.3s ease;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.additional-title {
|
|
131
|
+
margin: 0 0 12px 0;
|
|
132
|
+
font-size: 14px;
|
|
133
|
+
color: var(--text-secondary);
|
|
134
|
+
text-transform: uppercase;
|
|
135
|
+
letter-spacing: 0.5px;
|
|
136
|
+
font-weight: 600;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.additional-content {
|
|
140
|
+
display: grid;
|
|
141
|
+
grid-template-columns: 1fr 1fr;
|
|
142
|
+
gap: 12px;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.component-item {
|
|
146
|
+
color: var(--text-primary);
|
|
147
|
+
transition: color 0.3s ease;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.component-item strong {
|
|
151
|
+
color: var(--text-primary);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.component-value {
|
|
155
|
+
color: #0066cc;
|
|
156
|
+
font-weight: 600;
|
|
157
|
+
margin-left: 8px;
|
|
158
|
+
transition: color 0.3s ease;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.dark-mode .component-value {
|
|
162
|
+
color: #60a5fa;
|
|
163
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
function initializeStatistics(uniqueId) {
|
|
2
|
+
/**
|
|
3
|
+
* Initialize stat cards with data from attributes and apply colors.
|
|
4
|
+
* Supports light/dark mode with color schema from shared CSS.
|
|
5
|
+
* Uses uniqueId to support multiple instances on the same page.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function getColor(percentage) {
|
|
9
|
+
if (percentage < 50) return '#10b981'; // green
|
|
10
|
+
if (percentage < 80) return '#f59e0b'; // amber
|
|
11
|
+
return '#ef4444'; // red
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function renderCards() {
|
|
15
|
+
const container = document.getElementById('statisticsContainer-' + uniqueId);
|
|
16
|
+
if (!container) return;
|
|
17
|
+
|
|
18
|
+
const cards = container.querySelectorAll('.stat-card');
|
|
19
|
+
|
|
20
|
+
cards.forEach(card => {
|
|
21
|
+
// Skip if already rendered
|
|
22
|
+
if (card.querySelector('.stat-label')) return;
|
|
23
|
+
|
|
24
|
+
const current = parseInt(card.dataset.current);
|
|
25
|
+
const limit = parseInt(card.dataset.limit);
|
|
26
|
+
const label = card.dataset.label;
|
|
27
|
+
const percentage = (current / limit * 100) || 0;
|
|
28
|
+
const color = getColor(percentage);
|
|
29
|
+
|
|
30
|
+
// Build card HTML
|
|
31
|
+
card.innerHTML = `
|
|
32
|
+
<h3 class="stat-label">${label}</h3>
|
|
33
|
+
<div class="stat-value">
|
|
34
|
+
<span class="stat-current">${current}</span>
|
|
35
|
+
<span class="stat-limit">/ ${limit}</span>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="stat-progress-bg">
|
|
38
|
+
<div class="stat-progress-bar" style="background: ${color}; width: ${Math.min(percentage, 100)}%;"></div>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="stat-usage" style="color: ${color};">${percentage.toFixed(1)}% used</div>
|
|
41
|
+
`;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function setupThemeToggle() {
|
|
46
|
+
const container = document.getElementById('statisticsContainer-' + uniqueId);
|
|
47
|
+
if (!container) return;
|
|
48
|
+
|
|
49
|
+
// Check if theme toggle already exists
|
|
50
|
+
if (container.querySelector('.theme-toggle')) return;
|
|
51
|
+
|
|
52
|
+
// Create theme toggle button
|
|
53
|
+
const header = container.querySelector('.statistics-header');
|
|
54
|
+
if (!header) return;
|
|
55
|
+
|
|
56
|
+
const themeToggle = document.createElement('button');
|
|
57
|
+
themeToggle.className = 'theme-toggle';
|
|
58
|
+
themeToggle.id = 'themeToggleStats-' + uniqueId;
|
|
59
|
+
themeToggle.innerHTML = '<span id="themeIcon-' + uniqueId + '">🌙</span><span id="themeText-' + uniqueId + '">Dark</span>';
|
|
60
|
+
|
|
61
|
+
header.appendChild(themeToggle);
|
|
62
|
+
|
|
63
|
+
// Load saved theme preference
|
|
64
|
+
const storageKey = 'neat-statistics-theme-' + uniqueId;
|
|
65
|
+
const savedTheme = localStorage.getItem(storageKey) || 'light';
|
|
66
|
+
|
|
67
|
+
// Toggle theme on button click
|
|
68
|
+
themeToggle.addEventListener('click', () => {
|
|
69
|
+
const isDarkMode = container.classList.contains('dark-mode');
|
|
70
|
+
const newTheme = isDarkMode ? 'light' : 'dark';
|
|
71
|
+
applyTheme(newTheme);
|
|
72
|
+
localStorage.setItem(storageKey, newTheme);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
function applyTheme(theme) {
|
|
76
|
+
const isDark = theme === 'dark';
|
|
77
|
+
const themeIcon = document.querySelector('#themeIcon-' + uniqueId);
|
|
78
|
+
const themeText = document.querySelector('#themeText-' + uniqueId);
|
|
79
|
+
|
|
80
|
+
if (isDark) {
|
|
81
|
+
container.classList.add('dark-mode');
|
|
82
|
+
if (themeIcon) themeIcon.textContent = '☀️';
|
|
83
|
+
if (themeText) themeText.textContent = 'Light';
|
|
84
|
+
} else {
|
|
85
|
+
container.classList.remove('dark-mode');
|
|
86
|
+
if (themeIcon) themeIcon.textContent = '🌙';
|
|
87
|
+
if (themeText) themeText.textContent = 'Dark';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Apply initial theme
|
|
92
|
+
applyTheme(savedTheme);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Render cards and setup theme toggle
|
|
96
|
+
renderCards();
|
|
97
|
+
setupThemeToggle();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Call immediately with uniqueId for Jupyter notebooks
|
|
101
|
+
initializeStatistics(uniqueId);
|
|
102
|
+
|
|
103
|
+
// Also try on DOMContentLoaded for regular HTML pages
|
|
104
|
+
if (document.readyState === 'loading') {
|
|
105
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
106
|
+
initializeStatistics(uniqueId);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
{{SHARED_CSS}}
|
|
3
|
+
{{SPECIFIC_CSS}}
|
|
4
|
+
</style>
|
|
5
|
+
|
|
6
|
+
<div class="statistics-container" id="statisticsContainer-{{unique_id}}">
|
|
7
|
+
<div class="statistics-header">
|
|
8
|
+
<h2 class="statistics-title">CDF DMS Statistics</h2>
|
|
9
|
+
<p class="statistics-subtitle">Project usage and limits overview</p>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div class="statistics-grid">
|
|
13
|
+
<div class="stat-card" data-current="{{spaces_current}}" data-limit="{{spaces_limit}}" data-label="Spaces"></div>
|
|
14
|
+
<div class="stat-card" data-current="{{containers_current}}" data-limit="{{containers_limit}}" data-label="Containers"></div>
|
|
15
|
+
<div class="stat-card" data-current="{{views_current}}" data-limit="{{views_limit}}" data-label="Views"></div>
|
|
16
|
+
<div class="stat-card" data-current="{{data_models_current}}" data-limit="{{data_models_limit}}" data-label="Data Models"></div>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<script>
|
|
22
|
+
(function() {
|
|
23
|
+
const uniqueId = '{{unique_id}}';
|
|
24
|
+
{{SCRIPTS}}
|
|
25
|
+
})();
|
|
26
|
+
</script>
|
|
@@ -10,6 +10,7 @@ from cognite.neat._state_machine import EmptyState, PhysicalState
|
|
|
10
10
|
from cognite.neat._store import NeatStore
|
|
11
11
|
from cognite.neat._utils.http_client import ParametersRequest, SuccessResponse
|
|
12
12
|
|
|
13
|
+
from ._cdf import CDF
|
|
13
14
|
from ._issues import Issues
|
|
14
15
|
from ._physical import PhysicalDataModel
|
|
15
16
|
from ._result import Result
|
|
@@ -51,6 +52,9 @@ class NeatSession:
|
|
|
51
52
|
self.issues = Issues(self._store)
|
|
52
53
|
self.result = Result(self._store)
|
|
53
54
|
|
|
55
|
+
if self._config.alpha.enable_cdf_analysis:
|
|
56
|
+
self.cdf = CDF(self._store, self._client, self._config)
|
|
57
|
+
|
|
54
58
|
collector = Collector()
|
|
55
59
|
if collector.can_collect:
|
|
56
60
|
collector.collect("initSession", {"mode": self._config.modeling.mode})
|
cognite/neat/_utils/_reader.py
CHANGED
|
@@ -6,7 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
from typing import IO, Any, TextIO
|
|
7
7
|
from urllib.parse import urlparse
|
|
8
8
|
|
|
9
|
-
import
|
|
9
|
+
import httpx
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class NeatReader(ABC):
|
|
@@ -124,24 +124,24 @@ class HttpFileReader(NeatReader):
|
|
|
124
124
|
return self.path
|
|
125
125
|
|
|
126
126
|
def read_text(self) -> str:
|
|
127
|
-
response =
|
|
127
|
+
response = httpx.get(self._url)
|
|
128
128
|
response.raise_for_status()
|
|
129
129
|
return response.text
|
|
130
130
|
|
|
131
131
|
def read_bytes(self) -> bytes:
|
|
132
|
-
response =
|
|
132
|
+
response = httpx.get(self._url)
|
|
133
133
|
response.raise_for_status()
|
|
134
134
|
return response.content
|
|
135
135
|
|
|
136
136
|
def size(self) -> int:
|
|
137
|
-
response =
|
|
137
|
+
response = httpx.head(self._url)
|
|
138
138
|
response.raise_for_status()
|
|
139
139
|
return int(response.headers["Content-Length"])
|
|
140
140
|
|
|
141
141
|
def iterate(self, chunk_size: int) -> Iterable[str]:
|
|
142
|
-
with
|
|
142
|
+
with httpx.stream("GET", self._url) as response:
|
|
143
143
|
response.raise_for_status()
|
|
144
|
-
for chunk in response.
|
|
144
|
+
for chunk in response.iter_bytes(chunk_size):
|
|
145
145
|
yield chunk.decode("utf-8")
|
|
146
146
|
|
|
147
147
|
def __enter__(self) -> IO:
|
|
@@ -151,7 +151,7 @@ class HttpFileReader(NeatReader):
|
|
|
151
151
|
return self._url
|
|
152
152
|
|
|
153
153
|
def exists(self) -> bool:
|
|
154
|
-
response =
|
|
154
|
+
response = httpx.head(self._url)
|
|
155
155
|
return 200 <= response.status_code < 400
|
|
156
156
|
|
|
157
157
|
def materialize_path(self) -> Path:
|