cognite-neat 1.0.6__py3-none-any.whl → 1.0.7__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.
@@ -607,7 +607,7 @@ class DeploymentResult(BaseDeployObject):
607
607
 
608
608
  @property
609
609
  def is_dry_run(self) -> bool:
610
- return self.status == "pending"
610
+ return self.responses is None
611
611
 
612
612
  @property
613
613
  def is_success(self) -> bool:
@@ -132,6 +132,16 @@
132
132
  color: #9ca3af;
133
133
  }
134
134
 
135
+ .change-failed {
136
+ background: #fee2e2;
137
+ color: #991b1b;
138
+ }
139
+
140
+ .dark-mode .change-failed {
141
+ background: #7f1d1d;
142
+ color: #fca5a5;
143
+ }
144
+
135
145
  .severity-badge {
136
146
  padding: 4px 10px;
137
147
  border-radius: 4px;
@@ -286,6 +296,16 @@
286
296
  color: white;
287
297
  }
288
298
 
299
+ .status-recovered {
300
+ background: #10b981;
301
+ color: white;
302
+ }
303
+
304
+ .status-recovery_failed {
305
+ background: #dc2626;
306
+ color: white;
307
+ }
308
+
289
309
  .dark-mode .status-success {
290
310
  background: #059669;
291
311
  }
@@ -300,4 +320,157 @@
300
320
 
301
321
  .dark-mode .status-pending {
302
322
  background: #2563eb;
323
+ }
324
+
325
+ .dark-mode .status-recovered {
326
+ background: #059669;
327
+ }
328
+
329
+ .dark-mode .status-recovery_failed {
330
+ background: #b91c1c;
331
+ }
332
+
333
+
334
+ /* Error message handling */
335
+
336
+ .error-message-box {
337
+ background: #fee2e2;
338
+ border: 2px solid #dc2626;
339
+ border-left: 6px solid #dc2626;
340
+ border-radius: 8px;
341
+ padding: 16px;
342
+ margin-top: 12px;
343
+ animation: slideIn 0.3s ease-out;
344
+ }
345
+
346
+ .dark-mode .error-message-box {
347
+ background: #7f1d1d;
348
+ border-color: #ef4444;
349
+ }
350
+
351
+ .error-message-header {
352
+ display: flex;
353
+ align-items: center;
354
+ gap: 10px;
355
+ margin-bottom: 8px;
356
+ }
357
+
358
+ .error-icon {
359
+ font-size: 24px;
360
+ }
361
+
362
+ .error-title {
363
+ font-weight: 700;
364
+ font-size: 14px;
365
+ color: #991b1b;
366
+ }
367
+
368
+ .dark-mode .error-title {
369
+ color: #fca5a5;
370
+ }
371
+
372
+ .error-message-content {
373
+ color: #7f1d1d;
374
+ font-size: 13px;
375
+ line-height: 1.6;
376
+ padding-left: 34px;
377
+ font-family: 'Monaco', 'Courier New', monospace;
378
+ }
379
+
380
+ .dark-mode .error-message-content {
381
+ color: #fca5a5;
382
+ }
383
+
384
+ .change-item.failed-change {
385
+ border-left: 4px solid #dc2626;
386
+ background: rgba(254, 226, 226, 0.1);
387
+ }
388
+
389
+ .dark-mode .change-item.failed-change {
390
+ border-left-color: #ef4444;
391
+ background: rgba(127, 29, 29, 0.2);
392
+ }
393
+
394
+ .info-message-box {
395
+ border-radius: 6px;
396
+ padding: 12px 16px;
397
+ margin-top: 12px;
398
+ font-size: 13px;
399
+ line-height: 1.6;
400
+ }
401
+
402
+ .info-message-content {
403
+ color: inherit;
404
+ }
405
+
406
+ .info-message-create {
407
+ background: #d1fae5;
408
+ border-left: 4px solid #065f46;
409
+ color: #065f46;
410
+ }
411
+
412
+ .info-message-update {
413
+ background: #dbeafe;
414
+ border-left: 4px solid #1e40af;
415
+ color: #1e40af;
416
+ }
417
+
418
+ .info-message-delete {
419
+ background: #fee2e2;
420
+ border-left: 4px solid #991b1b;
421
+ color: #991b1b;
422
+ }
423
+
424
+ .info-message-skip {
425
+ background: #fef3c7;
426
+ border-left: 4px solid #92400e;
427
+ color: #92400e;
428
+ }
429
+
430
+ .info-message-unchanged {
431
+ background: #f3f4f6;
432
+ border-left: 4px solid #4b5563;
433
+ color: #4b5563;
434
+ }
435
+
436
+ .dark-mode .info-message-create {
437
+ background: #064e3b;
438
+ border-left-color: #6ee7b7;
439
+ color: #6ee7b7;
440
+ }
441
+
442
+ .dark-mode .info-message-update {
443
+ background: #1e3a8a;
444
+ border-left-color: #93c5fd;
445
+ color: #93c5fd;
446
+ }
447
+
448
+ .dark-mode .info-message-delete {
449
+ background: #7f1d1d;
450
+ border-left-color: #fca5a5;
451
+ color: #fca5a5;
452
+ }
453
+
454
+ .dark-mode .info-message-skip {
455
+ background: #78350f;
456
+ border-left-color: #fde68a;
457
+ color: #fde68a;
458
+ }
459
+
460
+ .dark-mode .info-message-unchanged {
461
+ background: #374151;
462
+ border-left-color: #9ca3af;
463
+ color: #9ca3af;
464
+ }
465
+
466
+
467
+ @keyframes slideIn {
468
+ from {
469
+ opacity: 0;
470
+ transform: translateY(-10px);
471
+ }
472
+ to {
473
+ opacity: 1;
474
+ transform: translateY(0);
475
+ }
303
476
  }
