cognite-neat 0.127.19__py3-none-any.whl → 0.127.21__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.
@@ -5,6 +5,8 @@ from typing import Any, Literal
5
5
 
6
6
  from cognite.neat._data_model._constants import DEFAULT_MAX_LIST_SIZE, DEFAULT_MAX_LIST_SIZE_DIRECT_RELATIONS
7
7
  from cognite.neat._data_model.importers._table_importer.data_classes import (
8
+ CREATOR_KEY,
9
+ CREATOR_MARKER,
8
10
  DMSContainer,
9
11
  DMSEnum,
10
12
  DMSNode,
@@ -78,14 +80,34 @@ class DMSTableWriter:
78
80
  )
79
81
 
80
82
  ### Metadata Sheet ###
81
- @staticmethod
82
- def write_metadata(data_model: DataModelRequest) -> list[MetadataValue]:
83
- return [
83
+ @classmethod
84
+ def write_metadata(cls, data_model: DataModelRequest) -> list[MetadataValue]:
85
+ metadata = [
84
86
  MetadataValue(key=key, value=value)
85
87
  for key, value in data_model.model_dump(
86
- mode="json", by_alias=True, exclude_none=True, exclude={"views"}
88
+ mode="json", by_alias=True, exclude_none=True, exclude={"views", "description"}
87
89
  ).items()
88
90
  ]
91
+ if data_model.description:
92
+ description, creator = cls._serialize_description(data_model.description)
93
+ if description:
94
+ metadata.append(MetadataValue(key="description", value=description))
95
+ if creator:
96
+ metadata.append(MetadataValue(key=CREATOR_KEY, value=creator))
97
+ return metadata
98
+
99
+ @staticmethod
100
+ def _serialize_description(description: str | None) -> tuple[str | None, str | None]:
101
+ """DataModelRequest does not have a 'creator' field, this is a special addition that the Neat tables
102
+ format supports (and recommends using). If the data model was created using Neat, the suffix of the
103
+ description will be Creator: <creator>. This function extracts that information."""
104
+ if description is None:
105
+ return None, None
106
+ if CREATOR_MARKER not in description:
107
+ return description, None
108
+
109
+ description, creator = description.rsplit(CREATOR_MARKER, 1)
110
+ return description.rstrip(), creator.strip()
89
111
 
90
112
  ### Container Properties Sheet ###
91
113
 
@@ -24,6 +24,10 @@ from cognite.neat.v0.core._data_model.models.entities import (
24
24
  RawFilter,
25
25
  )
26
26
 
27
+ # This marker is used to identify creator in the description field.
28
+ CREATOR_MARKER = "Creator: "
29
+ CREATOR_KEY = "creator"
30
+
27
31
 
28
32
  def parse_entity_str(v: str) -> ParsedEntity:
29
33
  if isinstance(v, ParsedEntity):
@@ -9,6 +9,7 @@ from cognite.neat._data_model.models.dms import (
9
9
  Constraint,
10
10
  ConstraintAdapter,
11
11
  ContainerPropertyDefinition,
12
+ ContainerReference,
12
13
  ContainerRequest,
13
14
  DataModelRequest,
14
15
  Index,
@@ -17,17 +18,19 @@ from cognite.neat._data_model.models.dms import (
17
18
  RequestSchema,
18
19
  SpaceRequest,
19
20
  UniquenessConstraintDefinition,
21
+ ViewReference,
20
22
  ViewRequest,
21
23
  ViewRequestProperty,
22
24
  ViewRequestPropertyAdapter,
23
25
  )
26
+ from cognite.neat._data_model.models.dms._constants import DATA_MODEL_DESCRIPTION_MAX_LENGTH
24
27
  from cognite.neat._data_model.models.entities import ParsedEntity, parse_entity
25
28
  from cognite.neat._exceptions import DataModelImportException
26
29
  from cognite.neat._issues import ModelSyntaxError
27
30
  from cognite.neat._utils.text import humanize_collection
28
31
  from cognite.neat._utils.validation import ValidationContext, humanize_validation_error
29
32
 
30
- from .data_classes import DMSContainer, DMSEnum, DMSNode, DMSProperty, DMSView, TableDMS
33
+ from .data_classes import CREATOR_KEY, CREATOR_MARKER, DMSContainer, DMSEnum, DMSNode, DMSProperty, DMSView, TableDMS
31
34
  from .source import TableSource
32
35
 
33
36
  T_BaseModel = TypeVar("T_BaseModel", bound=BaseModel)
@@ -169,7 +172,9 @@ class DMSTableReader:
169
172
  space_request = self.read_space(self.default_space)
170
173
  node_types = self.read_nodes(tables.nodes)
171
174
  enum_collections = self.read_enum_collections(tables.enum)
172
- read = self.read_properties(tables.properties, enum_collections)
175
+ container_ref_by_entity = self.read_entity_by_container_ref(tables.containers)
176
+ view_ref_by_entity = self.read_entity_by_view_ref(tables.views)
177
+ read = self.read_properties(tables.properties, enum_collections, container_ref_by_entity, view_ref_by_entity)
173
178
  processed = self.process_properties(read)
174
179
  containers = self.read_containers(tables.containers, processed)
175
180
  views, valid_view_entities = self.read_views(tables.views, processed.view)
@@ -207,18 +212,50 @@ class DMSTableReader:
207
212
  }
208
213
  return enum_collections
209
214
 
215
+ def read_entity_by_container_ref(self, containers: list[DMSContainer]) -> dict[ContainerReference, ParsedEntity]:
216
+ entity_by_container_ref: dict[ContainerReference, ParsedEntity] = {}
217
+ for container in containers:
218
+ data = self._create_container_ref(container.container)
219
+ try:
220
+ container_ref = ContainerReference.model_validate(data)
221
+ except ValidationError:
222
+ # Error will be reported when reading the containers
223
+ continue
224
+ entity_by_container_ref[container_ref] = container.container
225
+ return entity_by_container_ref
226
+
227
+ def read_entity_by_view_ref(self, views: list[DMSView]) -> dict[ViewReference, ParsedEntity]:
228
+ entity_by_view_ref: dict[ViewReference, ParsedEntity] = {}
229
+ for view in views:
230
+ data = self._create_view_ref(view.view)
231
+ try:
232
+ view_ref = ViewReference.model_validate(data)
233
+ except ValidationError:
234
+ # Error will be reported when reading the views
235
+ continue
236
+ entity_by_view_ref[view_ref] = view.view
237
+ return entity_by_view_ref
238
+
210
239
  def read_properties(
211
- self, properties: list[DMSProperty], enum_collections: dict[str, dict[str, Any]]
240
+ self,
241
+ properties: list[DMSProperty],
242
+ enum_collections: dict[str, dict[str, Any]],
243
+ container_ref_by_entity: dict[ContainerReference, ParsedEntity],
244
+ view_ref_by_entity: dict[ViewReference, ParsedEntity],
212
245
  ) -> ReadProperties:
213
246
  read = ReadProperties()
247
+ view_entities = set(view_ref_by_entity.values())
248
+ container_entities = set(container_ref_by_entity.values())
214
249
  for row_no, prop in enumerate(properties):
215
- self._process_view_property(prop, read, row_no)
250
+ self._process_view_property(prop, read, row_no, view_ref_by_entity, view_entities)
216
251
  if prop.container is None or prop.container_property is None:
217
252
  # This is when the property is an edge or reverse direct relation property.
218
253
  continue
219
- self._process_container_property(prop, read, enum_collections, row_no)
220
- self._process_index(prop, read, row_no)
221
- self._process_constraint(prop, read, row_no)
254
+ self._process_container_property(
255
+ prop, read, enum_collections, row_no, container_ref_by_entity, container_entities
256
+ )
257
+ self._process_index(prop, read, row_no, container_ref_by_entity, container_entities)
258
+ self._process_constraint(prop, read, row_no, container_ref_by_entity, container_entities)
222
259
  return read
223
260
 
224
261
  def process_properties(self, read: ReadProperties) -> ProcessedProperties:
@@ -370,31 +407,91 @@ class DMSTableReader:
370
407
  constraints[container_entity][constraint_id] = constraint
371
408
  return constraints
372
409
 
373
- def _process_view_property(self, prop: DMSProperty, read: ReadProperties, row_no: int) -> None:
410
+ def _process_view_property(
411
+ self,
412
+ prop: DMSProperty,
413
+ read: ReadProperties,
414
+ row_no: int,
415
+ view_ref_by_entity: dict[ViewReference, ParsedEntity],
416
+ view_entities: set[ParsedEntity],
417
+ ) -> None:
374
418
  loc = (self.Sheets.properties, row_no)
375
419
  data = self.read_view_property(prop, loc)
376
420
  view_prop = self._validate_adapter(ViewRequestPropertyAdapter, data, loc)
377
- if view_prop is not None:
378
- read.view[(prop.view, prop.view_property)].append(
379
- # MyPy has a very strange complaint here. It complains that given type is not expected type,
380
- # even though they are exactly the same.
381
- ReadViewProperty(prop.container_property, row_no, view_prop) # type: ignore[arg-type]
421
+ if view_prop is None:
422
+ return None
423
+ if prop.view in view_entities:
424
+ read.view[(prop.view, prop.view_property)].append(ReadViewProperty(prop.view_property, row_no, view_prop))
425
+ return None
426
+ # The view entity was not found in the views table. This could either be because the view is missing,
427
+ # or because either the view entity in the Properties table and the View table are specified with/without
428
+ # default space/version inconsistently.
429
+ try:
430
+ view_ref = ViewReference.model_validate(self._create_view_ref(prop.view))
431
+ except ValidationError:
432
+ # Error will be reported when reading the views
433
+ return None
434
+ if view_ref in view_ref_by_entity:
435
+ view_ref_entity = view_ref_by_entity[view_ref]
436
+ read.view[(view_ref_entity, prop.view_property)].append(
437
+ ReadViewProperty(prop.view_property, row_no, view_prop)
438
+ )
439
+ else:
440
+ self.errors.append(
441
+ ModelSyntaxError(
442
+ message=(
443
+ f"In {self.source.location(loc)} the View '{prop.view!s}' "
444
+ f"was not found in the {self.Sheets.views!r} table."
445
+ )
446
+ )
382
447
  )
383
448
  return None
384
449
 
385
450
  def _process_container_property(
386
- self, prop: DMSProperty, read: ReadProperties, enum_collections: dict[str, dict[str, Any]], row_no: int
451
+ self,
452
+ prop: DMSProperty,
453
+ read: ReadProperties,
454
+ enum_collections: dict[str, dict[str, Any]],
455
+ row_no: int,
456
+ container_ref_by_entity: dict[ContainerReference, ParsedEntity],
457
+ container_entities: set[ParsedEntity],
387
458
  ) -> None:
388
459
  loc = (self.Sheets.properties, row_no)
389
460
  data = self.read_container_property(prop, enum_collections, loc=loc)
390
461
  container_prop = self._validate_obj(ContainerPropertyDefinition, data, loc)
391
- if container_prop is not None and prop.container and prop.container_property:
462
+ if container_prop is None:
463
+ return None
464
+ if not (prop.container and prop.container_property):
465
+ return None
466
+ if prop.container in container_entities:
392
467
  read.container[(prop.container, prop.container_property)].append(
393
468
  ReadContainerProperty(prop.container_property, row_no, container_prop)
394
469
  )
470
+ return None
471
+ # The container entity was not found in the containers table. This could either be because the container
472
+ # is missing, or because either the container entity in the Properties table and the Container table are
473
+ # specified with/without default space/version inconsistently.
474
+ try:
475
+ container_ref = ContainerReference.model_validate(self._create_container_ref(prop.container))
476
+ except ValidationError:
477
+ # Error will be reported when reading the containers
478
+ return None
479
+ if container_ref in container_ref_by_entity:
480
+ container_ref_entity = container_ref_by_entity[container_ref]
481
+ read.container[(container_ref_entity, prop.container_property)].append(
482
+ ReadContainerProperty(prop.container_property, row_no, container_prop)
483
+ )
484
+ # Container can be in CDF, this will be reported by a validator later.
395
485
  return None
396
486
 
397
- def _process_index(self, prop: DMSProperty, read: ReadProperties, row_no: int) -> None:
487
+ def _process_index(
488
+ self,
489
+ prop: DMSProperty,
490
+ read: ReadProperties,
491
+ row_no: int,
492
+ container_ref_by_entity: dict[ContainerReference, ParsedEntity],
493
+ container_entities: set[ParsedEntity],
494
+ ) -> None:
398
495
  if prop.index is None or prop.container_property is None or prop.container is None:
399
496
  return
400
497
 
@@ -405,11 +502,41 @@ class DMSTableReader:
405
502
  if created is None:
406
503
  continue
407
504
  order = self._read_order(index.properties, loc)
408
- read.indices[(prop.container, index.suffix)].append(
409
- ReadIndex(
410
- prop_id=prop.container_property, order=order, row_no=row_no, index_id=index.suffix, index=created
505
+
506
+ if prop.container in container_entities:
507
+ read.indices[(prop.container, index.suffix)].append(
508
+ ReadIndex(
509
+ prop_id=prop.container_property,
510
+ order=order,
511
+ row_no=row_no,
512
+ index_id=index.suffix,
513
+ index=created,
514
+ )
411
515
  )
412
- )
516
+ continue
517
+ # The container entity was not found in the containers table. This could either be because the
518
+ # container is missing, or because either the container entity in the Properties table and the
519
+ # Container table are specified with/without default space/version inconsistently.
520
+
521
+ try:
522
+ container_ref = ContainerReference.model_validate(self._create_container_ref(prop.container))
523
+ except ValidationError:
524
+ # Error will be reported when reading the containers
525
+ continue
526
+ if container_ref in container_ref_by_entity:
527
+ container_ref_entity = container_ref_by_entity[container_ref]
528
+ read.indices[(container_ref_entity, index.suffix)].append(
529
+ ReadIndex(
530
+ prop_id=prop.container_property,
531
+ order=order,
532
+ row_no=row_no,
533
+ index_id=index.suffix,
534
+ index=created,
535
+ )
536
+ )
537
+ else:
538
+ # Error is reported when reading the property.
539
+ ...
413
540
 
414
541
  def _read_order(self, properties: dict[str, Any], loc: tuple[str | int, ...]) -> int | None:
415
542
  if "order" not in properties:
@@ -433,7 +560,14 @@ class DMSTableReader:
433
560
  **index.properties,
434
561
  }
435
562
 
436
- def _process_constraint(self, prop: DMSProperty, read: ReadProperties, row_no: int) -> None:
563
+ def _process_constraint(
564
+ self,
565
+ prop: DMSProperty,
566
+ read: ReadProperties,
567
+ row_no: int,
568
+ container_ref_by_entity: dict[ContainerReference, ParsedEntity],
569
+ container_entities: set[ParsedEntity],
570
+ ) -> None:
437
571
  if prop.constraint is None or prop.container_property is None or prop.container is None:
438
572
  return
439
573
  loc = (self.Sheets.properties, row_no, self.PropertyColumns.constraint)
@@ -443,15 +577,40 @@ class DMSTableReader:
443
577
  if created is None:
444
578
  continue
445
579
  order = self._read_order(constraint.properties, loc)
446
- read.constraints[(prop.container, constraint.suffix)].append(
447
- ReadConstraint(
448
- prop_id=prop.container_property,
449
- order=order,
450
- constraint_id=constraint.suffix,
451
- row_no=row_no,
452
- constraint=created,
580
+
581
+ if prop.container in container_entities:
582
+ read.constraints[(prop.container, constraint.suffix)].append(
583
+ ReadConstraint(
584
+ prop_id=prop.container_property,
585
+ order=order,
586
+ constraint_id=constraint.suffix,
587
+ row_no=row_no,
588
+ constraint=created,
589
+ )
453
590
  )
454
- )
591
+ continue
592
+ # The container entity was not found in the containers table. This could either be because the
593
+ # container is missing, or because either the container entity in the Properties table and the
594
+ # Container table are specified with/without default space/version inconsistently.
595
+ try:
596
+ container_ref = ContainerReference.model_validate(self._create_container_ref(prop.container))
597
+ except ValidationError:
598
+ # Error will be reported when reading the containers
599
+ continue
600
+ if container_ref in container_ref_by_entity:
601
+ container_ref_entity = container_ref_by_entity[container_ref]
602
+ read.constraints[(container_ref_entity, constraint.suffix)].append(
603
+ ReadConstraint(
604
+ prop_id=prop.container_property,
605
+ order=order,
606
+ constraint_id=constraint.suffix,
607
+ row_no=row_no,
608
+ constraint=created,
609
+ )
610
+ )
611
+ else:
612
+ # Error is reported when reading the property.
613
+ ...
455
614
 
456
615
  @staticmethod
457
616
  def read_property_constraint(constraint: ParsedEntity, prop_id: str) -> dict[str, Any]:
@@ -733,16 +892,45 @@ class DMSTableReader:
733
892
  return views_requests, set(rows_by_seen.keys())
734
893
 
735
894
  def read_data_model(self, tables: TableDMS, valid_view_entities: set[ParsedEntity]) -> DataModelRequest:
736
- data = {
895
+ data: dict[str, Any] = {
737
896
  **{meta.key: meta.value for meta in tables.metadata},
738
897
  "views": [self._create_view_ref(view.view) for view in tables.views if view.view in valid_view_entities],
739
898
  }
899
+ if description := self._create_description_field(data):
900
+ data["description"] = description
740
901
  model = self._validate_obj(DataModelRequest, data, (self.Sheets.metadata,), field_name="value")
741
902
  if model is None:
742
903
  # This is the last step, so we can raise the error here.
743
904
  raise DataModelImportException(self.errors) from None
744
905
  return model
745
906
 
907
+ def _create_description_field(self, data: dict[str, Any]) -> str | None:
908
+ """DataModelRequest does not have a 'creator' field, this is a special addition that the Neat tables
909
+ format supports (and recommends using). To keep it, Neat adds it to the suffix of the description field.
910
+ """
911
+ if CREATOR_KEY not in data and CREATOR_KEY.title() not in data:
912
+ return None
913
+ creator_val = data.pop(CREATOR_KEY, data.pop(CREATOR_KEY.title(), None))
914
+
915
+ if not creator_val:
916
+ return None
917
+
918
+ creator = str(creator_val)
919
+ # We do a split/join to clean up any spaces around commas. Ensuring that we have a consistent
920
+ # canonical format.
921
+ cleaned_creator = ", ".join(item.strip() for item in creator.split(","))
922
+ if not cleaned_creator:
923
+ return None
924
+ suffix = f"{CREATOR_MARKER}{cleaned_creator}"
925
+ description = data.get("description", "")
926
+ if len(description) + len(suffix) > DATA_MODEL_DESCRIPTION_MAX_LENGTH:
927
+ description = description[: DATA_MODEL_DESCRIPTION_MAX_LENGTH - len(suffix) - 4] + "..."
928
+ if description:
929
+ description = f"{description} {suffix}"
930
+ else:
931
+ description = suffix
932
+ return description
933
+
746
934
  def _parse_entity(self, entity: str, loc: tuple[str | int, ...]) -> ParsedEntity | None:
747
935
  try:
748
936
  parsed = parse_entity(entity)
@@ -4,6 +4,7 @@ CONTAINER_AND_VIEW_PROPERTIES_IDENTIFIER_PATTERN = r"^[a-zA-Z0-9][a-zA-Z0-9_-]{0
4
4
  INSTANCE_ID_PATTERN = r"^[^\x00]{1,256}$"
5
5
  ENUM_VALUE_IDENTIFIER_PATTERN = r"^[_A-Za-z][_0-9A-Za-z]{0,127}$"
6
6
  DM_VERSION_PATTERN = r"^[a-zA-Z0-9]([.a-zA-Z0-9_-]{0,41}[a-zA-Z0-9])?$"
7
+ DATA_MODEL_DESCRIPTION_MAX_LENGTH = 1024
7
8
  FORBIDDEN_ENUM_VALUES = frozenset({"true", "false", "null"})
8
9
  FORBIDDEN_SPACES = frozenset(["space", "cdf", "dms", "pg3", "shared", "system", "node", "edge"])
9
10
  FORBIDDEN_CONTAINER_AND_VIEW_EXTERNAL_IDS = frozenset(
@@ -6,6 +6,7 @@ from pydantic_core.core_schema import FieldSerializationInfo
6
6
 
7
7
  from ._base import APIResource, Resource, WriteableResource
8
8
  from ._constants import (
9
+ DATA_MODEL_DESCRIPTION_MAX_LENGTH,
9
10
  DM_EXTERNAL_ID_PATTERN,
10
11
  DM_VERSION_PATTERN,
11
12
  SPACE_FORMAT_PATTERN,
@@ -46,7 +47,7 @@ class DataModel(Resource, APIResource[DataModelReference], ABC):
46
47
  description: str | None = Field(
47
48
  default=None,
48
49
  description="Description of the data model.",
49
- max_length=1024,
50
+ max_length=DATA_MODEL_DESCRIPTION_MAX_LENGTH,
50
51
  )
51
52
  # The API supports View here, but in Neat we will only use ViewReference
52
53
  views: list[ViewReference] | None = Field(
@@ -1,12 +1,13 @@
1
1
  let currentFilter = 'all';
2
2
  let currentSearch = '';
3
- let isDarkMode = localStorage.getItem('neat-issues-theme') === 'dark';
3
+ const storageKey = 'neat-issues-theme-' + uniqueId;
4
+ let isDarkMode = localStorage.getItem(storageKey) === 'dark';
4
5
  let expandedGroups = new Set();
5
6
 
6
- const container = document.getElementById('issuesContainer');
7
- const themeToggle = document.getElementById('themeToggle');
8
- const themeIcon = document.getElementById('themeIcon');
9
- const themeText = document.getElementById('themeText');
7
+ const container = document.getElementById('issuesContainer-' + uniqueId);
8
+ const themeToggle = document.getElementById('themeToggle-' + uniqueId);
9
+ const themeIcon = document.getElementById('themeIcon-' + uniqueId);
10
+ const themeText = document.getElementById('themeText-' + uniqueId);
10
11
 
11
12
  // Initialize theme
12
13
  function updateTheme() {
@@ -26,7 +27,7 @@ updateTheme();
26
27
  // Theme toggle
27
28
  themeToggle.addEventListener('click', function() {
28
29
  isDarkMode = !isDarkMode;
29
- localStorage.setItem('neat-issues-theme', isDarkMode ? 'dark' : 'light');
30
+ localStorage.setItem(storageKey, isDarkMode ? 'dark' : 'light');
30
31
  updateTheme();
31
32
  });
32
33
 
@@ -46,7 +47,7 @@ function groupIssues(issuesList) {
46
47
  }
47
48
 
48
49
  function renderIssues() {
49
- const listContainer = document.getElementById('issuesList');
50
+ const listContainer = document.getElementById('issuesList-' + uniqueId);
50
51
  const filtered = issues.filter(issue => {
51
52
  const matchesFilter = currentFilter === 'all' || issue.type === currentFilter;
52
53
  const matchesSearch = !currentSearch ||
@@ -93,7 +94,7 @@ function renderIssues() {
93
94
  // Grouped issues
94
95
  html.push(`
95
96
  <div class="issue-group ${isExpanded ? 'expanded' : ''}">
96
- <div class="issue-group-header" onclick="toggleGroup('${key}')">
97
+ <div class="issue-group-header" onclick="toggleGroup_${uniqueId}('${key}')">
97
98
  <div class="issue-group-info">
98
99
  <span class="expand-icon">${isExpanded ? '▼' : '▶'}</span>
99
100
  <span class="issue-badge badge-${firstIssue.type}">${firstIssue.type}</span>
@@ -123,7 +124,7 @@ function renderIssues() {
123
124
  listContainer.innerHTML = html.join('');
124
125
  }
125
126
 
126
- window.toggleGroup = function(key) {
127
+ window['toggleGroup_' + uniqueId] = function(key) {
127
128
  if (expandedGroups.has(key)) {
128
129
  expandedGroups.delete(key);
129
130
  } else {
@@ -133,9 +134,9 @@ window.toggleGroup = function(key) {
133
134
  };
134
135
 
135
136
  // Stat item filters
136
- document.querySelectorAll('.stat-item').forEach(item => {
137
+ document.querySelectorAll('#issuesContainer-' + uniqueId + ' .stat-item').forEach(item => {
137
138
  item.addEventListener('click', function() {
138
- document.querySelectorAll('.stat-item').forEach(i => i.classList.remove('active'));
139
+ document.querySelectorAll('#issuesContainer-' + uniqueId + ' .stat-item').forEach(i => i.classList.remove('active'));
139
140
  this.classList.add('active');
140
141
  currentFilter = this.dataset.filter;
141
142
  renderIssues();
@@ -143,13 +144,13 @@ document.querySelectorAll('.stat-item').forEach(item => {
143
144
  });
144
145
 
145
146
  // Search
146
- document.getElementById('searchInput').addEventListener('input', function(e) {
147
+ document.getElementById('searchInput-' + uniqueId).addEventListener('input', function(e) {
147
148
  currentSearch = e.target.value;
148
149
  renderIssues();
149
150
  });
150
151
 
151
152
  // Export function
152
- window.exportIssues = function() {
153
+ window['exportIssues_' + uniqueId] = function() {
153
154
  const csv = [
154
155
  ['Type', 'Code', 'Message', 'Fix'],
155
156
  ...issues.map(i => [i.type, i.code || '', i.message, i.fix || ''])
@@ -3,11 +3,11 @@
3
3
  {{SPECIFIC_CSS}}
4
4
  </style>
5
5
 
6
- <div class="issues-container" id="issuesContainer">
6
+ <div class="issues-container" id="issuesContainer-{{unique_id}}">
7
7
  <div class="issues-header">
8
- <button class="theme-toggle" id="themeToggle">
9
- <span id="themeIcon">🌙</span>
10
- <span id="themeText">Dark</span>
8
+ <button class="theme-toggle" id="themeToggle-{{unique_id}}">
9
+ <span id="themeIcon-{{unique_id}}">🌙</span>
10
+ <span id="themeText-{{unique_id}}">Dark</span>
11
11
  </button>
12
12
  <h2 class="issues-title">Session Issues</h2>
13
13
  <div class="issues-stats">
@@ -23,22 +23,23 @@
23
23
  <div class="stat-item" data-filter="Recommendation">
24
24
  <span class="stat-number">{{recommendations}}</span> Recommendations
25
25
  </div>
26
- <button class="export-btn" onclick="exportIssues()">Export CSV</button>
26
+ <button class="export-btn" onclick="exportIssues_{{unique_id}}()">Export CSV</button>
27
27
  </div>
28
28
  </div>
29
29
 
30
30
  <div class="issues-controls">
31
31
  <div class="control-group">
32
- <input type="text" class="search-input" placeholder="🔍 Search messages, codes, fixes..." id="searchInput">
32
+ <input type="text" class="search-input" placeholder="🔍 Search messages, codes, fixes..." id="searchInput-{{unique_id}}">
33
33
  </div>
34
34
  </div>
35
35
 
36
- <div class="issues-list" id="issuesList"></div>
36
+ <div class="issues-list" id="issuesList-{{unique_id}}"></div>
37
37
  </div>
38
38
 
39
39
  <script>
40
40
  (function() {
41
41
  const issues = {{JSON}};
42
+ const uniqueId = '{{unique_id}}';
42
43
  {{SCRIPTS}}
43
44
  })();
44
45
  </script>
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import uuid
2
3
  from collections import defaultdict
3
4
  from typing import Any
4
5
 
@@ -65,12 +66,16 @@ class Issues:
65
66
  return "<b>No issues found.</b>"
66
67
  stats = self._stats
67
68
 
69
+ # Generate unique ID for this render to avoid conflicts in Jupyter
70
+ unique_id = uuid.uuid4().hex[:8]
71
+
68
72
  template_vars = {
69
73
  "JSON": json.dumps(self._serialized_issues),
70
74
  "total": stats["total"],
71
75
  "syntax_errors": stats["by_type"].get("ModelSyntaxError", 0),
72
76
  "consistency_errors": stats["by_type"].get("ConsistencyError", 0),
73
77
  "recommendations": stats["by_type"].get("Recommendation", 0),
78
+ "unique_id": unique_id,
74
79
  }
75
80
 
76
81
  return render("issues", template_vars)
cognite/neat/_version.py CHANGED
@@ -1,2 +1,2 @@
1
- __version__ = "0.127.19"
1
+ __version__ = "0.127.21"
2
2
  __engine__ = "^2.0.4"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cognite-neat
3
- Version: 0.127.19
3
+ Version: 0.127.21
4
4
  Summary: Knowledge graph transformation
5
5
  Project-URL: Documentation, https://cognite-neat.readthedocs-hosted.com/
6
6
  Project-URL: Homepage, https://cognite-neat.readthedocs-hosted.com/
@@ -1,7 +1,7 @@
1
1
  cognite/neat/__init__.py,sha256=Lo4DbjDOwnhCYUoAgPp5RG1fDdF7OlnomalTe7n1ydw,211
2
2
  cognite/neat/_exceptions.py,sha256=ox-5hXpee4UJlPE7HpuEHV2C96aLbLKo-BhPDoOAzhA,1650
3
3
  cognite/neat/_issues.py,sha256=wH1mnkrpBsHUkQMGUHFLUIQWQlfJ_qMfdF7q0d9wNhY,1871
4
- cognite/neat/_version.py,sha256=vcaIvZBc2oDGmy4pADaminCj_YlrXYEvuNvOXkG7FCM,47
4
+ cognite/neat/_version.py,sha256=6TSmw-RWqsWG5I8-dNbvOb-gEQhv4rs3V4jd6qB5P8s,47
5
5
  cognite/neat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  cognite/neat/v1.py,sha256=owqW5Mml2DSZx1AvPvwNRTBngfhBNrQ6EH-7CKL7Jp0,61
7
7
  cognite/neat/_client/__init__.py,sha256=75Bh7eGhaN4sOt3ZcRzHl7pXaheu1z27kmTHeaI05vo,114
@@ -33,14 +33,14 @@ cognite/neat/_data_model/exporters/_base.py,sha256=rG_qAU5i5Hh5hUMep2UmDFFZID4x3
33
33
  cognite/neat/_data_model/exporters/_table_exporter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
34
  cognite/neat/_data_model/exporters/_table_exporter/exporter.py,sha256=4BPu_Chtjh1EyOaKbThXYohsqllVOkCbSoNekNZuBXc,5159
35
35
  cognite/neat/_data_model/exporters/_table_exporter/workbook.py,sha256=1Afk1WqeNe9tiNeSAm0HrF8jTQ1kTbIv1D9hMztKwO8,18482
36
- cognite/neat/_data_model/exporters/_table_exporter/writer.py,sha256=X5q0kGQA4LuIbT0DLpPbKBjut1x9JELBnc9PTFP8O4o,18163
36
+ cognite/neat/_data_model/exporters/_table_exporter/writer.py,sha256=QsO2BWB-_Jw_hpawHtG26NOnLu6wwtDosT-c1acNLPw,19270
37
37
  cognite/neat/_data_model/importers/__init__.py,sha256=dHnKnC_AXk42z6wzEHK15dxIOh8xSEkuUf_AFRZls0E,193
38
38
  cognite/neat/_data_model/importers/_api_importer.py,sha256=H8Ow3Tt7utuAuBhC6s7yWvhGqunHAtE0r0XRsVAr6IE,7280
39
39
  cognite/neat/_data_model/importers/_base.py,sha256=NRB0FcEBj4GaethU68nRffBfTedBBA866A3zfJNfmiQ,433
40
40
  cognite/neat/_data_model/importers/_table_importer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
- cognite/neat/_data_model/importers/_table_importer/data_classes.py,sha256=5gFeAjAYFlzOBb9cfKZWeih5psKynpJwPMNel7h6cO8,9496
41
+ cognite/neat/_data_model/importers/_table_importer/data_classes.py,sha256=7oy0dYXj2lW2F-9jzrosSAIlDBzcAewMu8e5sU5lHPw,9618
42
42
  cognite/neat/_data_model/importers/_table_importer/importer.py,sha256=lQ4_Gpv0haEwQEDYZJaxtR9dL6Y0ys9jbjFfWxH6s2o,8870
43
- cognite/neat/_data_model/importers/_table_importer/reader.py,sha256=v7_PWuOtUQwRTTkZRw4z37G2IF9BtrMozmnnaM9avKE,40759
43
+ cognite/neat/_data_model/importers/_table_importer/reader.py,sha256=I9-zHCpJLo7bj4BabAzSgNBDVUAocdhlvBfy95JkWRw,49451
44
44
  cognite/neat/_data_model/importers/_table_importer/source.py,sha256=h7u5ur5oetmvBs3wgj7Ody5uPF21QwxeAceoIhJ5qzo,3300
45
45
  cognite/neat/_data_model/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
46
  cognite/neat/_data_model/models/conceptual/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -51,10 +51,10 @@ cognite/neat/_data_model/models/conceptual/_properties.py,sha256=CpF37vJYBTLT4DH
51
51
  cognite/neat/_data_model/models/conceptual/_property.py,sha256=blSZQxX52zaILAtjUkldPzPeysz7wnG-UGSNU5tacI8,4138
52
52
  cognite/neat/_data_model/models/dms/__init__.py,sha256=CW5NPMRrMyY4iyZgqYb8eZkRuwbbXUDSVNMWep3zEPI,5326
53
53
  cognite/neat/_data_model/models/dms/_base.py,sha256=931ODXnhrBrzf6vkjqu2IaFz8r2gGxuapn85yN_jkgg,889
54
- cognite/neat/_data_model/models/dms/_constants.py,sha256=_S-62UpDnBteLN2hLNCXBnHuMYSwK-cEBh3JKslv5GA,1343
54
+ cognite/neat/_data_model/models/dms/_constants.py,sha256=TaoE9kmNVEaTl_dDrZQL7YzgP4K13ff0Rc7nr4zbIgg,1384
55
55
  cognite/neat/_data_model/models/dms/_constraints.py,sha256=cyGgDlByXAuSMWJg7Oc25fkp33LsA61M927bCzTWlbo,1458
56
56
  cognite/neat/_data_model/models/dms/_container.py,sha256=wtQbNUwtpymltT1jav8wD4kIfjaIYnvhhz1KS0ffAbo,6044
57
- cognite/neat/_data_model/models/dms/_data_model.py,sha256=FyC5rJUGQmF32qBwVL2lEGf_Gw1Z438FfRr1dSNe9-Y,3151
57
+ cognite/neat/_data_model/models/dms/_data_model.py,sha256=tq_JGNN-1JxG46bhBhunZiLedklYbDXFEfINB0x3a3Q,3219
58
58
  cognite/neat/_data_model/models/dms/_data_types.py,sha256=FMt_d5aJD-o3s9VQWyyCVlHk7D_p3RlSNXBP1OACPs4,6424
59
59
  cognite/neat/_data_model/models/dms/_http.py,sha256=YIRRowqkphFAYkx3foTeLyPMe9fNnmzhUCBDXe0u9Kk,926
60
60
  cognite/neat/_data_model/models/dms/_indexes.py,sha256=ZtXe8ABuRcsAwRIZ9FCanS3uwZHpkOAhvDvjSXtx_Fs,900
@@ -83,7 +83,7 @@ cognite/neat/_data_model/validation/dms/_limits.py,sha256=LVxF1qmeEdUVVAPmywCgxW
83
83
  cognite/neat/_data_model/validation/dms/_orchestrator.py,sha256=FChXB7zflQtnR46BRUFkDCwosLu9YeFLeydEoyFtpVI,10176
84
84
  cognite/neat/_data_model/validation/dms/_views.py,sha256=3bHEEbFKTR_QH_tiJYHppQsZ9ruApv-kdyfehEjIlCU,4198
85
85
  cognite/neat/_session/__init__.py,sha256=owqW5Mml2DSZx1AvPvwNRTBngfhBNrQ6EH-7CKL7Jp0,61
86
- cognite/neat/_session/_issues.py,sha256=M_tYwYiHvvYToaVHCqeSIn4oAO0RZuVtxzd5W52Tn8s,2558
86
+ cognite/neat/_session/_issues.py,sha256=E8UQeSJURg2dm4MF1pfD9dp-heSRT7pgQZgKlD1-FGs,2723
87
87
  cognite/neat/_session/_opt.py,sha256=QcVK08JMmVzJpD0GKHelbljMOQi6CMD1w-maQOlbyZQ,1350
88
88
  cognite/neat/_session/_physical.py,sha256=VyMvvPyN9khR_26rMS0kVuZg23k5c8Hfs7XDeMrkF_w,10526
89
89
  cognite/neat/_session/_result.py,sha256=P_7d92OSSJAusjKMTTdsc67CKnPDGUSitlGxSk0KB_A,7714
@@ -95,11 +95,11 @@ cognite/neat/_session/_html/static/__init__.py,sha256=ZLQFJMITBgbiyTRaVbFAm1l-Dh
95
95
  cognite/neat/_session/_html/static/deployment.css,sha256=wRv2G0NKIxSq4kyOqd3ajZY60KeT4D6-D3lG-TmzURY,4894
96
96
  cognite/neat/_session/_html/static/deployment.js,sha256=3pcQYaW9NAGfeMotZIUIvA6PsWCyrMkJkz3ykNB5lh4,5490
97
97
  cognite/neat/_session/_html/static/issues.css,sha256=Egvqo2cnY8FKTtZp_v3rTWcIgb1vTJvToNCJJovWm70,3824
98
- cognite/neat/_session/_html/static/issues.js,sha256=vSV6FX_SlMxK9jHo18GrHumTbnVepS7IUqV81vtjHCY,6132
98
+ cognite/neat/_session/_html/static/issues.js,sha256=NHx_iAsZTvpZjOoFsxFU_sxqjF4-F4EdHRmoc2DjpIE,6348
99
99
  cognite/neat/_session/_html/static/shared.css,sha256=uUm5fqK1zrMBWCuAWdUoBRaAj9AO611hUxuGvxMzbzc,4190
100
100
  cognite/neat/_session/_html/templates/__init__.py,sha256=hgufJuBxUZ2nLCMTCxGixmk5ztZF38HzPcvtBkWJwxw,128
101
101
  cognite/neat/_session/_html/templates/deployment.html,sha256=COETMP0EEA46xzPlY-RTHo5TdtonV-dscZr15GAY0fs,3309
102
- cognite/neat/_session/_html/templates/issues.html,sha256=v7NkKqVnuXyA0oNlW5iswNLPTiVAJ87412z7VxZeDks,1523
102
+ cognite/neat/_session/_html/templates/issues.html,sha256=k9Ml3LLeWGhpMfqSoCl6QuNRRy__E6Sa5S_I1Kc33NU,1659
103
103
  cognite/neat/_session/_usage_analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
104
104
  cognite/neat/_session/_usage_analytics/_collector.py,sha256=XRyeIW7Att3pIgLpNxQ2joGdIDijfgrPfX7G13OKz-o,4786
105
105
  cognite/neat/_session/_usage_analytics/_constants.py,sha256=-tVdYrCTMKfuMlbO7AlzC29Nug41ug6uuX9DFuihpJg,561
@@ -312,7 +312,7 @@ cognite/neat/v0/session/engine/__init__.py,sha256=D3MxUorEs6-NtgoICqtZ8PISQrjrr4
312
312
  cognite/neat/v0/session/engine/_import.py,sha256=1QxA2_EK613lXYAHKQbZyw2yjo5P9XuiX4Z6_6-WMNQ,169
313
313
  cognite/neat/v0/session/engine/_interface.py,sha256=3W-cYr493c_mW3P5O6MKN1xEQg3cA7NHR_ev3zdF9Vk,533
314
314
  cognite/neat/v0/session/engine/_load.py,sha256=u0x7vuQCRoNcPt25KJBJRn8sJabonYK4vtSZpiTdP4k,5201
315
- cognite_neat-0.127.19.dist-info/METADATA,sha256=u1ketYdid4zGwQF0RBF2U5MV1DaulTOzLMcvi5bRl-U,9150
316
- cognite_neat-0.127.19.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
317
- cognite_neat-0.127.19.dist-info/licenses/LICENSE,sha256=W8VmvFia4WHa3Gqxq1Ygrq85McUNqIGDVgtdvzT-XqA,11351
318
- cognite_neat-0.127.19.dist-info/RECORD,,
315
+ cognite_neat-0.127.21.dist-info/METADATA,sha256=B9N21gWYbuIj-g0hT8DstCeNKA-QQ838OHJ-c1Ws1FM,9150
316
+ cognite_neat-0.127.21.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
317
+ cognite_neat-0.127.21.dist-info/licenses/LICENSE,sha256=W8VmvFia4WHa3Gqxq1Ygrq85McUNqIGDVgtdvzT-XqA,11351
318
+ cognite_neat-0.127.21.dist-info/RECORD,,