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,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)