@@ -17,7 +17,9 @@ const STATUS_CONFIG = {
17
17
  success: { icon: '✅', text: 'Success' },
18
18
  failure: { icon: '❌', text: 'Failure' },
19
19
  partial: { icon: '⚠️', text: 'Partial Success' },
20
- pending: { icon: '⏳', text: 'Pending (Dry Run)' }
20
+ pending: { icon: '⏳', text: 'Pending (Dry Run)' },
21
+ recovered: { icon: '🔄', text: 'Recovered' },
22
+ recovery_failed: { icon: '💔', text: 'Recovery Failed' }
21
23
  };
22
24
 
23
25
  // Initialize status badge
@@ -26,7 +28,7 @@ function initializeStatus() {
26
28
  const statusIcon = document.getElementById('statusIcon-' + uniqueId);
27
29
  const statusText = document.getElementById('statusText-' + uniqueId);
28
30
 
29
- const statusConfig = STATUS_CONFIG[stats.status] || STATUS_CONFIG.pending;
31
+ const statusConfig = STATUS_CONFIG[deploymentStatus] || STATUS_CONFIG.pending;
30
32
  statusIcon.textContent = statusConfig.icon;
31
33
  statusText.textContent = statusConfig.text;
32
34
  }
@@ -43,6 +45,31 @@ function updateTheme() {
43
45
  }
44
46
  }
45
47
 
48
+ function renderChangeMessage(change) {
49
+ if (!change.message) return '';
50
+
51
+ // Using textContent to set the message prevents XSS vulnerabilities
52
+ // by automatically escaping HTML characters.
53
+ const messageHolder = document.createElement('div');
54
+ messageHolder.textContent = change.message;
55
+ const escapedMessage = messageHolder.innerHTML;
56
+
57
+ if (change.change_type === 'failed') {
58
+ return `
59
+ <div class="error-message-box">
60
+ <div class="error-message-header">
61
+ <span class="error-icon">❌</span>
62
+ <span class="error-title">Deployment Failed</span>
63
+ </div>
64
+ <div class="error-message-content">${escapedMessage}</div>
65
+ </div>`;
66
+ }
67
+ return `
68
+ <div class="info-message-box info-message-${change.change_type}">
69
+ <div class="info-message-content">${escapedMessage}</div>
70
+ </div>`;
71
+ }
72
+
46
73
  updateTheme();
47
74
  initializeStatus();
48
75
 
@@ -76,13 +103,14 @@ function renderChanges() {
76
103
  }
77
104
 
78
105
  listContainer.innerHTML = filtered.map(change => `
79
- <div class="change-item">
106
+ <div class="change-item ${change.change_type === 'failed' ? 'failed-change' : ''}">
80
107
  <div class="change-header">
81
108
  <span class="endpoint-badge endpoint-${change.endpoint}">${change.endpoint}</span>
82
109
  <span class="change-type-badge change-${change.change_type}">${change.change_type}</span>
83
110
  <span class="severity-badge severity-${change.severity}">${change.severity}</span>
84
111
  </div>
85
112
  <div class="resource-id">${change.resource_id}</div>
113
+
86
114
  ${change.changes.length > 0 ? `
87
115
  <div class="field-changes">
88
116
  ${change.changes.map(fc => `
@@ -93,6 +121,9 @@ function renderChanges() {
93
121
  `).join('')}
94
122
  </div>
95
123
  ` : ''}
124
+
125
+ ${renderChangeMessage(change)}
126
+
96
127
  </div>
97
128
  `).join('');
