cognite-neat 1.0.28__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/_config.py +3 -0
- cognite/neat/_data_model/_analysis.py +441 -9
- cognite/neat/_data_model/_constants.py +1 -0
- cognite/neat/_data_model/importers/__init__.py +2 -2
- cognite/neat/_data_model/importers/_api_importer.py +168 -3
- 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/_exceptions.py +13 -0
- cognite/neat/_session/_physical.py +55 -1
- cognite/neat/_session/_wrappers.py +20 -2
- cognite/neat/_store/_store.py +4 -1
- cognite/neat/_version.py +1 -1
- {cognite_neat-1.0.28.dist-info → cognite_neat-1.0.30.dist-info}/METADATA +1 -1
- {cognite_neat-1.0.28.dist-info → cognite_neat-1.0.30.dist-info}/RECORD +16 -15
- {cognite_neat-1.0.28.dist-info → cognite_neat-1.0.30.dist-info}/WHEEL +0 -0
cognite/neat/_config.py
CHANGED
|
@@ -105,6 +105,9 @@ 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_solution_model_creation: bool = Field(
|
|
109
|
+
default=False, description="If enabled, neat.physical_data_model.create() will be available"
|
|
110
|
+
)
|
|
108
111
|
enable_cdf_analysis: bool = Field(default=False, description="If enabled, neat.cdf endpoint will be available.")
|
|
109
112
|
|
|
110
113
|
def __setattr__(self, key: str, value: Any) -> None:
|
|
@@ -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,12 +45,34 @@ 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
|
-
self,
|
|
68
|
+
self,
|
|
69
|
+
modus_operandi: ModusOperandi,
|
|
70
|
+
local: SchemaSnapshot,
|
|
71
|
+
cdf: SchemaSnapshot,
|
|
72
|
+
limits: SchemaLimits | None = None,
|
|
47
73
|
) -> None:
|
|
48
74
|
self._modus_operandi = modus_operandi
|
|
49
|
-
self.limits = limits
|
|
75
|
+
self.limits = limits or SchemaLimits()
|
|
50
76
|
|
|
51
77
|
self.local = local
|
|
52
78
|
self.cdf = cdf
|
|
@@ -385,6 +411,18 @@ class ValidationResources:
|
|
|
385
411
|
|
|
386
412
|
return connection_end_node_types
|
|
387
413
|
|
|
414
|
+
@staticmethod
|
|
415
|
+
def is_explicit_connection(property_: ViewRequestProperty) -> bool:
|
|
416
|
+
"""Check if a property is an explicit connection property, meaning end node type is explicitly defined.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
property_: The property to check.
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
True if the property is a connection property, False otherwise.
|
|
423
|
+
"""
|
|
424
|
+
return True if property_.source else False
|
|
425
|
+
|
|
388
426
|
@cached_property
|
|
389
427
|
def views_by_container(self) -> dict[ContainerReference, set[ViewReference]]:
|
|
390
428
|
"""Get a mapping from containers to the views that use them.
|
|
@@ -512,17 +550,84 @@ class ValidationResources:
|
|
|
512
550
|
graph.add_node(container_ref)
|
|
513
551
|
|
|
514
552
|
# Add edges for requires constraints from all known containers
|
|
515
|
-
for container_ref in graph.nodes():
|
|
553
|
+
for container_ref in list(graph.nodes()):
|
|
516
554
|
container = self.select_container(container_ref)
|
|
517
555
|
if not container or not container.constraints:
|
|
518
556
|
continue
|
|
519
|
-
for constraint in container.constraints.
|
|
557
|
+
for constraint_id, constraint in container.constraints.items():
|
|
520
558
|
if not isinstance(constraint, RequiresConstraintDefinition):
|
|
521
559
|
continue
|
|
522
|
-
|
|
560
|
+
is_auto = constraint_id.endswith("__auto")
|
|
561
|
+
graph.add_edge(container_ref, constraint.require, is_auto=is_auto)
|
|
523
562
|
|
|
524
563
|
return graph
|
|
525
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
|
+
|
|
526
631
|
@staticmethod
|
|
527
632
|
def forms_directed_path(nodes: set[_NodeT], graph: nx.DiGraph) -> bool:
|
|
528
633
|
"""Check if nodes form an uninterrupted directed path in the graph.
|
|
@@ -549,8 +654,14 @@ class ValidationResources:
|
|
|
549
654
|
if len(nodes) <= 1:
|
|
550
655
|
return True
|
|
551
656
|
|
|
552
|
-
|
|
553
|
-
|
|
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}
|
|
554
665
|
if others.issubset(nx.descendants(graph, candidate)):
|
|
555
666
|
return True
|
|
556
667
|
|
|
@@ -562,10 +673,331 @@ class ValidationResources:
|
|
|
562
673
|
Returns:
|
|
563
674
|
List of lists, where each list contains the ordered containers involved in forming the requires cycle.
|
|
564
675
|
"""
|
|
565
|
-
|
|
566
676
|
return self.graph_cycles(self.requires_constraint_graph)
|
|
567
677
|
|
|
568
678
|
@staticmethod
|
|
569
679
|
def graph_cycles(graph: nx.DiGraph) -> list[list[T_Reference]]:
|
|
570
680
|
"""Returns cycles in the graph otherwise empty list"""
|
|
571
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
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
from ._api_importer import DMSAPIImporter
|
|
1
|
+
from ._api_importer import DMSAPICreator, DMSAPIImporter
|
|
2
2
|
from ._base import DMSImporter
|
|
3
3
|
from ._table_importer.importer import DMSTableImporter
|
|
4
4
|
|
|
5
|
-
__all__ = ["DMSAPIImporter", "DMSImporter", "DMSTableImporter"]
|
|
5
|
+
__all__ = ["DMSAPICreator", "DMSAPIImporter", "DMSImporter", "DMSTableImporter"]
|
|
@@ -1,19 +1,30 @@
|
|
|
1
1
|
import difflib
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from typing import Any
|
|
3
|
+
from typing import Any, Literal
|
|
4
4
|
|
|
5
5
|
import yaml
|
|
6
6
|
from pydantic import ValidationError
|
|
7
7
|
|
|
8
8
|
from cognite.neat._client import NeatClient
|
|
9
|
+
from cognite.neat._data_model._analysis import ValidationResources
|
|
10
|
+
from cognite.neat._data_model._snapshot import SchemaSnapshot
|
|
9
11
|
from cognite.neat._data_model.importers._base import DMSImporter
|
|
10
12
|
from cognite.neat._data_model.models.dms import (
|
|
11
13
|
DataModelReference,
|
|
12
14
|
RequestSchema,
|
|
13
15
|
SpaceReference,
|
|
14
16
|
)
|
|
15
|
-
from cognite.neat.
|
|
16
|
-
from cognite.neat.
|
|
17
|
+
from cognite.neat._data_model.models.dms._data_model import DataModelRequest
|
|
18
|
+
from cognite.neat._data_model.models.dms._references import ViewReference
|
|
19
|
+
from cognite.neat._data_model.models.dms._views import ViewRequest
|
|
20
|
+
from cognite.neat._data_model.models.entities._parser import parse_entities
|
|
21
|
+
from cognite.neat._exceptions import (
|
|
22
|
+
CDFAPIException,
|
|
23
|
+
DataModelCreateException,
|
|
24
|
+
DataModelImportException,
|
|
25
|
+
FileReadException,
|
|
26
|
+
)
|
|
27
|
+
from cognite.neat._issues import ConsistencyError, ModelSyntaxError
|
|
17
28
|
from cognite.neat._utils.http_client import FailedRequestMessage
|
|
18
29
|
from cognite.neat._utils.text import humanize_collection
|
|
19
30
|
from cognite.neat._utils.validation import ValidationContext, humanize_validation_error
|
|
@@ -164,3 +175,157 @@ class DMSAPIImporter(DMSImporter):
|
|
|
164
175
|
)
|
|
165
176
|
schema_data["dataModel"] = data_model
|
|
166
177
|
return schema_data
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class DMSAPICreator(DMSImporter):
|
|
181
|
+
"""Creates data model from the existing schema components in CDF"""
|
|
182
|
+
|
|
183
|
+
def __init__(
|
|
184
|
+
self,
|
|
185
|
+
cdf_snapshot: SchemaSnapshot,
|
|
186
|
+
space: str,
|
|
187
|
+
external_id: str,
|
|
188
|
+
version: str,
|
|
189
|
+
views: list[str],
|
|
190
|
+
name: str | None = None,
|
|
191
|
+
description: str | None = None,
|
|
192
|
+
kind: Literal["solution"] = "solution",
|
|
193
|
+
) -> None:
|
|
194
|
+
self._resources = ValidationResources("rebuild", local=cdf_snapshot, cdf=cdf_snapshot)
|
|
195
|
+
self._space = space
|
|
196
|
+
self._external_id = external_id
|
|
197
|
+
self._version = version
|
|
198
|
+
self._name = name
|
|
199
|
+
self._description = description
|
|
200
|
+
self._kind = kind
|
|
201
|
+
self._views = views
|
|
202
|
+
|
|
203
|
+
def to_data_model(self) -> RequestSchema:
|
|
204
|
+
if self._kind == "solution":
|
|
205
|
+
return self._to_solution_model()
|
|
206
|
+
else:
|
|
207
|
+
raise NotImplementedError(f"Data model kind '{self._kind}' is not supported yet.")
|
|
208
|
+
|
|
209
|
+
def _to_solution_model(self) -> RequestSchema:
|
|
210
|
+
"""Creates solution data model from provided views"""
|
|
211
|
+
|
|
212
|
+
errors, view_refs = self._parse_view_references(self._views)
|
|
213
|
+
|
|
214
|
+
if not view_refs:
|
|
215
|
+
errors.append(ModelSyntaxError(message="No valid views provided to create the data model."))
|
|
216
|
+
|
|
217
|
+
# Check if all views exist in the cdf snapshot:
|
|
218
|
+
for view in view_refs:
|
|
219
|
+
if not self._resources.select_view(view):
|
|
220
|
+
errors.append(
|
|
221
|
+
ConsistencyError(
|
|
222
|
+
message=f"View '{view}' not found in the provided CDF snapshot. Cannot create data model."
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if errors:
|
|
227
|
+
raise DataModelCreateException(errors)
|
|
228
|
+
|
|
229
|
+
# now we iterate over the views, expand them and add them to the data model
|
|
230
|
+
expanded_views: list[ViewRequest] = []
|
|
231
|
+
|
|
232
|
+
for view in view_refs:
|
|
233
|
+
expanded_view = self._resources.expand_view_properties(view)
|
|
234
|
+
if not expanded_view:
|
|
235
|
+
raise RuntimeError(f"Failed to expand view '{view}' despite earlier checks. This is a bug.")
|
|
236
|
+
|
|
237
|
+
# make a deep copy to avoid modifying the cached view in resources
|
|
238
|
+
expanded_view = expanded_view.model_copy(deep=True)
|
|
239
|
+
|
|
240
|
+
# remove implements after expansion
|
|
241
|
+
expanded_view.implements = None
|
|
242
|
+
|
|
243
|
+
# set space and version to data model values
|
|
244
|
+
expanded_view.space = self._space
|
|
245
|
+
expanded_view.version = self._version
|
|
246
|
+
|
|
247
|
+
# Connections should be dropped if their source are not part of the expanded view
|
|
248
|
+
properties_to_remove = []
|
|
249
|
+
for id, property_ in expanded_view.properties.items():
|
|
250
|
+
# Currently only supporting explicit connections where source is defined
|
|
251
|
+
if self._resources.is_explicit_connection(property_):
|
|
252
|
+
source = property_.source
|
|
253
|
+
if source not in view_refs:
|
|
254
|
+
properties_to_remove.append(id)
|
|
255
|
+
|
|
256
|
+
# Update source to point to view of solution data model
|
|
257
|
+
else:
|
|
258
|
+
property_.source = ViewReference(
|
|
259
|
+
space=self._space, external_id=source.external_id, version=self._version
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# Dropping connections which sources will not be part of the data model
|
|
263
|
+
for property_id in properties_to_remove:
|
|
264
|
+
expanded_view.properties.pop(property_id)
|
|
265
|
+
|
|
266
|
+
expanded_views.append(expanded_view)
|
|
267
|
+
|
|
268
|
+
data_model = DataModelRequest(
|
|
269
|
+
space=self._space,
|
|
270
|
+
externalId=self._external_id,
|
|
271
|
+
version=self._version,
|
|
272
|
+
name=self._name,
|
|
273
|
+
description=self._description,
|
|
274
|
+
views=[view.as_reference() for view in expanded_views],
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
return RequestSchema(dataModel=data_model, views=expanded_views)
|
|
278
|
+
|
|
279
|
+
@classmethod
|
|
280
|
+
def _parse_view_references(
|
|
281
|
+
cls, views: list[str]
|
|
282
|
+
) -> tuple[list[ModelSyntaxError | ConsistencyError], list[ViewReference]]:
|
|
283
|
+
"""Parses list of view strings in to list of view references
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
views : list of view references in a short string format
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Tuple of errors and view references
|
|
290
|
+
"""
|
|
291
|
+
view_references: list[ViewReference] = []
|
|
292
|
+
errors: list[ModelSyntaxError | ConsistencyError] = []
|
|
293
|
+
|
|
294
|
+
for view in views:
|
|
295
|
+
try:
|
|
296
|
+
entity = parse_entities(view)
|
|
297
|
+
if not entity:
|
|
298
|
+
errors.append(ModelSyntaxError(message=f"Invalid view reference '{view}': Could not parse entity."))
|
|
299
|
+
continue
|
|
300
|
+
|
|
301
|
+
if len(entity) != 1:
|
|
302
|
+
errors.append(
|
|
303
|
+
ModelSyntaxError(message=f"Invalid view reference '{view}': Expected a single view definition.")
|
|
304
|
+
)
|
|
305
|
+
continue
|
|
306
|
+
|
|
307
|
+
if "version" not in entity[0].properties:
|
|
308
|
+
errors.append(
|
|
309
|
+
ModelSyntaxError(message=f"Invalid view reference '{view}': Missing 'version' property.")
|
|
310
|
+
)
|
|
311
|
+
continue
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
view_ref = ViewReference(
|
|
315
|
+
space=entity[0].prefix,
|
|
316
|
+
external_id=entity[0].suffix,
|
|
317
|
+
version=str(entity[0].properties["version"]),
|
|
318
|
+
)
|
|
319
|
+
view_references.append(view_ref)
|
|
320
|
+
except ValidationError as e:
|
|
321
|
+
humanized = [humanize_validation_error(error) for error in e.errors()]
|
|
322
|
+
errors.append(
|
|
323
|
+
ModelSyntaxError(
|
|
324
|
+
message=f"Invalid view reference '{view}', cannot parse it: " + ", ".join(humanized)
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
except ValueError as e:
|
|
329
|
+
errors.append(ModelSyntaxError(message=f"Invalid view reference '{view}', cannot parse it: {e}"))
|
|
330
|
+
|
|
331
|
+
return errors, view_references
|
|
@@ -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/_exceptions.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from abc import ABC
|
|
2
2
|
from typing import TYPE_CHECKING
|
|
3
3
|
|
|
4
|
+
from cognite.neat._issues import ConsistencyError
|
|
5
|
+
|
|
4
6
|
if TYPE_CHECKING:
|
|
5
7
|
from cognite.neat._issues import ModelSyntaxError
|
|
6
8
|
from cognite.neat._utils.http_client import HTTPMessage
|
|
@@ -23,6 +25,17 @@ class DataModelImportException(NeatException):
|
|
|
23
25
|
return f"Model import failed with {len(self.errors)} errors: " + "; ".join(map(str, self.errors))
|
|
24
26
|
|
|
25
27
|
|
|
28
|
+
class DataModelCreateException(NeatException):
|
|
29
|
+
"""Raised when there is an error creating a data model."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, errors: "list[ModelSyntaxError | ConsistencyError]") -> None:
|
|
32
|
+
super().__init__(errors)
|
|
33
|
+
self.errors = errors
|
|
34
|
+
|
|
35
|
+
def __str__(self) -> str:
|
|
36
|
+
return f"Model creation failed with {len(self.errors)} errors: " + "; ".join(map(str, self.errors))
|
|
37
|
+
|
|
38
|
+
|
|
26
39
|
class CDFAPIException(NeatException):
|
|
27
40
|
"""Raised when there is an error in an API call."""
|
|
28
41
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from types import MethodType
|
|
1
2
|
from typing import Any, Literal
|
|
2
3
|
|
|
3
4
|
from cognite.neat._client import NeatClient
|
|
@@ -13,7 +14,7 @@ from cognite.neat._data_model.exporters import (
|
|
|
13
14
|
DMSTableYamlExporter,
|
|
14
15
|
)
|
|
15
16
|
from cognite.neat._data_model.exporters._table_exporter.workbook import WorkbookOptions
|
|
16
|
-
from cognite.neat._data_model.importers import DMSAPIImporter, DMSImporter, DMSTableImporter
|
|
17
|
+
from cognite.neat._data_model.importers import DMSAPICreator, DMSAPIImporter, DMSImporter, DMSTableImporter
|
|
17
18
|
from cognite.neat._data_model.models.dms import DataModelReference
|
|
18
19
|
from cognite.neat._data_model.validation.dms import DmsDataModelValidation
|
|
19
20
|
from cognite.neat._exceptions import UserInputError
|
|
@@ -24,6 +25,7 @@ from cognite.neat._utils._reader import NeatReader
|
|
|
24
25
|
from ._wrappers import session_wrapper
|
|
25
26
|
|
|
26
27
|
|
|
28
|
+
@session_wrapper
|
|
27
29
|
class PhysicalDataModel:
|
|
28
30
|
"""Read from a data source into NeatSession graph store."""
|
|
29
31
|
|
|
@@ -34,6 +36,10 @@ class PhysicalDataModel:
|
|
|
34
36
|
self.read = ReadPhysicalDataModel(self._store, self._client, self._config)
|
|
35
37
|
self.write = WritePhysicalDataModel(self._store, self._client, self._config)
|
|
36
38
|
|
|
39
|
+
# attach alpha methods
|
|
40
|
+
if self._config.alpha.enable_solution_model_creation:
|
|
41
|
+
self.create = MethodType(create, self) # type: ignore[attr-defined]
|
|
42
|
+
|
|
37
43
|
def _repr_html_(self) -> str:
|
|
38
44
|
if not isinstance(self._store.state, PhysicalState):
|
|
39
45
|
return "No physical data model. Get started by reading physical data model <em>.physica_data_mode.read</em>"
|
|
@@ -292,3 +298,51 @@ class WritePhysicalDataModel:
|
|
|
292
298
|
)
|
|
293
299
|
on_success = SchemaDeployer(self._client, options)
|
|
294
300
|
return self._store.write_physical(writer, on_success)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def create(
|
|
304
|
+
self: PhysicalDataModel,
|
|
305
|
+
space: str,
|
|
306
|
+
external_id: str,
|
|
307
|
+
version: str,
|
|
308
|
+
views: list[str],
|
|
309
|
+
name: str | None = None,
|
|
310
|
+
description: str | None = None,
|
|
311
|
+
kind: Literal["solution"] = "solution",
|
|
312
|
+
) -> None:
|
|
313
|
+
"""Create a solution data model in Neat from CDF views.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
space (str): The schema space of the data model.
|
|
317
|
+
external_id (str): The external id of the data model.
|
|
318
|
+
version (str): The version of the data model.
|
|
319
|
+
views (list[str]): List of view external ids to include in the data model in the short string format
|
|
320
|
+
space:external_id(version=version)
|
|
321
|
+
name (str | None): The name of the data model. If None, the name will be fetched from CDF.
|
|
322
|
+
description (str | None): The description of the data model. If None, the description will be fetched from CDF.
|
|
323
|
+
kind (Literal["solution"]): The kind of the data model. Currently, only "solution" is supported.
|
|
324
|
+
"""
|
|
325
|
+
|
|
326
|
+
if not self._store.cdf_snapshot.data_model:
|
|
327
|
+
raise ValueError("There are no data models in CDF. Cannot create solution model.")
|
|
328
|
+
|
|
329
|
+
creator = DMSAPICreator(
|
|
330
|
+
space=space,
|
|
331
|
+
external_id=external_id,
|
|
332
|
+
version=version,
|
|
333
|
+
views=views,
|
|
334
|
+
name=name,
|
|
335
|
+
description=description,
|
|
336
|
+
kind=kind,
|
|
337
|
+
cdf_snapshot=self._store.cdf_snapshot,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
on_success = DmsDataModelValidation(
|
|
341
|
+
modus_operandi=self._config.modeling.mode,
|
|
342
|
+
cdf_snapshot=self._store.cdf_snapshot,
|
|
343
|
+
limits=self._store.cdf_limits,
|
|
344
|
+
can_run_validator=self._config.validation.can_run_validator,
|
|
345
|
+
enable_alpha_validators=self._config.alpha.enable_experimental_validators,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
return self._store.read_physical(creator, on_success)
|
|
@@ -96,6 +96,24 @@ def session_wrapper(cls: type[T_Class]) -> type[T_Class]:
|
|
|
96
96
|
if callable(attr):
|
|
97
97
|
# Replace the original method with wrapped version
|
|
98
98
|
setattr(cls, attr_name, _handle_method_call(attr))
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
# Intercept __init__ to wrap any methods added via setattr
|
|
100
|
+
original_init = cls.__init__
|
|
101
|
+
|
|
102
|
+
@wraps(original_init)
|
|
103
|
+
def pick_alpha_methods(self: HasStore, *args: Any, **kwargs: Any) -> Any:
|
|
104
|
+
"""This method wraps any instance methods added during __init__. which is the case for alpha methods"""
|
|
105
|
+
original_init(self, *args, **kwargs)
|
|
106
|
+
# Wrap any instance methods added during init
|
|
107
|
+
for attr_name in dir(self):
|
|
108
|
+
if not attr_name.startswith("_"):
|
|
109
|
+
attr = getattr(self, attr_name, None)
|
|
110
|
+
# Check if it's an instance method (not from the class)
|
|
111
|
+
if callable(attr) and attr_name not in vars(self.__class__):
|
|
112
|
+
# Wrap and set on the instance
|
|
113
|
+
wrapped = (
|
|
114
|
+
_handle_method_call(attr.__func__) if hasattr(attr, "__func__") else _handle_method_call(attr)
|
|
115
|
+
)
|
|
116
|
+
setattr(self, attr_name, wrapped.__get__(self, type(self)))
|
|
117
|
+
|
|
118
|
+
cls.__init__ = pick_alpha_methods # type: ignore[assignment]
|
|
101
119
|
return cls
|
cognite/neat/_store/_store.py
CHANGED
|
@@ -15,7 +15,7 @@ from cognite.neat._data_model.exporters._table_exporter.exporter import DMSTable
|
|
|
15
15
|
from cognite.neat._data_model.importers import DMSImporter, DMSTableImporter
|
|
16
16
|
from cognite.neat._data_model.models.dms import RequestSchema as PhysicalDataModel
|
|
17
17
|
from cognite.neat._data_model.models.dms._limits import SchemaLimits
|
|
18
|
-
from cognite.neat._exceptions import DataModelImportException
|
|
18
|
+
from cognite.neat._exceptions import DataModelCreateException, DataModelImportException
|
|
19
19
|
from cognite.neat._issues import IssueList
|
|
20
20
|
from cognite.neat._state_machine._states import EmptyState, PhysicalState, State
|
|
21
21
|
from cognite.neat._utils.text import NEWLINE
|
|
@@ -179,6 +179,9 @@ class NeatStore:
|
|
|
179
179
|
except DataModelImportException as e:
|
|
180
180
|
errors.extend(e.errors)
|
|
181
181
|
|
|
182
|
+
except DataModelCreateException as e:
|
|
183
|
+
errors.extend(e.errors)
|
|
184
|
+
|
|
182
185
|
# these are all other errors, such as missing file, wrong format, etc.
|
|
183
186
|
except Exception as e:
|
|
184
187
|
raise e
|
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>
|
|
@@ -15,10 +15,10 @@ cognite/neat/_client/init/main.py,sha256=D-9bsor07T84iFnTZLnhIFA1Xey9zsooHaDhXVe
|
|
|
15
15
|
cognite/neat/_client/spaces_api.py,sha256=yb26ju3sKbVgiduMEbN4LDGan6vsC3z_JaW3p4FUdr0,2929
|
|
16
16
|
cognite/neat/_client/statistics_api.py,sha256=5meeh0v5mxC2SMB7xGdOwMh4KkaX3DtpUjbGEPhNBJo,913
|
|
17
17
|
cognite/neat/_client/views_api.py,sha256=YMaw7IaxU4gmixpd_t1u9JK9BHfNerf5DMNinGPCAa0,3692
|
|
18
|
-
cognite/neat/_config.py,sha256=
|
|
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
|
|
@@ -37,8 +37,8 @@ cognite/neat/_data_model/exporters/_table_exporter/__init__.py,sha256=47DEQpj8HB
|
|
|
37
37
|
cognite/neat/_data_model/exporters/_table_exporter/exporter.py,sha256=4BPu_Chtjh1EyOaKbThXYohsqllVOkCbSoNekNZuBXc,5159
|
|
38
38
|
cognite/neat/_data_model/exporters/_table_exporter/workbook.py,sha256=1Afk1WqeNe9tiNeSAm0HrF8jTQ1kTbIv1D9hMztKwO8,18482
|
|
39
39
|
cognite/neat/_data_model/exporters/_table_exporter/writer.py,sha256=k1gPrI8OdS75x3ncLn27Z-wnWgkOJ_eiCFnfp-rgdyY,21748
|
|
40
|
-
cognite/neat/_data_model/importers/__init__.py,sha256=
|
|
41
|
-
cognite/neat/_data_model/importers/_api_importer.py,sha256=
|
|
40
|
+
cognite/neat/_data_model/importers/__init__.py,sha256=35MasidLXvQyi7aL7eyNbY0hRGZEGQOHjCSw3lHkBdg,225
|
|
41
|
+
cognite/neat/_data_model/importers/_api_importer.py,sha256=jkSw74kwKCFC1RvEr0DOn34dhTlcqbiBTcWT5vBuXYk,13838
|
|
42
42
|
cognite/neat/_data_model/importers/_base.py,sha256=NRB0FcEBj4GaethU68nRffBfTedBBA866A3zfJNfmiQ,433
|
|
43
43
|
cognite/neat/_data_model/importers/_table_importer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
44
44
|
cognite/neat/_data_model/importers/_table_importer/data_classes.py,sha256=_Hfub4GZNxiq01JcxijjeNFaecJXQmmMNoFe7apo0iM,11466
|
|
@@ -76,16 +76,17 @@ 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
|
-
cognite/neat/_exceptions.py,sha256=
|
|
89
|
+
cognite/neat/_exceptions.py,sha256=hOjPL1vFNNAZzqAHFB9l9ek-XJEBKcqiaPk0onwLPns,2540
|
|
89
90
|
cognite/neat/_issues.py,sha256=wH1mnkrpBsHUkQMGUHFLUIQWQlfJ_qMfdF7q0d9wNhY,1871
|
|
90
91
|
cognite/neat/_session/__init__.py,sha256=owqW5Mml2DSZx1AvPvwNRTBngfhBNrQ6EH-7CKL7Jp0,61
|
|
91
92
|
cognite/neat/_session/_cdf.py,sha256=Ps49pc2tKriImpDkcIABnCgrfoX-L3QGmnXK8ceJ1Lc,1365
|
|
@@ -104,7 +105,7 @@ cognite/neat/_session/_html/templates/deployment.html,sha256=aLDXMbF3pcSqnCpUYVG
|
|
|
104
105
|
cognite/neat/_session/_html/templates/issues.html,sha256=zjhkJcPK0hMp_ZKJ9RCf88tuZxQyTYRPxzpqx33Nkt0,1661
|
|
105
106
|
cognite/neat/_session/_html/templates/statistics.html,sha256=C-ghoT8xzBehAJLOonNzCB418LW-FA60MaZBHlW1D5c,968
|
|
106
107
|
cognite/neat/_session/_issues.py,sha256=E8UQeSJURg2dm4MF1pfD9dp-heSRT7pgQZgKlD1-FGs,2723
|
|
107
|
-
cognite/neat/_session/_physical.py,sha256=
|
|
108
|
+
cognite/neat/_session/_physical.py,sha256=uzgyNfchv5axhDMjdGB6hnpbvT_QfXKmI6zhwbsFW-o,14448
|
|
108
109
|
cognite/neat/_session/_result/__init__.py,sha256=8A0BKgsqnjxkiHUlCpHBNl3mrFWtyjaWYnh0jssE6QU,50
|
|
109
110
|
cognite/neat/_session/_result/_deployment/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
110
111
|
cognite/neat/_session/_result/_deployment/_physical/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -117,13 +118,13 @@ cognite/neat/_session/_usage_analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JC
|
|
|
117
118
|
cognite/neat/_session/_usage_analytics/_collector.py,sha256=8yVfzt8KFfZ-ldkVjDWazuQbs45Q3r6vWKcZEwU8i18,4734
|
|
118
119
|
cognite/neat/_session/_usage_analytics/_constants.py,sha256=-tVdYrCTMKfuMlbO7AlzC29Nug41ug6uuX9DFuihpJg,561
|
|
119
120
|
cognite/neat/_session/_usage_analytics/_storage.py,sha256=w3mUvmPysww6vM3PZBjg6jzNEsDISl7FJ1j19LNs26E,7779
|
|
120
|
-
cognite/neat/_session/_wrappers.py,sha256=
|
|
121
|
+
cognite/neat/_session/_wrappers.py,sha256=3GADffOVyGryXknnN88hKhwTHvtZfeFBYpCROuXyGXY,5447
|
|
121
122
|
cognite/neat/_state_machine/__init__.py,sha256=wrtQUHETiLzYM0pFo7JC6pJCiXetHADQbyMu8pU8rQU,195
|
|
122
123
|
cognite/neat/_state_machine/_base.py,sha256=-ZpeAhM6l6N6W70dET25tAzOxaaK5aa474eabwZVzjA,1112
|
|
123
124
|
cognite/neat/_state_machine/_states.py,sha256=nmj4SmunpDYcBsNx8A284xnXGS43wuUuWpMMORha2DE,1170
|
|
124
125
|
cognite/neat/_store/__init__.py,sha256=TvM9CcFbtOSrxydPAuJi6Bv_iiGard1Mxfx42ZFoTl0,55
|
|
125
126
|
cognite/neat/_store/_provenance.py,sha256=1zzRDWjR9twZu2jVyIG3UdYdIXtQKJ7uF8a0hV7LEuA,3368
|
|
126
|
-
cognite/neat/_store/_store.py,sha256=
|
|
127
|
+
cognite/neat/_store/_store.py,sha256=3mfw5zFL2nLad57gHbriDnwd5WMy4qoUKKinK5k3xwA,9738
|
|
127
128
|
cognite/neat/_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
128
129
|
cognite/neat/_utils/_reader.py,sha256=pBAwGK_UMd-mGnAP3cXXj3SrORocR3lFKxn-WaVjqsY,5367
|
|
129
130
|
cognite/neat/_utils/auxiliary.py,sha256=YQMpqCxccex_slmLYrR5icVX9aeLbD793ou7IrbNTFs,1654
|
|
@@ -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
|