cognite-neat 1.0.29__py3-none-any.whl → 1.0.30__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/_data_model/_analysis.py +423 -7
- cognite/neat/_data_model/_constants.py +1 -0
- cognite/neat/_data_model/validation/dms/__init__.py +8 -0
- cognite/neat/_data_model/validation/dms/_containers.py +15 -1
- cognite/neat/_data_model/validation/dms/_performance.py +194 -0
- cognite/neat/_version.py +1 -1
- {cognite_neat-1.0.29.dist-info → cognite_neat-1.0.30.dist-info}/METADATA +1 -1
- {cognite_neat-1.0.29.dist-info → cognite_neat-1.0.30.dist-info}/RECORD +9 -8
- {cognite_neat-1.0.29.dist-info → cognite_neat-1.0.30.dist-info}/WHEEL +0 -0
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import math
|
|
1
2
|
from collections import defaultdict
|
|
2
|
-
from
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from itertools import chain, combinations
|
|
3
6
|
from typing import Literal, TypeAlias, TypeVar
|
|
4
7
|
|
|
5
8
|
import networkx as nx
|
|
6
9
|
from pyparsing import cached_property
|
|
7
10
|
|
|
11
|
+
from cognite.neat._data_model._constants import COGNITE_SPACES
|
|
8
12
|
from cognite.neat._data_model._snapshot import SchemaSnapshot
|
|
9
13
|
from cognite.neat._data_model.models.dms._constraints import RequiresConstraintDefinition
|
|
10
14
|
from cognite.neat._data_model.models.dms._container import ContainerRequest
|
|
@@ -41,6 +45,24 @@ ResourceSource = Literal["auto", "merged", "cdf", "both"]
|
|
|
41
45
|
_NodeT = TypeVar("_NodeT", ContainerReference, ViewReference)
|
|
42
46
|
|
|
43
47
|
|
|
48
|
+
class RequiresChangeStatus(Enum):
|
|
49
|
+
"""Status of requires constraint changes for a view."""
|
|
50
|
+
|
|
51
|
+
OPTIMAL = "optimal" # Already optimized, no changes needed
|
|
52
|
+
CHANGES_AVAILABLE = "changes_available" # Recommendations available
|
|
53
|
+
UNSOLVABLE = "unsolvable" # Structural issue - can't create connected hierarchy
|
|
54
|
+
NO_MODIFIABLE_CONTAINERS = "no_modifiable_containers" # All containers are immutable
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class RequiresChangesForView:
|
|
59
|
+
"""Result of computing requires constraint changes for a view."""
|
|
60
|
+
|
|
61
|
+
to_add: set[tuple[ContainerReference, ContainerReference]]
|
|
62
|
+
to_remove: set[tuple[ContainerReference, ContainerReference]]
|
|
63
|
+
status: RequiresChangeStatus
|
|
64
|
+
|
|
65
|
+
|
|
44
66
|
class ValidationResources:
|
|
45
67
|
def __init__(
|
|
46
68
|
self,
|
|
@@ -528,17 +550,84 @@ class ValidationResources:
|
|
|
528
550
|
graph.add_node(container_ref)
|
|
529
551
|
|
|
530
552
|
# Add edges for requires constraints from all known containers
|
|
531
|
-
for container_ref in graph.nodes():
|
|
553
|
+
for container_ref in list(graph.nodes()):
|
|
532
554
|
container = self.select_container(container_ref)
|
|
533
555
|
if not container or not container.constraints:
|
|
534
556
|
continue
|
|
535
|
-
for constraint in container.constraints.
|
|
557
|
+
for constraint_id, constraint in container.constraints.items():
|
|
536
558
|
if not isinstance(constraint, RequiresConstraintDefinition):
|
|
537
559
|
continue
|
|
538
|
-
|
|
560
|
+
is_auto = constraint_id.endswith("__auto")
|
|
561
|
+
graph.add_edge(container_ref, constraint.require, is_auto=is_auto)
|
|
539
562
|
|
|
540
563
|
return graph
|
|
541
564
|
|
|
565
|
+
@cached_property
|
|
566
|
+
def modifiable_containers(self) -> set[ContainerReference]:
|
|
567
|
+
"""Containers whose requires constraints can be modified in this session.
|
|
568
|
+
|
|
569
|
+
A container is modifiable if:
|
|
570
|
+
- It's NOT in a CDF built-in space (CDM, IDM, etc.)
|
|
571
|
+
- It's a user container brought in through the loaded data model scope or view implements chain
|
|
572
|
+
"""
|
|
573
|
+
return {container_ref for container_ref in self.merged.containers if container_ref.space not in COGNITE_SPACES}
|
|
574
|
+
|
|
575
|
+
@cached_property
|
|
576
|
+
def immutable_requires_constraint_graph(self) -> nx.DiGraph:
|
|
577
|
+
"""Subgraph of requires constraints from non-modifiable (CDM) containers.
|
|
578
|
+
|
|
579
|
+
Used to check reachability via existing immutable constraints.
|
|
580
|
+
"""
|
|
581
|
+
return nx.subgraph_view(
|
|
582
|
+
self.requires_constraint_graph,
|
|
583
|
+
filter_edge=lambda src, _: src not in self.modifiable_containers,
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
@cached_property
|
|
587
|
+
def _fixed_constraint_graph(self) -> nx.DiGraph:
|
|
588
|
+
"""Graph of all fixed constraints (immutable + user-intentional).
|
|
589
|
+
|
|
590
|
+
Both are "fixed" from the optimizer's perspective - existing paths that can't be changed.
|
|
591
|
+
"""
|
|
592
|
+
G = nx.DiGraph()
|
|
593
|
+
G.add_edges_from(self.immutable_requires_constraint_graph.edges())
|
|
594
|
+
G.add_edges_from(self._user_intentional_constraints)
|
|
595
|
+
return G
|
|
596
|
+
|
|
597
|
+
@cached_property
|
|
598
|
+
def _fixed_descendants(self) -> defaultdict[ContainerReference, set[ContainerReference]]:
|
|
599
|
+
"""Pre-compute descendants via fixed constraints. Missing keys return empty set."""
|
|
600
|
+
result: defaultdict[ContainerReference, set[ContainerReference]] = defaultdict(set)
|
|
601
|
+
for container in self._fixed_constraint_graph.nodes():
|
|
602
|
+
result[container] = nx.descendants(self._fixed_constraint_graph, container)
|
|
603
|
+
return result
|
|
604
|
+
|
|
605
|
+
@cached_property
|
|
606
|
+
def _existing_requires_edges(self) -> set[tuple[ContainerReference, ContainerReference]]:
|
|
607
|
+
"""Cached set of existing requires constraint edges."""
|
|
608
|
+
return set(self.requires_constraint_graph.edges())
|
|
609
|
+
|
|
610
|
+
@cached_property
|
|
611
|
+
def _user_intentional_constraints(self) -> set[tuple[ContainerReference, ContainerReference]]:
|
|
612
|
+
"""Constraints that appear to be user-intentional and should not be auto-removed
|
|
613
|
+
|
|
614
|
+
A constraint is user-intentional if:
|
|
615
|
+
1. The constraint identifier does NOT have '__auto' postfix
|
|
616
|
+
2. Neither src nor dst is part of a cycle (cyclic constraints are errors)
|
|
617
|
+
|
|
618
|
+
These constraints are preserved even if they're not in the optimal structure, because
|
|
619
|
+
they may be used for data integrity purposes.
|
|
620
|
+
We DON'T consider manual-created constraints as user-intended if they form part of a cycle,
|
|
621
|
+
because that indicates a problem with the data model where we likely can provide a better solution.
|
|
622
|
+
"""
|
|
623
|
+
containers_in_cycles = {container for cycle in self.requires_constraint_cycles for container in cycle}
|
|
624
|
+
|
|
625
|
+
return {
|
|
626
|
+
(src, dst)
|
|
627
|
+
for src, dst, data in self.requires_constraint_graph.edges(data=True)
|
|
628
|
+
if not data.get("is_auto", False) and src not in containers_in_cycles and dst not in containers_in_cycles
|
|
629
|
+
}
|
|
630
|
+
|
|
542
631
|
@staticmethod
|
|
543
632
|
def forms_directed_path(nodes: set[_NodeT], graph: nx.DiGraph) -> bool:
|
|
544
633
|
"""Check if nodes form an uninterrupted directed path in the graph.
|
|
@@ -565,8 +654,14 @@ class ValidationResources:
|
|
|
565
654
|
if len(nodes) <= 1:
|
|
566
655
|
return True
|
|
567
656
|
|
|
568
|
-
|
|
569
|
-
|
|
657
|
+
# Filter to nodes that are actually in the graph
|
|
658
|
+
nodes_in_graph = {n for n in nodes if n in graph}
|
|
659
|
+
if len(nodes_in_graph) < len(nodes):
|
|
660
|
+
# Some nodes aren't in the graph, so we can't form a complete path
|
|
661
|
+
return False
|
|
662
|
+
|
|
663
|
+
for candidate in nodes_in_graph:
|
|
664
|
+
others = nodes_in_graph - {candidate}
|
|
570
665
|
if others.issubset(nx.descendants(graph, candidate)):
|
|
571
666
|
return True
|
|
572
667
|
|
|
@@ -578,10 +673,331 @@ class ValidationResources:
|
|
|
578
673
|
Returns:
|
|
579
674
|
List of lists, where each list contains the ordered containers involved in forming the requires cycle.
|
|
580
675
|
"""
|
|
581
|
-
|
|
582
676
|
return self.graph_cycles(self.requires_constraint_graph)
|
|
583
677
|
|
|
584
678
|
@staticmethod
|
|
585
679
|
def graph_cycles(graph: nx.DiGraph) -> list[list[T_Reference]]:
|
|
586
680
|
"""Returns cycles in the graph otherwise empty list"""
|
|
587
681
|
return [candidate for candidate in nx.simple_cycles(graph) if len(candidate) > 1]
|
|
682
|
+
|
|
683
|
+
@cached_property
|
|
684
|
+
def optimized_requires_constraint_graph(self) -> nx.DiGraph:
|
|
685
|
+
"""Target state of requires constraints after optimizing (MST + fixed constraints)."""
|
|
686
|
+
graph = nx.DiGraph()
|
|
687
|
+
graph.add_edges_from(self.oriented_mst_edges)
|
|
688
|
+
graph.add_edges_from(self._fixed_constraint_graph.edges())
|
|
689
|
+
return graph
|
|
690
|
+
|
|
691
|
+
@cached_property
|
|
692
|
+
def _requires_candidate_graph(self) -> nx.Graph:
|
|
693
|
+
"""Build weighted candidate graph for requires constraints.
|
|
694
|
+
|
|
695
|
+
Contains all container pairs that appear together in any view, with:
|
|
696
|
+
- weight: minimum directional weight (for MST computation)
|
|
697
|
+
- preferred_direction: direction with lower weight (for tie-breaking)
|
|
698
|
+
|
|
699
|
+
This graph is used to compute per-view MSTs.
|
|
700
|
+
"""
|
|
701
|
+
G = nx.Graph()
|
|
702
|
+
|
|
703
|
+
for view_ref in self.merged.views:
|
|
704
|
+
containers = self.containers_by_view.get(view_ref, set())
|
|
705
|
+
if len(containers) < 2:
|
|
706
|
+
continue # Need at least 2 containers to form a requires constraint
|
|
707
|
+
|
|
708
|
+
# Sort for deterministic preferred_direction when weights are equal
|
|
709
|
+
for src, dst in combinations(sorted(containers, key=str), 2):
|
|
710
|
+
if G.has_edge(src, dst):
|
|
711
|
+
continue # Already added from another view
|
|
712
|
+
|
|
713
|
+
w1 = self._compute_requires_edge_weight(src, dst)
|
|
714
|
+
w2 = self._compute_requires_edge_weight(dst, src)
|
|
715
|
+
direction = (src, dst) if w1 <= w2 else (dst, src)
|
|
716
|
+
weight = min(w1, w2)
|
|
717
|
+
|
|
718
|
+
G.add_edge(src, dst, weight=weight, preferred_direction=direction)
|
|
719
|
+
|
|
720
|
+
return G
|
|
721
|
+
|
|
722
|
+
@cached_property
|
|
723
|
+
def _mst_by_view(self) -> dict[ViewReference, nx.Graph]:
|
|
724
|
+
"""Compute per-view MST graphs.
|
|
725
|
+
|
|
726
|
+
Each view gets its own MST over just its containers. This ensures:
|
|
727
|
+
- No routing through containers not in the view
|
|
728
|
+
- Each view gets exactly the edges it needs
|
|
729
|
+
- Voting handles orientation conflicts between views
|
|
730
|
+
|
|
731
|
+
Skips inherently unsolvable views (no immutable anchor + all modifiables are roots).
|
|
732
|
+
"""
|
|
733
|
+
if not self._requires_candidate_graph:
|
|
734
|
+
return {}
|
|
735
|
+
|
|
736
|
+
result: dict[ViewReference, nx.Graph] = {}
|
|
737
|
+
|
|
738
|
+
for view_ref in self.merged.views:
|
|
739
|
+
if view_ref in self._views_with_root_conflicts:
|
|
740
|
+
continue
|
|
741
|
+
|
|
742
|
+
containers = self.containers_by_view.get(view_ref, set())
|
|
743
|
+
if len(containers) < 2: # Need at least 2 containers to have a constraint
|
|
744
|
+
continue
|
|
745
|
+
if not containers.intersection(self.modifiable_containers):
|
|
746
|
+
continue
|
|
747
|
+
|
|
748
|
+
subgraph = self._requires_candidate_graph.subgraph(containers)
|
|
749
|
+
if not nx.is_connected(subgraph):
|
|
750
|
+
continue
|
|
751
|
+
|
|
752
|
+
result[view_ref] = nx.minimum_spanning_tree(subgraph, weight="weight")
|
|
753
|
+
|
|
754
|
+
return result
|
|
755
|
+
|
|
756
|
+
@cached_property
|
|
757
|
+
def _root_by_view(self) -> dict[ViewReference, ContainerReference]:
|
|
758
|
+
"""Map each view (with 2+ containers) to its most view-specific (root) container.
|
|
759
|
+
|
|
760
|
+
Only includes views with 2+ containers since single-container views
|
|
761
|
+
are trivially satisfied and don't need root direction.
|
|
762
|
+
|
|
763
|
+
Selection criteria (in priority order):
|
|
764
|
+
1. Fewest views: Containers appearing in fewer views are more "view-specific"
|
|
765
|
+
2. Has existing constraint: Prefer containers with existing outgoing constraints
|
|
766
|
+
3. Alphabetical: Deterministic tie-breaker
|
|
767
|
+
"""
|
|
768
|
+
result: dict[ViewReference, ContainerReference] = {}
|
|
769
|
+
|
|
770
|
+
for view, containers in self.containers_by_view.items():
|
|
771
|
+
if len(containers) < 2:
|
|
772
|
+
continue
|
|
773
|
+
|
|
774
|
+
modifiable = containers.intersection(self.modifiable_containers)
|
|
775
|
+
if not modifiable:
|
|
776
|
+
continue
|
|
777
|
+
|
|
778
|
+
# Selection priority: fewest views, has existing constraint, alphabetical
|
|
779
|
+
result[view] = min(
|
|
780
|
+
modifiable,
|
|
781
|
+
key=lambda c: (
|
|
782
|
+
len(self.views_by_container.get(c, set())),
|
|
783
|
+
0 if any((c, other) in self._existing_requires_edges for other in containers) else 1,
|
|
784
|
+
str(c),
|
|
785
|
+
),
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
return result
|
|
789
|
+
|
|
790
|
+
@cached_property
|
|
791
|
+
def _views_with_root_conflicts(self) -> set[ViewReference]:
|
|
792
|
+
"""Views where all modifiable containers are forced roots for other views.
|
|
793
|
+
|
|
794
|
+
A view has root conflicts if:
|
|
795
|
+
1. It has no immutable containers (no shared CDM anchor)
|
|
796
|
+
2. All its modifiable containers are already forced roots for other views
|
|
797
|
+
|
|
798
|
+
Such views would require edges between forced roots, causing conflicts.
|
|
799
|
+
"""
|
|
800
|
+
forced_roots = set(self._root_by_view.values())
|
|
801
|
+
unsolvable: set[ViewReference] = set()
|
|
802
|
+
|
|
803
|
+
for view, containers in self.containers_by_view.items():
|
|
804
|
+
modifiable = containers & self.modifiable_containers
|
|
805
|
+
immutable = containers - self.modifiable_containers
|
|
806
|
+
|
|
807
|
+
# Need at least 2 modifiable containers to have a conflict
|
|
808
|
+
if len(modifiable) < 2:
|
|
809
|
+
continue
|
|
810
|
+
|
|
811
|
+
# No immutable anchor AND all modifiables are roots elsewhere
|
|
812
|
+
if not immutable and modifiable <= forced_roots:
|
|
813
|
+
unsolvable.add(view)
|
|
814
|
+
|
|
815
|
+
return unsolvable
|
|
816
|
+
|
|
817
|
+
@cached_property
|
|
818
|
+
def oriented_mst_edges(self) -> set[tuple[ContainerReference, ContainerReference]]:
|
|
819
|
+
"""Orient per-view MST edges by voting across views.
|
|
820
|
+
|
|
821
|
+
Each view votes for edge orientations based on BFS from its root container.
|
|
822
|
+
Views with only 1 modifiable container use 'inf' vote weight to force
|
|
823
|
+
that container as root.
|
|
824
|
+
|
|
825
|
+
Tie-breaker: preferred_direction from weight function.
|
|
826
|
+
|
|
827
|
+
Returns set of directed (src, dst) tuples.
|
|
828
|
+
"""
|
|
829
|
+
edge_votes: dict[tuple[ContainerReference, ContainerReference], float] = defaultdict(float)
|
|
830
|
+
all_edges: set[tuple[ContainerReference, ContainerReference]] = set()
|
|
831
|
+
|
|
832
|
+
# Sort for deterministic iteration (dict order can vary with hash randomization)
|
|
833
|
+
for view in sorted(self._mst_by_view.keys(), key=str):
|
|
834
|
+
mst = self._mst_by_view[view]
|
|
835
|
+
root = self._root_by_view[view] # Always exists for views in _mst_by_view
|
|
836
|
+
containers = self.containers_by_view.get(view, set())
|
|
837
|
+
modifiable_count = len(containers & self.modifiable_containers)
|
|
838
|
+
# Views with only 1 modifiable container have no choice - that container MUST be root
|
|
839
|
+
vote_weight = float("inf") if modifiable_count == 1 else 1.0
|
|
840
|
+
|
|
841
|
+
# BFS from root orients edges away from root (parent → child)
|
|
842
|
+
for parent, child in nx.bfs_edges(mst, root): # type: ignore[func-returns-value]
|
|
843
|
+
if parent in self.modifiable_containers:
|
|
844
|
+
edge_votes[(parent, child)] += vote_weight
|
|
845
|
+
|
|
846
|
+
# Normalize edges to canonical form so votes for same undirected edge are counted together
|
|
847
|
+
for c1, c2 in mst.edges():
|
|
848
|
+
all_edges.add((c1, c2) if str(c1) < str(c2) else (c2, c1))
|
|
849
|
+
|
|
850
|
+
# Pick direction: most votes wins, preferred_direction breaks ties
|
|
851
|
+
oriented: set[tuple[ContainerReference, ContainerReference]] = set()
|
|
852
|
+
|
|
853
|
+
# Sort for deterministic iteration (hash randomization affects set order)
|
|
854
|
+
for c1, c2 in sorted(all_edges, key=lambda e: (str(e[0]), str(e[1]))):
|
|
855
|
+
c1_votes = edge_votes.get((c1, c2), 0)
|
|
856
|
+
c2_votes = edge_votes.get((c2, c1), 0)
|
|
857
|
+
|
|
858
|
+
if c1_votes > c2_votes:
|
|
859
|
+
oriented.add((c1, c2))
|
|
860
|
+
elif c2_votes > c1_votes:
|
|
861
|
+
oriented.add((c2, c1))
|
|
862
|
+
else:
|
|
863
|
+
# Tie-breaker: use preferred_direction from weight function
|
|
864
|
+
oriented.add(self._requires_candidate_graph[c1][c2].get("preferred_direction", (c1, c2)))
|
|
865
|
+
|
|
866
|
+
return oriented
|
|
867
|
+
|
|
868
|
+
@cached_property
|
|
869
|
+
def _transitively_reduced_edges(self) -> set[tuple[ContainerReference, ContainerReference]]:
|
|
870
|
+
"""Reduce MST edges to minimal necessary set (remove edges with alternative paths via immutable)."""
|
|
871
|
+
if not self.oriented_mst_edges:
|
|
872
|
+
return set()
|
|
873
|
+
|
|
874
|
+
# Optimal graph = MST + immutable + user-intentional (these provide existing paths)
|
|
875
|
+
optimal = nx.DiGraph()
|
|
876
|
+
optimal.add_edges_from(self.immutable_requires_constraint_graph.edges())
|
|
877
|
+
optimal.add_edges_from(self._user_intentional_constraints)
|
|
878
|
+
optimal.add_edges_from(self.oriented_mst_edges)
|
|
879
|
+
|
|
880
|
+
reduced = nx.transitive_reduction(optimal)
|
|
881
|
+
|
|
882
|
+
# Return MST edges that survive reduction
|
|
883
|
+
return {e for e in reduced.edges() if e in self.oriented_mst_edges}
|
|
884
|
+
|
|
885
|
+
def get_requires_changes_for_view(self, view: ViewReference) -> RequiresChangesForView:
|
|
886
|
+
"""Get requires constraint changes needed to optimize a view.
|
|
887
|
+
|
|
888
|
+
Returns a RequiresChangesForView with:
|
|
889
|
+
- to_add: New constraints needed where source is mapped in this view
|
|
890
|
+
- to_remove: Existing constraints that are redundant or wrongly oriented
|
|
891
|
+
- status: The optimization status for this view
|
|
892
|
+
"""
|
|
893
|
+
modifiable_containers_in_view = self.containers_by_view.get(view, set()).intersection(
|
|
894
|
+
self.modifiable_containers
|
|
895
|
+
)
|
|
896
|
+
if not modifiable_containers_in_view:
|
|
897
|
+
return RequiresChangesForView(set(), set(), RequiresChangeStatus.NO_MODIFIABLE_CONTAINERS)
|
|
898
|
+
|
|
899
|
+
# Early exit for inherently unsolvable views (no CDM anchor + all modifiables are roots)
|
|
900
|
+
if view in self._views_with_root_conflicts:
|
|
901
|
+
return RequiresChangesForView(
|
|
902
|
+
set[tuple[ContainerReference, ContainerReference]](), set(), RequiresChangeStatus.UNSOLVABLE
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
# Filter edges to those where source is in this view's modifiable containers
|
|
906
|
+
existing_from_view = {
|
|
907
|
+
edge for edge in self._existing_requires_edges if edge[0] in modifiable_containers_in_view
|
|
908
|
+
}
|
|
909
|
+
optimal_for_view = {
|
|
910
|
+
edge for edge in self._transitively_reduced_edges if edge[0] in modifiable_containers_in_view
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
to_add = optimal_for_view - existing_from_view
|
|
914
|
+
|
|
915
|
+
# To remove: existing edges with wrong direction or not in MST (and not needed externally)
|
|
916
|
+
# But NEVER remove user-intentional constraints (manually defined, no __auto postfix)
|
|
917
|
+
to_remove: set[tuple[ContainerReference, ContainerReference]] = set()
|
|
918
|
+
for src, dst in existing_from_view:
|
|
919
|
+
# Skip user-intentional constraints - they were set by the user on purpose
|
|
920
|
+
if (src, dst) in self._user_intentional_constraints:
|
|
921
|
+
continue
|
|
922
|
+
if (dst, src) in self.oriented_mst_edges:
|
|
923
|
+
to_remove.add((src, dst)) # Always remove if opposite direction from optimal solution
|
|
924
|
+
elif (src, dst) not in self.oriented_mst_edges and not (
|
|
925
|
+
self.find_views_mapping_to_containers([src, dst]) - set(self.merged.views)
|
|
926
|
+
):
|
|
927
|
+
to_remove.add((src, dst)) # Remove if not in optimal solution and not needed by external views
|
|
928
|
+
|
|
929
|
+
# Check solvability in optimized state
|
|
930
|
+
if not self.forms_directed_path(
|
|
931
|
+
self.containers_by_view.get(view, set()), self.optimized_requires_constraint_graph
|
|
932
|
+
):
|
|
933
|
+
return RequiresChangesForView(set(), set(), RequiresChangeStatus.UNSOLVABLE)
|
|
934
|
+
|
|
935
|
+
if not to_add and not to_remove:
|
|
936
|
+
return RequiresChangesForView(set(), set(), RequiresChangeStatus.OPTIMAL)
|
|
937
|
+
|
|
938
|
+
return RequiresChangesForView(to_add, to_remove, RequiresChangeStatus.CHANGES_AVAILABLE)
|
|
939
|
+
|
|
940
|
+
# ========================================================================
|
|
941
|
+
# REQUIRES CONSTRAINT MST WEIGHT CONSTANTS
|
|
942
|
+
# ========================================================================
|
|
943
|
+
# Weight = TIER + sub_weight.
|
|
944
|
+
#
|
|
945
|
+
# WHY THIS ENCODING:
|
|
946
|
+
# MST algorithm only support scalar weights, but we need a strict priority
|
|
947
|
+
# hierarchy where USER→USER ALWAYS beats USER→EXTERNAL. By using a large
|
|
948
|
+
# gap (1000) between tiers, sub-weights (max ~50) can never cause a lower
|
|
949
|
+
# tier to beat a higher tier.
|
|
950
|
+
#
|
|
951
|
+
# Tiers (explicit priority order):
|
|
952
|
+
# - Tier 1 (USER→USER): Both containers modifiable - always preferred
|
|
953
|
+
# - Tier 2 (USER→EXTERNAL): Target is CDF/CDM - only when needed
|
|
954
|
+
# - Tier ∞ (FORBIDDEN): Invalid edge, forms cycle or source is not modifiable
|
|
955
|
+
#
|
|
956
|
+
# Sub-weights refine ordering WITHIN a tier (shared views, direction, etc).
|
|
957
|
+
# - These have been empirically tuned through trial and error.
|
|
958
|
+
# ========================================================================
|
|
959
|
+
|
|
960
|
+
# Tier base weights (gap of 1000 ensures tier always wins)
|
|
961
|
+
_TIER_USER_TO_USER = 1000
|
|
962
|
+
_TIER_USER_TO_EXTERNAL = 2000
|
|
963
|
+
_TIER_FORBIDDEN = math.inf
|
|
964
|
+
|
|
965
|
+
# Sub-weight adjustments (applied within tier, max ~100)
|
|
966
|
+
_BONUS_SHARED_VIEWS_PER = 5 # Per shared view (max 5 views → 25)
|
|
967
|
+
_BONUS_SHARED_VIEWS_MAX = 25
|
|
968
|
+
_BONUS_COVERAGE_PER = 5 # Per descendant via immutable edges (max 3 → 15)
|
|
969
|
+
_BONUS_COVERAGE_MAX = 15
|
|
970
|
+
_PENALTY_VIEW_COUNT = 10 # When src is in more views than dst
|
|
971
|
+
|
|
972
|
+
# Tie-breaker for deterministic ordering
|
|
973
|
+
_TIE_BREAKER_DIVISOR = 1e9
|
|
974
|
+
|
|
975
|
+
def _compute_requires_edge_weight(self, src: ContainerReference, dst: ContainerReference) -> float:
|
|
976
|
+
"""Compute the weight/cost of adding edge src → dst.
|
|
977
|
+
|
|
978
|
+
Returns TIER + sub_weight where tier dominates (gap of 1000).
|
|
979
|
+
Sub-weights refine ordering within a tier based on shared views, direction, coverage.
|
|
980
|
+
"""
|
|
981
|
+
# Opposite direction of fixed constraints is forbidden (would conflict with existing path)
|
|
982
|
+
if src in self._fixed_descendants[dst]:
|
|
983
|
+
return self._TIER_FORBIDDEN
|
|
984
|
+
|
|
985
|
+
if src not in self.modifiable_containers:
|
|
986
|
+
return self._TIER_FORBIDDEN
|
|
987
|
+
|
|
988
|
+
src_views = self.views_by_container.get(src, set())
|
|
989
|
+
dst_views = self.views_by_container.get(dst, set())
|
|
990
|
+
|
|
991
|
+
# Sub-weight adjustments
|
|
992
|
+
shared_bonus = min(len(src_views & dst_views) * self._BONUS_SHARED_VIEWS_PER, self._BONUS_SHARED_VIEWS_MAX)
|
|
993
|
+
coverage_bonus = min(len(self._fixed_descendants[dst]) * self._BONUS_COVERAGE_PER, self._BONUS_COVERAGE_MAX)
|
|
994
|
+
view_penalty = self._PENALTY_VIEW_COUNT if len(src_views) > len(dst_views) else 0
|
|
995
|
+
|
|
996
|
+
# Deterministic tie-breaker (very small, only matters when all else is equal)
|
|
997
|
+
edge_str = f"{src.space}:{src.external_id}->{dst.space}:{dst.external_id}"
|
|
998
|
+
tie_breaker = sum(ord(c) for c in edge_str) / self._TIE_BREAKER_DIVISOR
|
|
999
|
+
|
|
1000
|
+
if dst in self.modifiable_containers:
|
|
1001
|
+
return self._TIER_USER_TO_USER - shared_bonus - coverage_bonus + view_penalty + tie_breaker
|
|
1002
|
+
|
|
1003
|
+
return self._TIER_USER_TO_EXTERNAL - shared_bonus - coverage_bonus + tie_breaker
|
|
@@ -37,6 +37,11 @@ from ._limits import (
|
|
|
37
37
|
ViewPropertyCountIsOutOfLimits,
|
|
38
38
|
)
|
|
39
39
|
from ._orchestrator import DmsDataModelValidation
|
|
40
|
+
from ._performance import (
|
|
41
|
+
MissingRequiresConstraint,
|
|
42
|
+
SuboptimalRequiresConstraint,
|
|
43
|
+
UnresolvableQueryPerformance,
|
|
44
|
+
)
|
|
40
45
|
from ._views import ImplementedViewNotExisting, ViewToContainerMappingNotPossible
|
|
41
46
|
|
|
42
47
|
__all__ = [
|
|
@@ -54,6 +59,7 @@ __all__ = [
|
|
|
54
59
|
"ExternalContainerDoesNotExist",
|
|
55
60
|
"ExternalContainerPropertyDoesNotExist",
|
|
56
61
|
"ImplementedViewNotExisting",
|
|
62
|
+
"MissingRequiresConstraint",
|
|
57
63
|
"RequiredContainerDoesNotExist",
|
|
58
64
|
"RequiresConstraintCycle",
|
|
59
65
|
"ReverseConnectionContainerMissing",
|
|
@@ -65,6 +71,8 @@ __all__ = [
|
|
|
65
71
|
"ReverseConnectionSourceViewMissing",
|
|
66
72
|
"ReverseConnectionTargetMismatch",
|
|
67
73
|
"ReverseConnectionTargetMissing",
|
|
74
|
+
"SuboptimalRequiresConstraint",
|
|
75
|
+
"UnresolvableQueryPerformance",
|
|
68
76
|
"ViewContainerCountIsOutOfLimits",
|
|
69
77
|
"ViewImplementsCountIsOutOfLimits",
|
|
70
78
|
"ViewMissingDescription",
|
|
@@ -223,12 +223,26 @@ class RequiresConstraintCycle(DataModelValidator):
|
|
|
223
223
|
|
|
224
224
|
def run(self) -> list[ConsistencyError]:
|
|
225
225
|
errors: list[ConsistencyError] = []
|
|
226
|
+
optimal_edges = self.validation_resources.oriented_mst_edges
|
|
226
227
|
|
|
227
228
|
for cycle in self.validation_resources.requires_constraint_cycles:
|
|
228
229
|
cycle_str = " -> ".join(str(c) for c in cycle) + f" -> {cycle[0]!s}"
|
|
230
|
+
|
|
231
|
+
# Find edges in cycle that are NOT in optimal structure (these should be removed)
|
|
232
|
+
edges_to_remove = []
|
|
233
|
+
for i, container in enumerate(cycle):
|
|
234
|
+
next_container = cycle[(i + 1) % len(cycle)]
|
|
235
|
+
edge = (container, next_container)
|
|
236
|
+
if edge not in optimal_edges:
|
|
237
|
+
edges_to_remove.append(f"{container} -> {next_container}")
|
|
238
|
+
|
|
239
|
+
message = f"Requires constraints form a cycle: {cycle_str}"
|
|
240
|
+
if edges_to_remove:
|
|
241
|
+
message += f". Recommended removal: {', '.join(edges_to_remove)} (not in optimal structure)"
|
|
242
|
+
|
|
229
243
|
errors.append(
|
|
230
244
|
ConsistencyError(
|
|
231
|
-
message=
|
|
245
|
+
message=message,
|
|
232
246
|
fix="Remove one of the requires constraints to break the cycle",
|
|
233
247
|
code=self.code,
|
|
234
248
|
)
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Validators for checking performance-related aspects of the data model."""
|
|
2
|
+
|
|
3
|
+
from cognite.neat._data_model._analysis import RequiresChangeStatus
|
|
4
|
+
from cognite.neat._data_model._constants import COGNITE_SPACES
|
|
5
|
+
from cognite.neat._data_model.validation.dms._base import DataModelValidator
|
|
6
|
+
from cognite.neat._issues import Recommendation
|
|
7
|
+
|
|
8
|
+
BASE_CODE = "NEAT-DMS-PERFORMANCE"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MissingRequiresConstraint(DataModelValidator):
|
|
12
|
+
"""
|
|
13
|
+
Recommends adding requires constraints to optimize query performance.
|
|
14
|
+
|
|
15
|
+
## What it does
|
|
16
|
+
Identifies views that is mapping to containers where adding a requires constraint,
|
|
17
|
+
would improve query performance. The recommendation message indicates whether the
|
|
18
|
+
change is "safe" or requires attention to potential ingestion dependencies.
|
|
19
|
+
|
|
20
|
+
## Why is this important?
|
|
21
|
+
Views without proper requires constraints may have poor query performance.
|
|
22
|
+
Adding requires constraints enables queries to perform under-the-hood optimizations.
|
|
23
|
+
|
|
24
|
+
## Example
|
|
25
|
+
View `Valve` is mapping to both containers `Valve` and `CogniteEquipment`.
|
|
26
|
+
A `requires` constraint from `Valve` to `CogniteEquipment` is likely needed
|
|
27
|
+
to enable efficient query performance.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
code = f"{BASE_CODE}-001"
|
|
31
|
+
issue_type = Recommendation
|
|
32
|
+
alpha = True
|
|
33
|
+
|
|
34
|
+
def run(self) -> list[Recommendation]:
|
|
35
|
+
recommendations: list[Recommendation] = []
|
|
36
|
+
|
|
37
|
+
for view_ref in self.validation_resources.merged.views:
|
|
38
|
+
changes = self.validation_resources.get_requires_changes_for_view(view_ref)
|
|
39
|
+
|
|
40
|
+
if changes.status != RequiresChangeStatus.CHANGES_AVAILABLE:
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
for src, dst in changes.to_add:
|
|
44
|
+
src_views = self.validation_resources.views_by_container.get(src, set())
|
|
45
|
+
other_views_with_src = src_views - {view_ref}
|
|
46
|
+
views_impacted_by_change = self.validation_resources.find_views_mapping_to_containers([src, dst])
|
|
47
|
+
|
|
48
|
+
# Check if this is a "safe" recommendation (no cross-view dependencies)
|
|
49
|
+
is_safe = not other_views_with_src or view_ref in views_impacted_by_change
|
|
50
|
+
|
|
51
|
+
message = (
|
|
52
|
+
f"View '{view_ref!s}' is not optimized for querying. "
|
|
53
|
+
f"Add a 'requires' constraint from '{src!s}' to '{dst!s}'."
|
|
54
|
+
)
|
|
55
|
+
if not is_safe:
|
|
56
|
+
# Find a superset view to suggest for ingestion
|
|
57
|
+
merged_views = set(self.validation_resources.merged.views)
|
|
58
|
+
superset_views = views_impacted_by_change & merged_views
|
|
59
|
+
view_example = min(superset_views, key=str) if superset_views else None
|
|
60
|
+
message += (
|
|
61
|
+
" Note: this causes an ingestion dependency for this view, "
|
|
62
|
+
" if you will be using this view to ingest instances, you will "
|
|
63
|
+
f"need to populate these instances into '{dst!s}' first"
|
|
64
|
+
+ (f", for example through view '{view_example!s}'." if view_example else ".")
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
recommendations.append(
|
|
68
|
+
Recommendation(
|
|
69
|
+
message=message,
|
|
70
|
+
fix="Add requires constraint between the containers",
|
|
71
|
+
code=self.code,
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return recommendations
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class SuboptimalRequiresConstraint(DataModelValidator):
|
|
79
|
+
"""
|
|
80
|
+
Recommends removing requires constraints that are not optimal.
|
|
81
|
+
|
|
82
|
+
## What it does
|
|
83
|
+
Identifies existing requires constraints that are not optimal for querying purposes,
|
|
84
|
+
as they are either redundant or create unnecessary ingestion dependencies when all
|
|
85
|
+
other optimal constraints are applied. These constraints can be safely removed
|
|
86
|
+
without affecting query performance.
|
|
87
|
+
|
|
88
|
+
## Why is this important?
|
|
89
|
+
Unnecessary requires constraints can:
|
|
90
|
+
- Create unnecessary ingestion dependencies
|
|
91
|
+
- Cause invalid requires constraint cycles if optimal constraints are applied
|
|
92
|
+
|
|
93
|
+
## Example
|
|
94
|
+
Container `Tag` has a `requires` constraint to `Pump`, but NEAT determined that
|
|
95
|
+
`Pump → Tag` is more optimal. The existing `Tag → Pump` constraint should then
|
|
96
|
+
be removed when applying all optimal constraints.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
code = f"{BASE_CODE}-002"
|
|
100
|
+
issue_type = Recommendation
|
|
101
|
+
alpha = True
|
|
102
|
+
|
|
103
|
+
def run(self) -> list[Recommendation]:
|
|
104
|
+
recommendations: list[Recommendation] = []
|
|
105
|
+
|
|
106
|
+
for view_ref in self.validation_resources.merged.views:
|
|
107
|
+
changes = self.validation_resources.get_requires_changes_for_view(view_ref)
|
|
108
|
+
|
|
109
|
+
if changes.status != RequiresChangeStatus.CHANGES_AVAILABLE:
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
for src, dst in changes.to_remove:
|
|
113
|
+
recommendations.append(
|
|
114
|
+
Recommendation(
|
|
115
|
+
message=(
|
|
116
|
+
f"View '{view_ref!s}' has a requires constraint '{src!s}' -> '{dst!s}' "
|
|
117
|
+
"that is not part of the optimal structure. Consider removing it."
|
|
118
|
+
),
|
|
119
|
+
fix="Remove the unnecessary requires constraint",
|
|
120
|
+
code=self.code,
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return recommendations
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class UnresolvableQueryPerformance(DataModelValidator):
|
|
128
|
+
"""
|
|
129
|
+
Identifies views with query performance issues that cannot be resolved.
|
|
130
|
+
This is likely to be caused by unintended modeling choices.
|
|
131
|
+
|
|
132
|
+
## What it does
|
|
133
|
+
Detects views where no valid requires constraint solution exists:
|
|
134
|
+
|
|
135
|
+
1. **View maps only to CDF built-in containers**:
|
|
136
|
+
Since CDF containers cannot be modified, no requires can be added.
|
|
137
|
+
|
|
138
|
+
2. **No valid solution**:
|
|
139
|
+
This view is causing issues when optimizing requires constraints
|
|
140
|
+
for other views, due to its structure (mapping non-overlapping containers)
|
|
141
|
+
|
|
142
|
+
## Why is this important?
|
|
143
|
+
These views will have suboptimal query performance that CANNOT be fixed by
|
|
144
|
+
adding or removing requires constraints. The only solutions require restructuring:
|
|
145
|
+
- Add a view-specific container that requires all the other containers in the view
|
|
146
|
+
- Restructure the view to use different containers
|
|
147
|
+
|
|
148
|
+
## Example
|
|
149
|
+
View `MultipleEquipments` maps only to containers `Valve` and `InstrumentEquipment`.
|
|
150
|
+
The optimal constraints are `Valve → CogniteEquipment` and `InstrumentEquipment → CogniteEquipment`
|
|
151
|
+
due to other views needing these constraints to optimize their query performance.
|
|
152
|
+
This means however, that neither Valve nor InstrumentEquipment can reach each other without
|
|
153
|
+
creating complex ingestion dependencies. The view needs a new container or restructuring.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
code = f"{BASE_CODE}-003"
|
|
157
|
+
issue_type = Recommendation
|
|
158
|
+
alpha = True
|
|
159
|
+
|
|
160
|
+
def run(self) -> list[Recommendation]:
|
|
161
|
+
recommendations: list[Recommendation] = []
|
|
162
|
+
|
|
163
|
+
for view_ref in self.validation_resources.merged.views:
|
|
164
|
+
if view_ref.space in COGNITE_SPACES:
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
changes = self.validation_resources.get_requires_changes_for_view(view_ref)
|
|
168
|
+
|
|
169
|
+
if changes.status == RequiresChangeStatus.NO_MODIFIABLE_CONTAINERS:
|
|
170
|
+
recommendations.append(
|
|
171
|
+
Recommendation(
|
|
172
|
+
message=(
|
|
173
|
+
f"View '{view_ref!s}' has poor query performance. "
|
|
174
|
+
"It maps only to CDF built-in containers which cannot have requires constraints. "
|
|
175
|
+
"Consider adding a view-specific container that requires the others."
|
|
176
|
+
),
|
|
177
|
+
fix="Add a container that requires the others, or restructure the view",
|
|
178
|
+
code=self.code,
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
elif changes.status == RequiresChangeStatus.UNSOLVABLE:
|
|
182
|
+
recommendations.append(
|
|
183
|
+
Recommendation(
|
|
184
|
+
message=(
|
|
185
|
+
f"View '{view_ref!s}' has poor query performance. "
|
|
186
|
+
"No valid requires constraint solution was found. "
|
|
187
|
+
"Consider adding a view-specific container that requires the others."
|
|
188
|
+
),
|
|
189
|
+
fix="Add a container that requires the others, or restructure the view",
|
|
190
|
+
code=self.code,
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return recommendations
|
cognite/neat/_version.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
__version__ = "1.0.
|
|
1
|
+
__version__ = "1.0.30"
|
|
2
2
|
__engine__ = "^2.0.4"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cognite-neat
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.30
|
|
4
4
|
Summary: Knowledge graph transformation
|
|
5
5
|
Author: Nikola Vasiljevic, Anders Albert
|
|
6
6
|
Author-email: Nikola Vasiljevic <nikola.vasiljevic@cognite.com>, Anders Albert <anders.albert@cognite.com>
|
|
@@ -17,8 +17,8 @@ cognite/neat/_client/statistics_api.py,sha256=5meeh0v5mxC2SMB7xGdOwMh4KkaX3DtpUj
|
|
|
17
17
|
cognite/neat/_client/views_api.py,sha256=YMaw7IaxU4gmixpd_t1u9JK9BHfNerf5DMNinGPCAa0,3692
|
|
18
18
|
cognite/neat/_config.py,sha256=IJS_R-86M4SLxdo644LZ0dE9h0jg2XBL1A4QKWJj0TQ,10160
|
|
19
19
|
cognite/neat/_data_model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
-
cognite/neat/_data_model/_analysis.py,sha256=
|
|
21
|
-
cognite/neat/_data_model/_constants.py,sha256=
|
|
20
|
+
cognite/neat/_data_model/_analysis.py,sha256=Z5HQ1ExB58Y_2W79ovOXbhyHQiAuB0S86117ZYMMjpg,43285
|
|
21
|
+
cognite/neat/_data_model/_constants.py,sha256=h6FaREqE2KG7hy2ZQVDq-3_yjVjlpSG6Usz2kadIUIs,1722
|
|
22
22
|
cognite/neat/_data_model/_identifiers.py,sha256=lDLvMvYDgRNFgk5GmxWzOUunG7M3synAciNjzJI0m_o,1913
|
|
23
23
|
cognite/neat/_data_model/_shared.py,sha256=H0gFqa8tKFNWuvdat5jL6OwySjCw3aQkLPY3wtb9Wrw,1302
|
|
24
24
|
cognite/neat/_data_model/_snapshot.py,sha256=JBaKmL0Tmprz59SZ1JeB49BPMB8Hqa-OAOt0Bai8cw4,6305
|
|
@@ -76,14 +76,15 @@ cognite/neat/_data_model/models/entities/_data_types.py,sha256=DfdEWGek7gODro-_0
|
|
|
76
76
|
cognite/neat/_data_model/models/entities/_identifiers.py,sha256=a7ojJKY1ErZgUANHscEwkctX4RJ7bWEEWOQt5g5Tsdk,1915
|
|
77
77
|
cognite/neat/_data_model/models/entities/_parser.py,sha256=zef_pSDZYMZrJl4IKreFDR577KutfhtN1xpH3Ayjt2o,7669
|
|
78
78
|
cognite/neat/_data_model/validation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
79
|
-
cognite/neat/_data_model/validation/dms/__init__.py,sha256=
|
|
79
|
+
cognite/neat/_data_model/validation/dms/__init__.py,sha256=Z6P8TTXZR1KIJreDieRVTcJVvovdZXCLXRbzm86UojI,2936
|
|
80
80
|
cognite/neat/_data_model/validation/dms/_ai_readiness.py,sha256=bffMQJ5pqumU5P3KaEdQP67OO5eMKqzN2BAWbUjG6KE,16143
|
|
81
81
|
cognite/neat/_data_model/validation/dms/_base.py,sha256=G_gMPTgKwyBW62UcCkKIBVHWp9ufAZPJ2p7o69_dJI0,820
|
|
82
82
|
cognite/neat/_data_model/validation/dms/_connections.py,sha256=-kUXf2_3V50ckxwXRwJoTHsKkS5zxiBKkkkHg8Dm4WI,30353
|
|
83
83
|
cognite/neat/_data_model/validation/dms/_consistency.py,sha256=IKSUoRQfQQcsymviESW9VuTFX7jsZMXfsObeZHPdov4,2435
|
|
84
|
-
cognite/neat/_data_model/validation/dms/_containers.py,sha256=
|
|
84
|
+
cognite/neat/_data_model/validation/dms/_containers.py,sha256=n2L8HiJLylHIkzGi1veGb2QLVEFZuLlOwws77mxm0Fw,10552
|
|
85
85
|
cognite/neat/_data_model/validation/dms/_limits.py,sha256=rAIh54DJBPi3J7d7eD-jMdldZS8R2vlkQ5MD9RxNsrI,14830
|
|
86
86
|
cognite/neat/_data_model/validation/dms/_orchestrator.py,sha256=qiuUSUmNhekFyBARUUO2yhG-X9AeU_LL49UrJ65JXFA,2964
|
|
87
|
+
cognite/neat/_data_model/validation/dms/_performance.py,sha256=hq4neZfoa0EKdYjhiQpugP9jrhSpTsn8_VJRncTj5Bk,8500
|
|
87
88
|
cognite/neat/_data_model/validation/dms/_views.py,sha256=pRdnj5ZBnHNnbLKleXGbipteGma8_l5AYsDIfqgAil4,6345
|
|
88
89
|
cognite/neat/_exceptions.py,sha256=hOjPL1vFNNAZzqAHFB9l9ek-XJEBKcqiaPk0onwLPns,2540
|
|
89
90
|
cognite/neat/_issues.py,sha256=wH1mnkrpBsHUkQMGUHFLUIQWQlfJ_qMfdF7q0d9wNhY,1871
|
|
@@ -327,9 +328,9 @@ cognite/neat/_v0/session/_template.py,sha256=BNcvrW5y7LWzRM1XFxZkfR1Nc7e8UgjBClH
|
|
|
327
328
|
cognite/neat/_v0/session/_to.py,sha256=AnsRSDDdfFyYwSgi0Z-904X7WdLtPfLlR0x1xsu_jAo,19447
|
|
328
329
|
cognite/neat/_v0/session/_wizard.py,sha256=baPJgXAAF3d1bn4nbIzon1gWfJOeS5T43UXRDJEnD3c,1490
|
|
329
330
|
cognite/neat/_v0/session/exceptions.py,sha256=jv52D-SjxGfgqaHR8vnpzo0SOJETIuwbyffSWAxSDJw,3495
|
|
330
|
-
cognite/neat/_version.py,sha256=
|
|
331
|
+
cognite/neat/_version.py,sha256=6cfMmDsIfl6IMAFwlEU9DPRkWwKrndMYQPZ5zAZvnuk,45
|
|
331
332
|
cognite/neat/legacy.py,sha256=DMFeLCSBLT2enk-nm1KfX1rKR2DQDpxY-w6ThY0y9c8,421
|
|
332
333
|
cognite/neat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
333
|
-
cognite_neat-1.0.
|
|
334
|
-
cognite_neat-1.0.
|
|
335
|
-
cognite_neat-1.0.
|
|
334
|
+
cognite_neat-1.0.30.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
|
|
335
|
+
cognite_neat-1.0.30.dist-info/METADATA,sha256=ixyEZ8MjefyUNCq_1yz_hoqgVeARW7_jFhxCrF0OfW0,6872
|
|
336
|
+
cognite_neat-1.0.30.dist-info/RECORD,,
|
|
File without changes
|