implica 0.3.0__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.

Potentially problematic release.


This version of implica might be problematic. Click here for more details.

@@ -0,0 +1,51 @@
1
+ """
2
+ Mutation system for transactional graph modifications.
3
+
4
+ This module defines mutations that can be applied to a graph with forward
5
+ and backward operations, enabling transactional rollback on failures.
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+ from typing import TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ from ..graph.graph import Graph
13
+
14
+
15
+ class Mutation(ABC):
16
+ """
17
+ Abstract base class for graph mutations.
18
+
19
+ Each mutation must implement both forward (apply) and backward (revert)
20
+ operations to support transactional rollback.
21
+ """
22
+
23
+ @abstractmethod
24
+ def forward(self, graph: "Graph") -> None:
25
+ """
26
+ Apply the mutation to the graph.
27
+
28
+ Args:
29
+ graph: The graph to modify
30
+
31
+ Raises:
32
+ Exception: If the mutation cannot be applied
33
+ """
34
+ pass
35
+
36
+ @abstractmethod
37
+ def backward(self, graph: "Graph") -> None:
38
+ """
39
+ Revert the mutation from the graph.
40
+
41
+ This operation should undo what forward() did.
42
+
43
+ Args:
44
+ graph: The graph to modify
45
+ """
46
+ pass
47
+
48
+ @abstractmethod
49
+ def __str__(self) -> str:
50
+ """Return a string representation of the mutation."""
51
+ pass
@@ -0,0 +1,54 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from ..graph.elements import Edge
4
+ from .base import Mutation
5
+
6
+ if TYPE_CHECKING:
7
+ from ..graph.graph import Graph
8
+
9
+
10
+ class RemoveEdge(Mutation):
11
+ """Mutation to remove an edge from the graph."""
12
+
13
+ def __init__(self, edge_uid: str):
14
+ """
15
+ Initialize the RemoveEdge mutation.
16
+
17
+ Args:
18
+ edge_uid: The uid of the edge to remove
19
+ """
20
+ self.edge_uid = edge_uid
21
+ self._removed_edge: Edge | None = None
22
+
23
+ def forward(self, graph: "Graph") -> None:
24
+ """
25
+ Remove the edge from the graph.
26
+
27
+ Args:
28
+ graph: The graph to modify
29
+
30
+ Raises:
31
+ KeyError: If the edge doesn't exist
32
+ """
33
+ # Store the edge for rollback
34
+ self._removed_edge = graph.get_edge(self.edge_uid)
35
+
36
+ # Remove the edge
37
+ graph._remove_edge(self.edge_uid)
38
+
39
+ def backward(self, graph: "Graph") -> None:
40
+ """
41
+ Re-add the edge to the graph.
42
+
43
+ Args:
44
+ graph: The graph to modify
45
+ """
46
+ if self._removed_edge is None:
47
+ raise RuntimeError("Cannot revert RemoveEdge: edge was not stored")
48
+
49
+ # Add the edge back
50
+ graph._add_edge(self._removed_edge)
51
+
52
+ def __str__(self) -> str:
53
+ """Return a string representation."""
54
+ return f"RemoveEdge({self.edge_uid})"
@@ -0,0 +1,64 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from ..graph.elements import Edge
4
+ from .base import Mutation
5
+
6
+ if TYPE_CHECKING:
7
+ from ..graph.graph import Graph
8
+
9
+
10
+ class RemoveManyEdges(Mutation):
11
+ """Mutation to remove multiple edges from the graph."""
12
+
13
+ def __init__(self, edge_uids: list[str]):
14
+ """
15
+ Initialize the RemoveManyEdges mutation.
16
+
17
+ Args:
18
+ edge_uids: List of edge uids to remove
19
+ """
20
+ self.edge_uids = edge_uids
21
+ self._removed_edges: list[Edge] = []
22
+
23
+ def forward(self, graph: "Graph") -> None:
24
+ """
25
+ Remove all edges from the graph.
26
+
27
+ If any edge fails to be removed, all previously removed edges are restored.
28
+
29
+ Args:
30
+ graph: The graph to modify
31
+
32
+ Raises:
33
+ KeyError: If any edge doesn't exist
34
+ """
35
+ self._removed_edges = []
36
+
37
+ try:
38
+ for edge_uid in self.edge_uids:
39
+ # Store the edge for rollback
40
+ edge = graph.get_edge(edge_uid)
41
+ self._removed_edges.append(edge)
42
+
43
+ # Remove the edge
44
+ graph._remove_edge(edge_uid)
45
+ except Exception:
46
+ # Rollback: restore all edges that were removed
47
+ for edge in self._removed_edges:
48
+ graph._add_edge(edge)
49
+ self._removed_edges = []
50
+ raise
51
+
52
+ def backward(self, graph: "Graph") -> None:
53
+ """
54
+ Re-add all removed edges to the graph.
55
+
56
+ Args:
57
+ graph: The graph to modify
58
+ """
59
+ for edge in reversed(self._removed_edges):
60
+ graph._add_edge(edge)
61
+
62
+ def __str__(self) -> str:
63
+ """Return a string representation."""
64
+ return f"RemoveManyEdges(count={len(self.edge_uids)})"
@@ -0,0 +1,69 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from ..graph.elements import Node, Edge
4
+ from .base import Mutation
5
+
6
+ if TYPE_CHECKING:
7
+ from ..graph.graph import Graph
8
+
9
+
10
+ class RemoveManyNodes(Mutation):
11
+ """Mutation to remove multiple nodes from the graph."""
12
+
13
+ def __init__(self, node_uids: list[str]):
14
+ """
15
+ Initialize the RemoveManyNodes mutation.
16
+
17
+ Args:
18
+ node_uids: List of node uids to remove
19
+ """
20
+ self.node_uids = node_uids
21
+ self._removed_data: list[tuple[Node, list[Edge]]] = []
22
+
23
+ def forward(self, graph: "Graph") -> None:
24
+ """
25
+ Remove all nodes from the graph.
26
+
27
+ If any node fails to be removed, all previously removed nodes are restored.
28
+
29
+ Args:
30
+ graph: The graph to modify
31
+
32
+ Raises:
33
+ KeyError: If any node doesn't exist
34
+ """
35
+ self._removed_data = []
36
+
37
+ try:
38
+ for node_uid in self.node_uids:
39
+ # Store the node and its edges for rollback
40
+ node = graph.get_node(node_uid)
41
+ edges = graph.get_edges_for_node(node_uid)
42
+ self._removed_data.append((node, edges))
43
+
44
+ # Remove the node
45
+ graph._remove_node(node_uid)
46
+ except Exception:
47
+ # Rollback: restore all nodes that were removed
48
+ for node, edges in self._removed_data:
49
+ graph._add_node(node)
50
+ for edge in edges:
51
+ graph._add_edge(edge)
52
+ self._removed_data = []
53
+ raise
54
+
55
+ def backward(self, graph: "Graph") -> None:
56
+ """
57
+ Re-add all removed nodes and their edges to the graph.
58
+
59
+ Args:
60
+ graph: The graph to modify
61
+ """
62
+ for node, edges in reversed(self._removed_data):
63
+ graph._add_node(node)
64
+ for edge in edges:
65
+ graph._add_edge(edge)
66
+
67
+ def __str__(self) -> str:
68
+ """Return a string representation."""
69
+ return f"RemoveManyNodes(count={len(self.node_uids)})"
@@ -0,0 +1,62 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from ..graph import Edge, Node
4
+ from .base import Mutation
5
+
6
+ if TYPE_CHECKING:
7
+ from ..graph import Graph
8
+
9
+
10
+ class RemoveNode(Mutation):
11
+ """Mutation to remove a node from the graph."""
12
+
13
+ def __init__(self, node_uid: str):
14
+ """
15
+ Initialize the RemoveNode mutation.
16
+
17
+ Args:
18
+ node_uid: The uid of the node to remove
19
+ """
20
+ self.node_uid = node_uid
21
+ self._removed_node: Node | None = None
22
+ self._removed_edges: list[Edge] = []
23
+
24
+ def forward(self, graph: "Graph") -> None:
25
+ """
26
+ Remove the node from the graph.
27
+
28
+ Also stores the node and its connected edges for potential rollback.
29
+
30
+ Args:
31
+ graph: The graph to modify
32
+
33
+ Raises:
34
+ KeyError: If the node doesn't exist
35
+ """
36
+ # Store the node and its edges for rollback
37
+ self._removed_node = graph.get_node(self.node_uid)
38
+ self._removed_edges = graph.get_edges_for_node(self.node_uid)
39
+
40
+ # Remove the node (this will also remove connected edges)
41
+ graph._remove_node(self.node_uid)
42
+
43
+ def backward(self, graph: "Graph") -> None:
44
+ """
45
+ Re-add the node and its edges to the graph.
46
+
47
+ Args:
48
+ graph: The graph to modify
49
+ """
50
+ if self._removed_node is None:
51
+ raise RuntimeError("Cannot revert RemoveNode: node was not stored")
52
+
53
+ # Add the node back
54
+ graph._add_node(self._removed_node)
55
+
56
+ # Add the edges back
57
+ for edge in self._removed_edges:
58
+ graph._add_edge(edge)
59
+
60
+ def __str__(self) -> str:
61
+ """Return a string representation."""
62
+ return f"RemoveNode({self.node_uid})"
@@ -0,0 +1,53 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from ..graph.elements import Edge
4
+ from .base import Mutation
5
+
6
+ if TYPE_CHECKING:
7
+ from ..graph.graph import Graph
8
+
9
+
10
+ class TryAddEdge(Mutation):
11
+ """Mutation to add an edge to the graph, or do nothing if it already exists."""
12
+
13
+ def __init__(self, edge: Edge):
14
+ """
15
+ Initialize the TryAddEdge mutation.
16
+
17
+ Args:
18
+ edge: The edge to add
19
+ """
20
+ self.edge = edge
21
+ self._was_added: bool = False
22
+
23
+ def forward(self, graph: "Graph") -> None:
24
+ """
25
+ Try to add the edge to the graph.
26
+
27
+ If the edge already exists, this is a no-op.
28
+
29
+ Args:
30
+ graph: The graph to modify
31
+
32
+ Raises:
33
+ KeyError: If source or destination nodes don't exist
34
+ """
35
+ if graph.has_edge(self.edge.uid):
36
+ self._was_added = False
37
+ else:
38
+ graph._add_edge(self.edge)
39
+ self._was_added = True
40
+
41
+ def backward(self, graph: "Graph") -> None:
42
+ """
43
+ Remove the edge from the graph if it was added.
44
+
45
+ Args:
46
+ graph: The graph to modify
47
+ """
48
+ if self._was_added:
49
+ graph._remove_edge(self.edge.uid)
50
+
51
+ def __str__(self) -> str:
52
+ """Return a string representation."""
53
+ return f"TryAddEdge({self.edge})"
@@ -0,0 +1,50 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from ..graph import Edge, Node
4
+ from .base import Mutation
5
+
6
+ if TYPE_CHECKING:
7
+ from ..graph import Graph
8
+
9
+
10
+ class TryAddNode(Mutation):
11
+ """Mutation to add a node to the graph, or do nothing if it already exists."""
12
+
13
+ def __init__(self, node: Node):
14
+ """
15
+ Initialize the TryAddNode mutation.
16
+
17
+ Args:
18
+ node: The node to add
19
+ """
20
+ self.node = node
21
+ self._was_added: bool = False
22
+
23
+ def forward(self, graph: "Graph") -> None:
24
+ """
25
+ Try to add the node to the graph.
26
+
27
+ If the node already exists, this is a no-op.
28
+
29
+ Args:
30
+ graph: The graph to modify
31
+ """
32
+ if graph.has_node(self.node.uid):
33
+ self._was_added = False
34
+ else:
35
+ graph._add_node(self.node)
36
+ self._was_added = True
37
+
38
+ def backward(self, graph: "Graph") -> None:
39
+ """
40
+ Remove the node from the graph if it was added.
41
+
42
+ Args:
43
+ graph: The graph to modify
44
+ """
45
+ if self._was_added:
46
+ graph._remove_node(self.node.uid)
47
+
48
+ def __str__(self) -> str:
49
+ """Return a string representation."""
50
+ return f"TryAddNode({self.node})"
@@ -0,0 +1,58 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from ..graph.elements import Edge
4
+ from .base import Mutation
5
+
6
+ if TYPE_CHECKING:
7
+ from ..graph.graph import Graph
8
+
9
+
10
+ class TryRemoveEdge(Mutation):
11
+ """Mutation to remove an edge from the graph, or do nothing if it doesn't exist."""
12
+
13
+ def __init__(self, edge_uid: str):
14
+ """
15
+ Initialize the TryRemoveEdge mutation.
16
+
17
+ Args:
18
+ edge_uid: The uid of the edge to remove
19
+ """
20
+ self.edge_uid = edge_uid
21
+ self._was_removed: bool = False
22
+ self._removed_edge: Edge | None = None
23
+
24
+ def forward(self, graph: "Graph") -> None:
25
+ """
26
+ Try to remove the edge from the graph.
27
+
28
+ If the edge doesn't exist, this is a no-op.
29
+
30
+ Args:
31
+ graph: The graph to modify
32
+ """
33
+ if not graph.has_edge(self.edge_uid):
34
+ self._was_removed = False
35
+ else:
36
+ # Store the edge for rollback
37
+ self._removed_edge = graph.get_edge(self.edge_uid)
38
+
39
+ # Remove the edge
40
+ graph._remove_edge(self.edge_uid)
41
+ self._was_removed = True
42
+
43
+ def backward(self, graph: "Graph") -> None:
44
+ """
45
+ Re-add the edge if it was removed.
46
+
47
+ Args:
48
+ graph: The graph to modify
49
+ """
50
+ if self._was_removed:
51
+ if self._removed_edge is None:
52
+ raise RuntimeError("Cannot revert TryRemoveEdge: edge was not stored")
53
+
54
+ graph._add_edge(self._removed_edge)
55
+
56
+ def __str__(self) -> str:
57
+ """Return a string representation."""
58
+ return f"TryRemoveEdge({self.edge_uid})"
@@ -0,0 +1,62 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from ..graph import Edge, Node
4
+ from .base import Mutation
5
+
6
+ if TYPE_CHECKING:
7
+ from ..graph import Graph
8
+
9
+
10
+ class TryRemoveNode(Mutation):
11
+ """Mutation to remove a node from the graph, or do nothing if it doesn't exist."""
12
+
13
+ def __init__(self, node_uid: str):
14
+ """
15
+ Initialize the TryRemoveNode mutation.
16
+
17
+ Args:
18
+ node_uid: The uid of the node to remove
19
+ """
20
+ self.node_uid = node_uid
21
+ self._was_removed: bool = False
22
+ self._removed_node: Node | None = None
23
+ self._removed_edges: list[Edge] = []
24
+
25
+ def forward(self, graph: "Graph") -> None:
26
+ """
27
+ Try to remove the node from the graph.
28
+
29
+ If the node doesn't exist, this is a no-op.
30
+
31
+ Args:
32
+ graph: The graph to modify
33
+ """
34
+ if not graph.has_node(self.node_uid):
35
+ self._was_removed = False
36
+ else:
37
+ # Store the node and its edges for rollback
38
+ self._removed_node = graph.get_node(self.node_uid)
39
+ self._removed_edges = graph.get_edges_for_node(self.node_uid)
40
+
41
+ # Remove the node
42
+ graph._remove_node(self.node_uid)
43
+ self._was_removed = True
44
+
45
+ def backward(self, graph: "Graph") -> None:
46
+ """
47
+ Re-add the node and its edges if it was removed.
48
+
49
+ Args:
50
+ graph: The graph to modify
51
+ """
52
+ if self._was_removed:
53
+ if self._removed_node is None:
54
+ raise RuntimeError("Cannot revert TryRemoveNode: node was not stored")
55
+
56
+ graph._add_node(self._removed_node)
57
+ for edge in self._removed_edges:
58
+ graph._add_edge(edge)
59
+
60
+ def __str__(self) -> str:
61
+ """Return a string representation."""
62
+ return f"TryRemoveNode({self.node_uid})"
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Carlos Fernandez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.