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
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Connection system for transactional graph modifications.
|
|
3
|
+
|
|
4
|
+
This module defines the Connection class which accumulates mutations and
|
|
5
|
+
executes them transactionally with automatic rollback on failure.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from .elements import Node
|
|
11
|
+
from ..core.combinator import Combinator
|
|
12
|
+
from ..mutations import (
|
|
13
|
+
Mutation,
|
|
14
|
+
AddNode,
|
|
15
|
+
RemoveNode,
|
|
16
|
+
AddEdge,
|
|
17
|
+
RemoveEdge,
|
|
18
|
+
AddManyNodes,
|
|
19
|
+
AddManyEdges,
|
|
20
|
+
TryAddNode,
|
|
21
|
+
TryAddEdge,
|
|
22
|
+
RemoveManyNodes,
|
|
23
|
+
RemoveManyEdges,
|
|
24
|
+
TryRemoveNode,
|
|
25
|
+
TryRemoveEdge,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from .graph import Graph
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Connection:
|
|
33
|
+
"""
|
|
34
|
+
A connection for making transactional modifications to a graph.
|
|
35
|
+
|
|
36
|
+
Usage:
|
|
37
|
+
with graph.connect() as conn:
|
|
38
|
+
conn.add_node(node1)
|
|
39
|
+
conn.add_edge(edge1)
|
|
40
|
+
# Changes are committed automatically if no exception occurs
|
|
41
|
+
# Otherwise, all changes are rolled back
|
|
42
|
+
|
|
43
|
+
Can also be used without context manager:
|
|
44
|
+
conn = graph.connect()
|
|
45
|
+
conn.add_node(node1)
|
|
46
|
+
conn.commit() # or conn.rollback()
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, graph: "Graph"):
|
|
50
|
+
"""
|
|
51
|
+
Initialize a connection.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
graph: The graph to operate on
|
|
55
|
+
"""
|
|
56
|
+
self.graph = graph
|
|
57
|
+
self.mutations: list[Mutation] = []
|
|
58
|
+
self._committed = False
|
|
59
|
+
self._rolled_back = False
|
|
60
|
+
|
|
61
|
+
def add_node(self, node: Node) -> "Connection":
|
|
62
|
+
"""
|
|
63
|
+
Add a mutation to create a node.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
node: The node to add
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Connection: Self for method chaining
|
|
70
|
+
"""
|
|
71
|
+
self._check_state()
|
|
72
|
+
self.mutations.append(AddNode(node))
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
def remove_node(self, node_uid: str) -> "Connection":
|
|
76
|
+
"""
|
|
77
|
+
Add a mutation to remove a node.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
node_uid: The uid of the node to remove
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Connection: Self for method chaining
|
|
84
|
+
"""
|
|
85
|
+
self._check_state()
|
|
86
|
+
self.mutations.append(RemoveNode(node_uid))
|
|
87
|
+
return self
|
|
88
|
+
|
|
89
|
+
def add_edge(self, combinator: Combinator) -> "Connection":
|
|
90
|
+
"""
|
|
91
|
+
Add a mutation to create an edge from a combinator.
|
|
92
|
+
|
|
93
|
+
The edge will be created with nodes corresponding to the combinator's
|
|
94
|
+
input and output types. The nodes must exist when the mutation is applied.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
combinator: The combinator for this edge
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Connection: Self for method chaining
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
ValueError: If the combinator doesn't have an Application type
|
|
104
|
+
"""
|
|
105
|
+
self._check_state()
|
|
106
|
+
|
|
107
|
+
# Import here to avoid circular import
|
|
108
|
+
from .elements import edge
|
|
109
|
+
from ..core.types import Application
|
|
110
|
+
|
|
111
|
+
# Validate combinator type
|
|
112
|
+
if not isinstance(combinator.type, Application):
|
|
113
|
+
raise ValueError(
|
|
114
|
+
f"Combinator must have an Application type to create an edge, "
|
|
115
|
+
f"got {type(combinator.type).__name__}"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Create the edge - validation that nodes exist will happen at forward() time
|
|
119
|
+
e = edge(combinator)
|
|
120
|
+
self.mutations.append(AddEdge(e))
|
|
121
|
+
return self
|
|
122
|
+
|
|
123
|
+
def remove_edge(self, edge_uid: str) -> "Connection":
|
|
124
|
+
"""
|
|
125
|
+
Add a mutation to remove an edge.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
edge_uid: The uid of the edge to remove
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Connection: Self for method chaining
|
|
132
|
+
"""
|
|
133
|
+
self._check_state()
|
|
134
|
+
self.mutations.append(RemoveEdge(edge_uid))
|
|
135
|
+
return self
|
|
136
|
+
|
|
137
|
+
def add_many_nodes(self, nodes: list[Node]) -> "Connection":
|
|
138
|
+
"""
|
|
139
|
+
Add a mutation to create multiple nodes.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
nodes: List of nodes to add
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Connection: Self for method chaining
|
|
146
|
+
"""
|
|
147
|
+
self._check_state()
|
|
148
|
+
self.mutations.append(AddManyNodes(nodes))
|
|
149
|
+
return self
|
|
150
|
+
|
|
151
|
+
def add_many_edges(self, combinators: list[Combinator]) -> "Connection":
|
|
152
|
+
"""
|
|
153
|
+
Add a mutation to create multiple edges from combinators.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
combinators: List of combinators for the edges
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Connection: Self for method chaining
|
|
160
|
+
|
|
161
|
+
Raises:
|
|
162
|
+
ValueError: If any combinator doesn't have an Application type
|
|
163
|
+
"""
|
|
164
|
+
self._check_state()
|
|
165
|
+
|
|
166
|
+
# Import here to avoid circular import
|
|
167
|
+
from .elements import edge
|
|
168
|
+
from ..core.types import Application
|
|
169
|
+
|
|
170
|
+
# Validate all combinators and create edges
|
|
171
|
+
edges = []
|
|
172
|
+
for combinator in combinators:
|
|
173
|
+
if not isinstance(combinator.type, Application):
|
|
174
|
+
raise ValueError(
|
|
175
|
+
f"Combinator must have an Application type to create an edge, "
|
|
176
|
+
f"got {type(combinator.type).__name__}"
|
|
177
|
+
)
|
|
178
|
+
edges.append(edge(combinator))
|
|
179
|
+
|
|
180
|
+
self.mutations.append(AddManyEdges(edges))
|
|
181
|
+
return self
|
|
182
|
+
|
|
183
|
+
def try_add_node(self, node: Node) -> "Connection":
|
|
184
|
+
"""
|
|
185
|
+
Add a mutation to create a node, or do nothing if it already exists.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
node: The node to add
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Connection: Self for method chaining
|
|
192
|
+
"""
|
|
193
|
+
self._check_state()
|
|
194
|
+
self.mutations.append(TryAddNode(node))
|
|
195
|
+
return self
|
|
196
|
+
|
|
197
|
+
def try_add_edge(self, combinator: Combinator) -> "Connection":
|
|
198
|
+
"""
|
|
199
|
+
Add a mutation to create an edge from a combinator, or do nothing if it already exists.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
combinator: The combinator for this edge
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Connection: Self for method chaining
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
ValueError: If the combinator doesn't have an Application type
|
|
209
|
+
"""
|
|
210
|
+
self._check_state()
|
|
211
|
+
|
|
212
|
+
# Import here to avoid circular import
|
|
213
|
+
from .elements import edge
|
|
214
|
+
from ..core.types import Application
|
|
215
|
+
|
|
216
|
+
# Validate combinator type
|
|
217
|
+
if not isinstance(combinator.type, Application):
|
|
218
|
+
raise ValueError(
|
|
219
|
+
f"Combinator must have an Application type to create an edge, "
|
|
220
|
+
f"got {type(combinator.type).__name__}"
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Create the edge
|
|
224
|
+
e = edge(combinator)
|
|
225
|
+
self.mutations.append(TryAddEdge(e))
|
|
226
|
+
return self
|
|
227
|
+
|
|
228
|
+
def remove_many_nodes(self, node_uids: list[str]) -> "Connection":
|
|
229
|
+
"""
|
|
230
|
+
Add a mutation to remove multiple nodes.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
node_uids: List of node uids to remove
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Connection: Self for method chaining
|
|
237
|
+
"""
|
|
238
|
+
self._check_state()
|
|
239
|
+
self.mutations.append(RemoveManyNodes(node_uids))
|
|
240
|
+
return self
|
|
241
|
+
|
|
242
|
+
def remove_many_edges(self, edge_uids: list[str]) -> "Connection":
|
|
243
|
+
"""
|
|
244
|
+
Add a mutation to remove multiple edges.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
edge_uids: List of edge uids to remove
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Connection: Self for method chaining
|
|
251
|
+
"""
|
|
252
|
+
self._check_state()
|
|
253
|
+
self.mutations.append(RemoveManyEdges(edge_uids))
|
|
254
|
+
return self
|
|
255
|
+
|
|
256
|
+
def try_remove_node(self, node_uid: str) -> "Connection":
|
|
257
|
+
"""
|
|
258
|
+
Add a mutation to remove a node, or do nothing if it doesn't exist.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
node_uid: The uid of the node to remove
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Connection: Self for method chaining
|
|
265
|
+
"""
|
|
266
|
+
self._check_state()
|
|
267
|
+
self.mutations.append(TryRemoveNode(node_uid))
|
|
268
|
+
return self
|
|
269
|
+
|
|
270
|
+
def try_remove_edge(self, edge_uid: str) -> "Connection":
|
|
271
|
+
"""
|
|
272
|
+
Add a mutation to remove an edge, or do nothing if it doesn't exist.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
edge_uid: The uid of the edge to remove
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
Connection: Self for method chaining
|
|
279
|
+
"""
|
|
280
|
+
self._check_state()
|
|
281
|
+
self.mutations.append(TryRemoveEdge(edge_uid))
|
|
282
|
+
return self
|
|
283
|
+
|
|
284
|
+
def commit(self) -> None:
|
|
285
|
+
"""
|
|
286
|
+
Commit all mutations to the graph.
|
|
287
|
+
|
|
288
|
+
If any mutation fails or the graph doesn't validate after all mutations,
|
|
289
|
+
all changes are rolled back.
|
|
290
|
+
|
|
291
|
+
Raises:
|
|
292
|
+
RuntimeError: If already committed or rolled back
|
|
293
|
+
Exception: Any exception from mutations or validation
|
|
294
|
+
"""
|
|
295
|
+
self._check_state()
|
|
296
|
+
|
|
297
|
+
applied_count = 0
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
# Apply all mutations
|
|
301
|
+
for mutation in self.mutations:
|
|
302
|
+
mutation.forward(self.graph)
|
|
303
|
+
applied_count += 1
|
|
304
|
+
|
|
305
|
+
# Validate the graph
|
|
306
|
+
self.graph.validate()
|
|
307
|
+
|
|
308
|
+
# Mark as committed
|
|
309
|
+
self._committed = True
|
|
310
|
+
|
|
311
|
+
except Exception as e:
|
|
312
|
+
# Rollback all applied mutations in reverse order
|
|
313
|
+
for i in range(applied_count - 1, -1, -1):
|
|
314
|
+
try:
|
|
315
|
+
self.mutations[i].backward(self.graph)
|
|
316
|
+
except Exception as rollback_error:
|
|
317
|
+
# Log rollback error but continue rolling back
|
|
318
|
+
print(f"Warning: Error during rollback: {rollback_error}")
|
|
319
|
+
|
|
320
|
+
self._rolled_back = True
|
|
321
|
+
|
|
322
|
+
# Re-raise the original exception
|
|
323
|
+
raise RuntimeError(
|
|
324
|
+
f"Transaction failed and was rolled back. "
|
|
325
|
+
f"Applied {applied_count}/{len(self.mutations)} mutations before failure."
|
|
326
|
+
) from e
|
|
327
|
+
|
|
328
|
+
def rollback(self) -> None:
|
|
329
|
+
"""
|
|
330
|
+
Explicitly rollback all mutations without applying them.
|
|
331
|
+
|
|
332
|
+
Raises:
|
|
333
|
+
RuntimeError: If already committed or rolled back
|
|
334
|
+
"""
|
|
335
|
+
self._check_state()
|
|
336
|
+
self._rolled_back = True
|
|
337
|
+
|
|
338
|
+
def _check_state(self) -> None:
|
|
339
|
+
"""
|
|
340
|
+
Check that the connection is in a valid state for operations.
|
|
341
|
+
|
|
342
|
+
Raises:
|
|
343
|
+
RuntimeError: If already committed or rolled back
|
|
344
|
+
"""
|
|
345
|
+
if self._committed:
|
|
346
|
+
raise RuntimeError("Connection has already been committed")
|
|
347
|
+
if self._rolled_back:
|
|
348
|
+
raise RuntimeError("Connection has already been rolled back")
|
|
349
|
+
|
|
350
|
+
def __enter__(self) -> "Connection":
|
|
351
|
+
"""Enter the context manager."""
|
|
352
|
+
return self
|
|
353
|
+
|
|
354
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
|
|
355
|
+
"""
|
|
356
|
+
Exit the context manager.
|
|
357
|
+
|
|
358
|
+
Commits if no exception occurred, otherwise rolls back.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
bool: False to propagate any exception
|
|
362
|
+
"""
|
|
363
|
+
if exc_type is None:
|
|
364
|
+
# No exception, commit
|
|
365
|
+
try:
|
|
366
|
+
self.commit()
|
|
367
|
+
except Exception:
|
|
368
|
+
# Let the commit exception propagate
|
|
369
|
+
return False
|
|
370
|
+
else:
|
|
371
|
+
# Exception occurred, rollback
|
|
372
|
+
if not self._rolled_back:
|
|
373
|
+
self.rollback()
|
|
374
|
+
|
|
375
|
+
# Don't suppress the exception
|
|
376
|
+
return False
|
|
377
|
+
|
|
378
|
+
def __str__(self) -> str:
|
|
379
|
+
"""Return a string representation."""
|
|
380
|
+
status = (
|
|
381
|
+
"committed"
|
|
382
|
+
if self._committed
|
|
383
|
+
else ("rolled_back" if self._rolled_back else "open")
|
|
384
|
+
)
|
|
385
|
+
return f"Connection(mutations={len(self.mutations)}, status={status})"
|
|
386
|
+
|
|
387
|
+
def __repr__(self) -> str:
|
|
388
|
+
"""Return a detailed representation."""
|
|
389
|
+
return f"Connection(graph={self.graph}, mutations={self.mutations}, committed={self._committed}, rolled_back={self._rolled_back})"
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Graph elements for the implicational logic graph.
|
|
3
|
+
|
|
4
|
+
This module defines the Node and Edge types that compose the graph structure.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
from functools import cached_property
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
12
|
+
|
|
13
|
+
from ..core.combinator import Combinator
|
|
14
|
+
from ..core.types import BaseType, Application
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Node(BaseModel):
|
|
18
|
+
"""
|
|
19
|
+
A node in the implicational logic graph.
|
|
20
|
+
|
|
21
|
+
Each node represents a type (either a Variable or an Application).
|
|
22
|
+
Nodes are identified by their type's uid for efficient lookup.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
type: BaseType = Field(..., description="The type represented by this node")
|
|
26
|
+
|
|
27
|
+
model_config = ConfigDict(frozen=True) # Make immutable
|
|
28
|
+
|
|
29
|
+
@cached_property
|
|
30
|
+
def uid(self) -> str:
|
|
31
|
+
"""
|
|
32
|
+
Return the unique identifier for this node.
|
|
33
|
+
|
|
34
|
+
The uid is the same as the type's uid, making nodes uniquely
|
|
35
|
+
identifiable by their type.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
str: The node's unique identifier
|
|
39
|
+
"""
|
|
40
|
+
return self.type.uid
|
|
41
|
+
|
|
42
|
+
def __str__(self) -> str:
|
|
43
|
+
"""Return a string representation of the node."""
|
|
44
|
+
return f"Node({self.type})"
|
|
45
|
+
|
|
46
|
+
def __repr__(self) -> str:
|
|
47
|
+
"""Return a detailed representation."""
|
|
48
|
+
return f"Node(type={repr(self.type)})"
|
|
49
|
+
|
|
50
|
+
def __hash__(self) -> int:
|
|
51
|
+
"""Make node hashable based on its type's uid."""
|
|
52
|
+
return hash(self.uid)
|
|
53
|
+
|
|
54
|
+
def __eq__(self, other: Any) -> bool:
|
|
55
|
+
"""Check equality based on type's uid."""
|
|
56
|
+
if not isinstance(other, Node):
|
|
57
|
+
return False
|
|
58
|
+
return self.uid == other.uid
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Edge(BaseModel):
|
|
62
|
+
"""
|
|
63
|
+
An edge in the implicational logic graph.
|
|
64
|
+
|
|
65
|
+
Each edge represents a combinator that transforms from the source node's
|
|
66
|
+
type to the destination node's type.
|
|
67
|
+
|
|
68
|
+
The edge is valid if and only if:
|
|
69
|
+
- src_node.type matches the combinator's input type
|
|
70
|
+
- dst_node.type matches the combinator's output type
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
src_node: Node = Field(..., description="Source node of the edge")
|
|
74
|
+
dst_node: Node = Field(..., description="Destination node of the edge")
|
|
75
|
+
combinator: Combinator = Field(
|
|
76
|
+
..., description="The combinator for this transformation"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
model_config = ConfigDict(frozen=True) # Make immutable
|
|
80
|
+
|
|
81
|
+
@model_validator(mode="after")
|
|
82
|
+
def validate_combinator_type(self) -> "Edge":
|
|
83
|
+
"""
|
|
84
|
+
Validate that the combinator's type matches the transformation.
|
|
85
|
+
|
|
86
|
+
The combinator must have an Application type where:
|
|
87
|
+
- input_type matches the source node's type
|
|
88
|
+
- output_type matches the destination node's type
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Edge: The validated edge instance
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
ValueError: If the combinator type doesn't match the edge transformation
|
|
95
|
+
"""
|
|
96
|
+
# Check if combinator type is an Application
|
|
97
|
+
if not isinstance(self.combinator.type, Application):
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"Combinator must have an Application type, "
|
|
100
|
+
f"got {type(self.combinator.type).__name__}"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Check if input type matches source node
|
|
104
|
+
if self.combinator.type.input_type.uid != self.src_node.type.uid:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
f"Combinator input type {self.combinator.type.input_type} "
|
|
107
|
+
f"does not match source node type {self.src_node.type}"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Check if output type matches destination node
|
|
111
|
+
if self.combinator.type.output_type.uid != self.dst_node.type.uid:
|
|
112
|
+
raise ValueError(
|
|
113
|
+
f"Combinator output type {self.combinator.type.output_type} "
|
|
114
|
+
f"does not match destination node type {self.dst_node.type}"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return self
|
|
118
|
+
|
|
119
|
+
@cached_property
|
|
120
|
+
def uid(self) -> str:
|
|
121
|
+
"""
|
|
122
|
+
Return a unique identifier for this edge.
|
|
123
|
+
|
|
124
|
+
The uid is composed of the source uid, combinator uid, and destination uid.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
str: SHA256 hash of the edge structure
|
|
128
|
+
"""
|
|
129
|
+
content = f"Edge({self.src_node.uid},{self.combinator.uid},{self.dst_node.uid})"
|
|
130
|
+
return hashlib.sha256(content.encode("utf-8")).hexdigest()
|
|
131
|
+
|
|
132
|
+
def __str__(self) -> str:
|
|
133
|
+
"""Return a string representation of the edge."""
|
|
134
|
+
return f"{self.src_node.type} --[{self.combinator}]--> {self.dst_node.type}"
|
|
135
|
+
|
|
136
|
+
def __repr__(self) -> str:
|
|
137
|
+
"""Return a detailed representation."""
|
|
138
|
+
return (
|
|
139
|
+
f"Edge(src_node={repr(self.src_node)}, "
|
|
140
|
+
f"dst_node={repr(self.dst_node)}, "
|
|
141
|
+
f"combinator={repr(self.combinator)})"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def __hash__(self) -> int:
|
|
145
|
+
"""Make edge hashable based on its uid."""
|
|
146
|
+
return hash(self.uid)
|
|
147
|
+
|
|
148
|
+
def __eq__(self, other: Any) -> bool:
|
|
149
|
+
"""Check equality based on uid."""
|
|
150
|
+
if not isinstance(other, Edge):
|
|
151
|
+
return False
|
|
152
|
+
return self.uid == other.uid
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# Helper functions
|
|
156
|
+
def node(type: BaseType) -> Node:
|
|
157
|
+
"""
|
|
158
|
+
Helper function to create a Node.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
type: The type for the node
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Node: A new Node instance
|
|
165
|
+
"""
|
|
166
|
+
return Node(type=type)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def edge(combinator: Combinator) -> Edge:
|
|
170
|
+
"""
|
|
171
|
+
Helper function to create an Edge from a Combinator.
|
|
172
|
+
|
|
173
|
+
The combinator must have an Application type, and the edge will be created
|
|
174
|
+
with nodes corresponding to the input and output types.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
combinator: The combinator for this edge
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Edge: A new Edge instance
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
ValueError: If the combinator doesn't have an Application type
|
|
184
|
+
"""
|
|
185
|
+
if not isinstance(combinator.type, Application):
|
|
186
|
+
raise ValueError(
|
|
187
|
+
f"Combinator must have an Application type to create an edge, "
|
|
188
|
+
f"got {type(combinator.type).__name__}"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
src_node = node(combinator.type.input_type)
|
|
192
|
+
dst_node = node(combinator.type.output_type)
|
|
193
|
+
|
|
194
|
+
return Edge(src_node=src_node, dst_node=dst_node, combinator=combinator)
|