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.
- implica/__init__.py +44 -0
- implica/core/__init__.py +4 -0
- implica/core/combinator.py +149 -0
- implica/core/types.py +140 -0
- implica/graph/__init__.py +5 -0
- implica/graph/connection.py +389 -0
- implica/graph/elements.py +194 -0
- implica/graph/graph.py +443 -0
- implica/mutations/__init__.py +29 -0
- implica/mutations/add_edge.py +45 -0
- implica/mutations/add_many_edges.py +60 -0
- implica/mutations/add_many_nodes.py +60 -0
- implica/mutations/add_node.py +45 -0
- implica/mutations/base.py +51 -0
- implica/mutations/remove_edge.py +54 -0
- implica/mutations/remove_many_edges.py +64 -0
- implica/mutations/remove_many_nodes.py +69 -0
- implica/mutations/remove_node.py +62 -0
- implica/mutations/try_add_edge.py +53 -0
- implica/mutations/try_add_node.py +50 -0
- implica/mutations/try_remove_edge.py +58 -0
- implica/mutations/try_remove_node.py +62 -0
- implica-0.3.0.dist-info/LICENSE +21 -0
- implica-0.3.0.dist-info/METADATA +945 -0
- implica-0.3.0.dist-info/RECORD +26 -0
- implica-0.3.0.dist-info/WHEEL +4 -0
implica/graph/graph.py
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Graph structure for the implicational logic system.
|
|
3
|
+
|
|
4
|
+
This module defines the Graph class which manages nodes and edges with
|
|
5
|
+
optimized data structures for large graphs.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Iterator, Optional
|
|
9
|
+
|
|
10
|
+
from .connection import Connection
|
|
11
|
+
from .elements import Node, Edge
|
|
12
|
+
from ..core.types import BaseType
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Graph:
|
|
16
|
+
"""
|
|
17
|
+
A graph representing the implicational logic model.
|
|
18
|
+
|
|
19
|
+
The graph uses optimized data structures:
|
|
20
|
+
- Dict for O(1) node and edge lookups by uid
|
|
21
|
+
- Sets for O(1) membership checks
|
|
22
|
+
- Adjacency lists for efficient edge traversal
|
|
23
|
+
|
|
24
|
+
The graph maintains type consistency through validation.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self):
|
|
28
|
+
"""Initialize an empty graph."""
|
|
29
|
+
# Core storage: uid -> object
|
|
30
|
+
self._nodes: dict[str, Node] = {}
|
|
31
|
+
self._edges: dict[str, Edge] = {}
|
|
32
|
+
|
|
33
|
+
# Adjacency lists for efficient traversal
|
|
34
|
+
# node_uid -> set of outgoing edge uids
|
|
35
|
+
self._outgoing_edges: dict[str, set[str]] = {}
|
|
36
|
+
# node_uid -> set of incoming edge uids
|
|
37
|
+
self._incoming_edges: dict[str, set[str]] = {}
|
|
38
|
+
|
|
39
|
+
# ========== Low-level API (internal use) ==========
|
|
40
|
+
|
|
41
|
+
def _add_node(self, node: Node) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Add a node to the graph (low-level API).
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
node: The node to add
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
ValueError: If a node with the same uid already exists
|
|
50
|
+
"""
|
|
51
|
+
uid = node.uid
|
|
52
|
+
|
|
53
|
+
if uid in self._nodes:
|
|
54
|
+
raise ValueError(f"Node with uid '{uid}' already exists in the graph")
|
|
55
|
+
|
|
56
|
+
self._nodes[uid] = node
|
|
57
|
+
self._outgoing_edges[uid] = set()
|
|
58
|
+
self._incoming_edges[uid] = set()
|
|
59
|
+
|
|
60
|
+
def _remove_node(self, node_uid: str) -> None:
|
|
61
|
+
"""
|
|
62
|
+
Remove a node from the graph (low-level API).
|
|
63
|
+
|
|
64
|
+
This also removes all edges connected to the node.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
node_uid: The uid of the node to remove
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
KeyError: If the node doesn't exist
|
|
71
|
+
"""
|
|
72
|
+
if node_uid not in self._nodes:
|
|
73
|
+
raise KeyError(f"Node with uid '{node_uid}' not found in the graph")
|
|
74
|
+
|
|
75
|
+
# Remove all connected edges
|
|
76
|
+
outgoing = list(self._outgoing_edges[node_uid])
|
|
77
|
+
incoming = list(self._incoming_edges[node_uid])
|
|
78
|
+
|
|
79
|
+
for edge_uid in outgoing:
|
|
80
|
+
self._remove_edge(edge_uid)
|
|
81
|
+
|
|
82
|
+
for edge_uid in incoming:
|
|
83
|
+
self._remove_edge(edge_uid)
|
|
84
|
+
|
|
85
|
+
# Remove the node
|
|
86
|
+
del self._nodes[node_uid]
|
|
87
|
+
del self._outgoing_edges[node_uid]
|
|
88
|
+
del self._incoming_edges[node_uid]
|
|
89
|
+
|
|
90
|
+
def _add_edge(self, edge: Edge) -> None:
|
|
91
|
+
"""
|
|
92
|
+
Add an edge to the graph (low-level API).
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
edge: The edge to add
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
ValueError: If the edge already exists
|
|
99
|
+
KeyError: If source or destination nodes don't exist
|
|
100
|
+
"""
|
|
101
|
+
uid = edge.uid
|
|
102
|
+
src_uid = edge.src_node.uid
|
|
103
|
+
dst_uid = edge.dst_node.uid
|
|
104
|
+
|
|
105
|
+
if uid in self._edges:
|
|
106
|
+
raise ValueError(f"Edge with uid '{uid}' already exists in the graph")
|
|
107
|
+
|
|
108
|
+
if src_uid not in self._nodes:
|
|
109
|
+
raise KeyError(f"Source node with uid '{src_uid}' not found in the graph")
|
|
110
|
+
|
|
111
|
+
if dst_uid not in self._nodes:
|
|
112
|
+
raise KeyError(
|
|
113
|
+
f"Destination node with uid '{dst_uid}' not found in the graph"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Verify nodes match
|
|
117
|
+
if self._nodes[src_uid] != edge.src_node:
|
|
118
|
+
raise ValueError(f"Source node mismatch for uid '{src_uid}'")
|
|
119
|
+
|
|
120
|
+
if self._nodes[dst_uid] != edge.dst_node:
|
|
121
|
+
raise ValueError(f"Destination node mismatch for uid '{dst_uid}'")
|
|
122
|
+
|
|
123
|
+
self._edges[uid] = edge
|
|
124
|
+
self._outgoing_edges[src_uid].add(uid)
|
|
125
|
+
self._incoming_edges[dst_uid].add(uid)
|
|
126
|
+
|
|
127
|
+
def _remove_edge(self, edge_uid: str) -> None:
|
|
128
|
+
"""
|
|
129
|
+
Remove an edge from the graph (low-level API).
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
edge_uid: The uid of the edge to remove
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
KeyError: If the edge doesn't exist
|
|
136
|
+
"""
|
|
137
|
+
if edge_uid not in self._edges:
|
|
138
|
+
raise KeyError(f"Edge with uid '{edge_uid}' not found in the graph")
|
|
139
|
+
|
|
140
|
+
edge = self._edges[edge_uid]
|
|
141
|
+
src_uid = edge.src_node.uid
|
|
142
|
+
dst_uid = edge.dst_node.uid
|
|
143
|
+
|
|
144
|
+
del self._edges[edge_uid]
|
|
145
|
+
self._outgoing_edges[src_uid].discard(edge_uid)
|
|
146
|
+
self._incoming_edges[dst_uid].discard(edge_uid)
|
|
147
|
+
|
|
148
|
+
# ========== Public query API ==========
|
|
149
|
+
|
|
150
|
+
def has_node(self, node_uid: str) -> bool:
|
|
151
|
+
"""
|
|
152
|
+
Check if a node exists in the graph.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
node_uid: The uid of the node
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
bool: True if the node exists, False otherwise
|
|
159
|
+
"""
|
|
160
|
+
return node_uid in self._nodes
|
|
161
|
+
|
|
162
|
+
def get_node(self, node_uid: str) -> Node:
|
|
163
|
+
"""
|
|
164
|
+
Get a node by its uid.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
node_uid: The uid of the node
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Node: The node
|
|
171
|
+
|
|
172
|
+
Raises:
|
|
173
|
+
KeyError: If the node doesn't exist
|
|
174
|
+
"""
|
|
175
|
+
if node_uid not in self._nodes:
|
|
176
|
+
raise KeyError(f"Node with uid '{node_uid}' not found in the graph")
|
|
177
|
+
return self._nodes[node_uid]
|
|
178
|
+
|
|
179
|
+
def get_node_by_type(self, type: BaseType) -> Optional[Node]:
|
|
180
|
+
"""
|
|
181
|
+
Get a node by its type.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
type: The type to search for
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Optional[Node]: The node if found, None otherwise
|
|
188
|
+
"""
|
|
189
|
+
uid = type.uid
|
|
190
|
+
return self._nodes.get(uid)
|
|
191
|
+
|
|
192
|
+
def has_edge(self, edge_uid: str) -> bool:
|
|
193
|
+
"""
|
|
194
|
+
Check if an edge exists in the graph.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
edge_uid: The uid of the edge
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
bool: True if the edge exists, False otherwise
|
|
201
|
+
"""
|
|
202
|
+
return edge_uid in self._edges
|
|
203
|
+
|
|
204
|
+
def get_edge(self, edge_uid: str) -> Edge:
|
|
205
|
+
"""
|
|
206
|
+
Get an edge by its uid.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
edge_uid: The uid of the edge
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Edge: The edge
|
|
213
|
+
|
|
214
|
+
Raises:
|
|
215
|
+
KeyError: If the edge doesn't exist
|
|
216
|
+
"""
|
|
217
|
+
if edge_uid not in self._edges:
|
|
218
|
+
raise KeyError(f"Edge with uid '{edge_uid}' not found in the graph")
|
|
219
|
+
return self._edges[edge_uid]
|
|
220
|
+
|
|
221
|
+
def get_edges_for_node(self, node_uid: str) -> list[Edge]:
|
|
222
|
+
"""
|
|
223
|
+
Get all edges connected to a node (both incoming and outgoing).
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
node_uid: The uid of the node
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
list[Edge]: List of connected edges
|
|
230
|
+
"""
|
|
231
|
+
if node_uid not in self._nodes:
|
|
232
|
+
return []
|
|
233
|
+
|
|
234
|
+
edge_uids = self._outgoing_edges[node_uid] | self._incoming_edges[node_uid]
|
|
235
|
+
return [self._edges[uid] for uid in edge_uids]
|
|
236
|
+
|
|
237
|
+
def get_outgoing_edges(self, node_uid: str) -> list[Edge]:
|
|
238
|
+
"""
|
|
239
|
+
Get all outgoing edges from a node.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
node_uid: The uid of the node
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
list[Edge]: List of outgoing edges
|
|
246
|
+
"""
|
|
247
|
+
if node_uid not in self._nodes:
|
|
248
|
+
return []
|
|
249
|
+
|
|
250
|
+
return [self._edges[uid] for uid in self._outgoing_edges[node_uid]]
|
|
251
|
+
|
|
252
|
+
def get_incoming_edges(self, node_uid: str) -> list[Edge]:
|
|
253
|
+
"""
|
|
254
|
+
Get all incoming edges to a node.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
node_uid: The uid of the node
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
list[Edge]: List of incoming edges
|
|
261
|
+
"""
|
|
262
|
+
if node_uid not in self._nodes:
|
|
263
|
+
return []
|
|
264
|
+
|
|
265
|
+
return [self._edges[uid] for uid in self._incoming_edges[node_uid]]
|
|
266
|
+
|
|
267
|
+
def nodes(self) -> Iterator[Node]:
|
|
268
|
+
"""
|
|
269
|
+
Iterate over all nodes in the graph.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Iterator[Node]: An iterator over all nodes
|
|
273
|
+
"""
|
|
274
|
+
return iter(self._nodes.values())
|
|
275
|
+
|
|
276
|
+
def edges(self) -> Iterator[Edge]:
|
|
277
|
+
"""
|
|
278
|
+
Iterate over all edges in the graph.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Iterator[Edge]: An iterator over all edges
|
|
282
|
+
"""
|
|
283
|
+
return iter(self._edges.values())
|
|
284
|
+
|
|
285
|
+
def node_count(self) -> int:
|
|
286
|
+
"""
|
|
287
|
+
Get the number of nodes in the graph.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
int: The number of nodes
|
|
291
|
+
"""
|
|
292
|
+
return len(self._nodes)
|
|
293
|
+
|
|
294
|
+
def edge_count(self) -> int:
|
|
295
|
+
"""
|
|
296
|
+
Get the number of edges in the graph.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
int: The number of edges
|
|
300
|
+
"""
|
|
301
|
+
return len(self._edges)
|
|
302
|
+
|
|
303
|
+
# ========== Validation ==========
|
|
304
|
+
|
|
305
|
+
def validate(self) -> bool:
|
|
306
|
+
"""
|
|
307
|
+
Validate the graph for consistency.
|
|
308
|
+
|
|
309
|
+
Checks:
|
|
310
|
+
- All edges reference existing nodes
|
|
311
|
+
- Edge node references match stored nodes
|
|
312
|
+
- Adjacency lists are consistent
|
|
313
|
+
- Adjacency lists only reference existing nodes
|
|
314
|
+
- Each edge in adjacency lists matches the correct source/destination
|
|
315
|
+
- No orphaned entries in adjacency structures
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
bool: True if the graph is valid
|
|
319
|
+
|
|
320
|
+
Raises:
|
|
321
|
+
ValueError: If validation fails
|
|
322
|
+
"""
|
|
323
|
+
# First: Check that all nodes have corresponding adjacency list entries
|
|
324
|
+
# This must be done before we try to access adjacency lists
|
|
325
|
+
for node_uid in self._nodes:
|
|
326
|
+
if node_uid not in self._outgoing_edges:
|
|
327
|
+
raise ValueError(
|
|
328
|
+
f"Node '{node_uid}' missing from outgoing edges structure"
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
if node_uid not in self._incoming_edges:
|
|
332
|
+
raise ValueError(
|
|
333
|
+
f"Node '{node_uid}' missing from incoming edges structure"
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Second: Check that all adjacency list keys correspond to existing nodes
|
|
337
|
+
for node_uid in self._outgoing_edges:
|
|
338
|
+
if node_uid not in self._nodes:
|
|
339
|
+
raise ValueError(
|
|
340
|
+
f"Outgoing edges structure contains orphaned node '{node_uid}'"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
for node_uid in self._incoming_edges:
|
|
344
|
+
if node_uid not in self._nodes:
|
|
345
|
+
raise ValueError(
|
|
346
|
+
f"Incoming edges structure contains orphaned node '{node_uid}'"
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# Third: Check all edges reference valid nodes
|
|
350
|
+
for edge_uid, edge in self._edges.items():
|
|
351
|
+
src_uid = edge.src_node.uid
|
|
352
|
+
dst_uid = edge.dst_node.uid
|
|
353
|
+
|
|
354
|
+
if src_uid not in self._nodes:
|
|
355
|
+
raise ValueError(
|
|
356
|
+
f"Edge '{edge_uid}' references non-existent source node '{src_uid}'"
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
if dst_uid not in self._nodes:
|
|
360
|
+
raise ValueError(
|
|
361
|
+
f"Edge '{edge_uid}' references non-existent destination node '{dst_uid}'"
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# Check node references match
|
|
365
|
+
if self._nodes[src_uid] != edge.src_node:
|
|
366
|
+
raise ValueError(
|
|
367
|
+
f"Edge '{edge_uid}' source node doesn't match stored node '{src_uid}'"
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
if self._nodes[dst_uid] != edge.dst_node:
|
|
371
|
+
raise ValueError(
|
|
372
|
+
f"Edge '{edge_uid}' destination node doesn't match stored node '{dst_uid}'"
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# Check adjacency lists
|
|
376
|
+
if edge_uid not in self._outgoing_edges[src_uid]:
|
|
377
|
+
raise ValueError(
|
|
378
|
+
f"Edge '{edge_uid}' not in outgoing edges of node '{src_uid}'"
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
if edge_uid not in self._incoming_edges[dst_uid]:
|
|
382
|
+
raise ValueError(
|
|
383
|
+
f"Edge '{edge_uid}' not in incoming edges of node '{dst_uid}'"
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
# Fourth: Check adjacency lists reference valid edges and correct nodes
|
|
387
|
+
for node_uid in self._nodes:
|
|
388
|
+
# Validate outgoing edges
|
|
389
|
+
for edge_uid in self._outgoing_edges[node_uid]:
|
|
390
|
+
if edge_uid not in self._edges:
|
|
391
|
+
raise ValueError(
|
|
392
|
+
f"Outgoing edge list of node '{node_uid}' references "
|
|
393
|
+
f"non-existent edge '{edge_uid}'"
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# Verify the edge actually originates from this node
|
|
397
|
+
edge = self._edges[edge_uid]
|
|
398
|
+
if edge.src_node.uid != node_uid:
|
|
399
|
+
raise ValueError(
|
|
400
|
+
f"Edge '{edge_uid}' in outgoing list of node '{node_uid}' "
|
|
401
|
+
f"has incorrect source node '{edge.src_node.uid}'"
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
# Validate incoming edges
|
|
405
|
+
for edge_uid in self._incoming_edges[node_uid]:
|
|
406
|
+
if edge_uid not in self._edges:
|
|
407
|
+
raise ValueError(
|
|
408
|
+
f"Incoming edge list of node '{node_uid}' references "
|
|
409
|
+
f"non-existent edge '{edge_uid}'"
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
# Verify the edge actually terminates at this node
|
|
413
|
+
edge = self._edges[edge_uid]
|
|
414
|
+
if edge.dst_node.uid != node_uid:
|
|
415
|
+
raise ValueError(
|
|
416
|
+
f"Edge '{edge_uid}' in incoming list of node '{node_uid}' "
|
|
417
|
+
f"has incorrect destination node '{edge.dst_node.uid}'"
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
return True
|
|
421
|
+
|
|
422
|
+
# ========== Connection API ==========
|
|
423
|
+
|
|
424
|
+
def connect(self) -> Connection:
|
|
425
|
+
"""
|
|
426
|
+
Create a connection for transactional graph modifications.
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
Connection: A new connection object
|
|
430
|
+
"""
|
|
431
|
+
|
|
432
|
+
return Connection(self)
|
|
433
|
+
|
|
434
|
+
def __str__(self) -> str:
|
|
435
|
+
"""Return a string representation of the graph."""
|
|
436
|
+
return f"Graph(nodes={self.node_count()}, edges={self.edge_count()})"
|
|
437
|
+
|
|
438
|
+
def __repr__(self) -> str:
|
|
439
|
+
"""Return a detailed representation."""
|
|
440
|
+
return (
|
|
441
|
+
f"Graph(nodes={self.node_count()}, edges={self.edge_count()}, "
|
|
442
|
+
f"node_list={list(self._nodes.keys())})"
|
|
443
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from .base import Mutation
|
|
2
|
+
from .add_node import AddNode
|
|
3
|
+
from .remove_node import RemoveNode
|
|
4
|
+
from .try_add_node import TryAddNode
|
|
5
|
+
from .try_remove_node import TryRemoveNode
|
|
6
|
+
from .add_edge import AddEdge
|
|
7
|
+
from .remove_edge import RemoveEdge
|
|
8
|
+
from .try_add_edge import TryAddEdge
|
|
9
|
+
from .try_remove_edge import TryRemoveEdge
|
|
10
|
+
from .add_many_nodes import AddManyNodes
|
|
11
|
+
from .add_many_edges import AddManyEdges
|
|
12
|
+
from .remove_many_nodes import RemoveManyNodes
|
|
13
|
+
from .remove_many_edges import RemoveManyEdges
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"Mutation",
|
|
17
|
+
"AddNode",
|
|
18
|
+
"RemoveNode",
|
|
19
|
+
"TryAddNode",
|
|
20
|
+
"TryRemoveNode",
|
|
21
|
+
"AddEdge",
|
|
22
|
+
"RemoveEdge",
|
|
23
|
+
"TryAddEdge",
|
|
24
|
+
"TryRemoveEdge",
|
|
25
|
+
"AddManyNodes",
|
|
26
|
+
"AddManyEdges",
|
|
27
|
+
"RemoveManyNodes",
|
|
28
|
+
"RemoveManyEdges",
|
|
29
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
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 AddEdge(Mutation):
|
|
11
|
+
"""Mutation to add an edge to the graph."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, edge: Edge):
|
|
14
|
+
"""
|
|
15
|
+
Initialize the AddEdge mutation.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
edge: The edge to add
|
|
19
|
+
"""
|
|
20
|
+
self.edge = edge
|
|
21
|
+
|
|
22
|
+
def forward(self, graph: "Graph") -> None:
|
|
23
|
+
"""
|
|
24
|
+
Add the edge to the graph.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
graph: The graph to modify
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
ValueError: If the edge already exists or nodes don't exist
|
|
31
|
+
"""
|
|
32
|
+
graph._add_edge(self.edge)
|
|
33
|
+
|
|
34
|
+
def backward(self, graph: "Graph") -> None:
|
|
35
|
+
"""
|
|
36
|
+
Remove the edge from the graph.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
graph: The graph to modify
|
|
40
|
+
"""
|
|
41
|
+
graph._remove_edge(self.edge.uid)
|
|
42
|
+
|
|
43
|
+
def __str__(self) -> str:
|
|
44
|
+
"""Return a string representation."""
|
|
45
|
+
return f"AddEdge({self.edge})"
|
|
@@ -0,0 +1,60 @@
|
|
|
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 AddManyEdges(Mutation):
|
|
11
|
+
"""Mutation to add multiple edges to the graph."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, edges: list[Edge]):
|
|
14
|
+
"""
|
|
15
|
+
Initialize the AddManyEdges mutation.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
edges: List of edges to add
|
|
19
|
+
"""
|
|
20
|
+
self.edges = edges
|
|
21
|
+
self._added_edges: list[Edge] = []
|
|
22
|
+
|
|
23
|
+
def forward(self, graph: "Graph") -> None:
|
|
24
|
+
"""
|
|
25
|
+
Add all edges to the graph.
|
|
26
|
+
|
|
27
|
+
If any edge fails to be added, all previously added edges are rolled back.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
graph: The graph to modify
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
ValueError: If any edge already exists or nodes don't exist
|
|
34
|
+
"""
|
|
35
|
+
self._added_edges = []
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
for edge in self.edges:
|
|
39
|
+
graph._add_edge(edge)
|
|
40
|
+
self._added_edges.append(edge)
|
|
41
|
+
except Exception:
|
|
42
|
+
# Rollback: remove all edges that were added
|
|
43
|
+
for edge in self._added_edges:
|
|
44
|
+
graph._remove_edge(edge.uid)
|
|
45
|
+
self._added_edges = []
|
|
46
|
+
raise
|
|
47
|
+
|
|
48
|
+
def backward(self, graph: "Graph") -> None:
|
|
49
|
+
"""
|
|
50
|
+
Remove all added edges from the graph.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
graph: The graph to modify
|
|
54
|
+
"""
|
|
55
|
+
for edge in reversed(self._added_edges):
|
|
56
|
+
graph._remove_edge(edge.uid)
|
|
57
|
+
|
|
58
|
+
def __str__(self) -> str:
|
|
59
|
+
"""Return a string representation."""
|
|
60
|
+
return f"AddManyEdges(count={len(self.edges)})"
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from ..graph.elements import Node
|
|
4
|
+
from .base import Mutation
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from ..graph.graph import Graph
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AddManyNodes(Mutation):
|
|
11
|
+
"""Mutation to add multiple nodes to the graph."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, nodes: list[Node]):
|
|
14
|
+
"""
|
|
15
|
+
Initialize the AddManyNodes mutation.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
nodes: List of nodes to add
|
|
19
|
+
"""
|
|
20
|
+
self.nodes = nodes
|
|
21
|
+
self._added_nodes: list[Node] = []
|
|
22
|
+
|
|
23
|
+
def forward(self, graph: "Graph") -> None:
|
|
24
|
+
"""
|
|
25
|
+
Add all nodes to the graph.
|
|
26
|
+
|
|
27
|
+
If any node fails to be added, all previously added nodes are rolled back.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
graph: The graph to modify
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
ValueError: If any node already exists
|
|
34
|
+
"""
|
|
35
|
+
self._added_nodes = []
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
for node in self.nodes:
|
|
39
|
+
graph._add_node(node)
|
|
40
|
+
self._added_nodes.append(node)
|
|
41
|
+
except Exception:
|
|
42
|
+
# Rollback: remove all nodes that were added
|
|
43
|
+
for node in self._added_nodes:
|
|
44
|
+
graph._remove_node(node.uid)
|
|
45
|
+
self._added_nodes = []
|
|
46
|
+
raise
|
|
47
|
+
|
|
48
|
+
def backward(self, graph: "Graph") -> None:
|
|
49
|
+
"""
|
|
50
|
+
Remove all added nodes from the graph.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
graph: The graph to modify
|
|
54
|
+
"""
|
|
55
|
+
for node in reversed(self._added_nodes):
|
|
56
|
+
graph._remove_node(node.uid)
|
|
57
|
+
|
|
58
|
+
def __str__(self) -> str:
|
|
59
|
+
"""Return a string representation."""
|
|
60
|
+
return f"AddManyNodes(count={len(self.nodes)})"
|
|
@@ -0,0 +1,45 @@
|
|
|
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 AddNode(Mutation):
|
|
11
|
+
"""Mutation to add a node to the graph."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, node: Node):
|
|
14
|
+
"""
|
|
15
|
+
Initialize the AddNode mutation.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
node: The node to add
|
|
19
|
+
"""
|
|
20
|
+
self.node = node
|
|
21
|
+
|
|
22
|
+
def forward(self, graph: "Graph") -> None:
|
|
23
|
+
"""
|
|
24
|
+
Add the node to the graph.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
graph: The graph to modify
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
ValueError: If the node already exists
|
|
31
|
+
"""
|
|
32
|
+
graph._add_node(self.node)
|
|
33
|
+
|
|
34
|
+
def backward(self, graph: "Graph") -> None:
|
|
35
|
+
"""
|
|
36
|
+
Remove the node from the graph.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
graph: The graph to modify
|
|
40
|
+
"""
|
|
41
|
+
graph._remove_node(self.node.uid)
|
|
42
|
+
|
|
43
|
+
def __str__(self) -> str:
|
|
44
|
+
"""Return a string representation."""
|
|
45
|
+
return f"AddNode({self.node})"
|