98
129
  }
@@ -130,8 +161,8 @@ document.getElementById('searchInput-' + uniqueId).addEventListener('input', fun
130
161
  window['exportDeployment_' + uniqueId] = function() {
131
162
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
132
163
  const report = {
133
- status: stats.status,
134
- is_dry_run: stats.is_dry_run,
164
+ status: deploymentStatus,
165
+ is_dry_run: is_dry_run === 'True',
135
166
  timestamp: timestamp,
136
167
  statistics: stats,
137
168
  changes: changes,
@@ -20,6 +20,9 @@
20
20
  <div class="stat-item active" data-filter="all">
21
21
  <span class="stat-number">{{total_changes}}</span> Total Changes
22
22
  </div>
23
+ <div class="stat-item" data-filter="failed">
24
+ <span class="stat-number">{{failed}}</span> Failed
25
+ </div>
23
26
  <div class="stat-item" data-filter="create">
24
27
  <span class="stat-number">{{created}}</span> Created
25
28
  </div>
@@ -29,12 +32,12 @@
29
32
  <div class="stat-item" data-filter="delete">
30
33
  <span class="stat-number">{{deleted}}</span> Deleted
31
34
  </div>
32
- <div class="stat-item" data-filter="skip">
33
- <span class="stat-number">{{skipped}}</span> Skipped
34
- </div>
35
35
  <div class="stat-item" data-filter="unchanged">
36
36
  <span class="stat-number">{{unchanged}}</span> Unchanged
37
37
  </div>
38
+ <div class="stat-item" data-filter="skip">
39
+ <span class="stat-number">{{skipped}}</span> Skipped
40
+ </div>
38
41
  <button class="export-btn" onclick="exportDeployment_{{unique_id}}()">Export Report</button>
39
42
  </div>
40
43
  </div>
@@ -70,6 +73,8 @@
70
73
  const changes = {{CHANGES_JSON}};
71
74
  const stats = {{STATS_JSON}};
72
75
  const uniqueId = '{{unique_id}}';
76
+ const is_dry_run = '{{is_dry_run}}';
77
+ const deploymentStatus = '{{status}}';
73
78
  {{SCRIPTS}}
74
79
  })();
75
80
  </script>
@@ -0,0 +1,3 @@
1
+ from ._result import Result
2
+
3
+ __all__ = ["Result"]
File without changes
@@ -0,0 +1,195 @@
1
+ import itertools
2
+ from typing import Any
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+ from cognite.neat._data_model.deployer.data_classes import (
7
+ AddedField,
8
+ AppliedChanges,
9
+ ChangedField,
10
+ ChangeResult,
11
+ DeploymentResult,
12
+ FieldChange,
13
+ FieldChanges,
14
+ RemovedField,
15
+ ResourceChange,
16
+ )
17
+
18
+
19
+ class SerializedFieldChange(BaseModel):
20
+ """Serialized field change for JSON output."""
21
+
22
+ field_path: str
23
+ severity: str
24
+ description: str
25
+
26
+ @classmethod
27
+ def from_field_change(cls, field_change: FieldChange) -> list["SerializedFieldChange"]:
28
+ """Serialize a field change, handling nested FieldChanges recursively.
29
+
30
+ Args:
31
+ field_change: The field change to serialize.
32
+
33
+ Returns:
34
+ List of serialized field changes (may be multiple if nested).
35
+ """
36
+ serialized_changes: list[SerializedFieldChange] = []
37
+
38
+ if isinstance(field_change, FieldChanges):
39
+ # Recursively handle nested changes
40
+ for change in field_change.changes:
41
+ serialized_changes.extend(cls.from_field_change(change))
42
+ else:
43
+ # Base case: single field change
44
+ serialized_changes.append(cls._from_single_field_change(field_change))
45
+
46
+ return serialized_changes
47
+
48
+ @classmethod
49
+ def _from_single_field_change(cls, field_change: FieldChange) -> "SerializedFieldChange":
50
+ """Serialize a single non-nested field change.
51
+
52
+ Args:
53
+ field_change: The single field change to serialize.
54
+
55
+ Returns:
56
+ Serialized field change.
57
+ """
58
+ return cls(
59
+ field_path=field_change.field_path,
60
+ severity=field_change.severity.name,
61
+ description=field_change.description
62
+ if isinstance(field_change, AddedField | RemovedField | ChangedField)
63
+ else "Field changed",
64
+ )
65
+
66
+
67
+ class SerializedResourceChange(BaseModel):
68
+ """Serialized resource change for JSON output."""
69
+
70
+ id: int
71
+ endpoint: str
72
+ change_type: str
73
+ severity: str
74
+ resource_id: str
75
+ message: str | None = None
76
+ changes: list[SerializedFieldChange] = Field(default_factory=list)
77
+
78
+ @classmethod
79
+ def from_resource_change(
80
+ cls, resource: ResourceChange, endpoint: str, change_id: int
81
+ ) -> "SerializedResourceChange":
82
+ """Serialize a single resource change.
83
+
84
+ Args:
85
+ resource: The resource change to serialize.
86
+ endpoint: The endpoint type.
87
+ change_id: Unique ID for this change.
88
+
89
+ Returns:
90
+ Serialized resource change.
91
+ """
92
+ changes: list[SerializedFieldChange] = []
93
+
94
+ for change in resource.changes:
95
+ changes.extend(SerializedFieldChange.from_field_change(change))
96
+
97
+ return cls(
98
+ id=change_id,
99
+ endpoint=endpoint,
100
+ change_type=resource.change_type,
101
+ severity=resource.severity.name,
102
+ resource_id=str(resource.resource_id),
103
+ changes=changes,
104
+ )
105
+
106
+ @classmethod
107
+ def from_change_result(cls, change_id: int, response: ChangeResult) -> "SerializedResourceChange":
108
+ """Serialize from a change result (actual deployment).
109
+
110
+ Args:
111
+ change_id: Unique ID for this change.
112
+ response: The change result from deployment.
113
+
114
+ Returns:
115
+ Serialized resource change with deployment status.
116
+ """
117
+ serialized_resource_change = cls.from_resource_change(
118
+ resource=response.change,
119
+ endpoint=response.endpoint,
120
+ change_id=change_id,
121
+ )
122
+
123
+ serialized_resource_change.message = response.message
124
+ if not response.is_success:
125
+ serialized_resource_change.change_type = "failed"
126
+
127
+ return serialized_resource_change
128
+
129
+
130
+ class SerializedChanges(BaseModel):
131
+ """Container for all serialized changes."""
132
+
133
+ changes: list[SerializedResourceChange] = Field(default_factory=list)
134
+
135
+ @classmethod
136
+ def from_deployment_result(cls, result: DeploymentResult) -> "SerializedChanges":
137
+ """Create SerializedChanges from a DeploymentResult.
138
+
139
+ Args:
140
+ result: The deployment result to serialize changes from.
141
+
142
+ Returns:
143
+ SerializedChanges instance with all changes.
144
+ """
145
+ serialized = cls()
146
+
147
+ if not result.responses:
148
+ serialized._add_from_dry_run(result)
149
+ else:
150
+ serialized._add_from_applied_changes(result.responses)
151
+
152
+ return serialized
153
+
154
+ def _add_from_dry_run(self, result: DeploymentResult) -> None:
155
+ """Add changes from dry run deployment.
156
+
157
+ Args:
158
+ result: The deployment result in dry run mode.
159
+ """
160
+ # Iterate over each endpoint plan
161
+ for endpoint_plan in result.plan:
162
+ # Then per resource in the endpoint
163
+ for resource in endpoint_plan.resources:
164
+ # Then serialize individual resource change
165
+ serialized_resource_change = SerializedResourceChange.from_resource_change(
166
+ resource=resource,
167
+ endpoint=endpoint_plan.endpoint,
168
+ change_id=len(self.changes),
169
+ )
170
+ self.changes.append(serialized_resource_change)
171
+
172
+ def _add_from_applied_changes(self, applied_changes: AppliedChanges) -> None:
173
+ """Add changes from actual deployment.
174
+ Args:
175
+ result: The deployment result from actual deployment.
176
+ """
177
+ for response in itertools.chain(
178
+ applied_changes.created,
179
+ applied_changes.merged_updated,
180
+ applied_changes.deletions,
181
+ applied_changes.unchanged,
182
+ applied_changes.skipped,
183
+ ):
184
+ self.changes.append(SerializedResourceChange.from_change_result(len(self.changes), response))
185
+
186
+ def model_dump_json_flat(self, **kwargs: Any) -> str:
187
+ """Dump changes as JSON array without the wrapper key.
188
+ Returns:
189
+ JSON string of the changes array.
190
+ """
191
+ if not self.changes:
192
+ return "[]"
193
+
194
+ iterator = (change.model_dump_json(**kwargs) for change in self.changes)
195
+ return f"[{','.join(iterator)}]"