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.
@@ -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())
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
+ }
@@ -2,3 +2,4 @@ from pathlib import Path
2
2
 
3
3
  issues = Path(__file__).parent / "issues.html"
4
4
  deployment = Path(__file__).parent / "deployment.html"
5
+ statistics = Path(__file__).parent / "statistics.html"
@@ -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})
@@ -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 requests
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 = requests.get(self._url)
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 = requests.get(self._url)
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 = requests.head(self._url)
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 requests.get(self._url, stream=True) as response:
142
+ with httpx.stream("GET", self._url) as response:
143
143
  response.raise_for_status()
144
- for chunk in response.iter_content(chunk_size):
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 = requests.head(self._url)
154
+ response = httpx.head(self._url)
155
155
  return 200 <= response.status_code < 400
156
156
 
157
157
  def materialize_path(self) -> Path: