smartcomment 0.1.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.
@@ -0,0 +1,69 @@
1
+ """Debugging utilities for execution graph visualization.
2
+
3
+ This module provides one-shot trace-and-visualize helpers that create
4
+ an anonymous (non-persistent) execution graph, run user code inside it,
5
+ and render the resulting graph. It is intended for interactive debugging and
6
+ quick inspection of traced pipelines.
7
+ """
8
+
9
+ import asyncio
10
+ from .runtime.context import comment_graph
11
+ from typing import Any, Callable
12
+
13
+
14
+ def draw_graph(
15
+ fn: Callable,
16
+ /,
17
+ *,
18
+ fn_kwargs: dict[str, Any] | None = None,
19
+ backend: str = "graphviz",
20
+ **kwargs: Any,
21
+ ) -> Any:
22
+ """Trace a function call and visualize the resulting execution graph.
23
+
24
+ It creates an anonymous execution graph, runs the given function
25
+ inside it so that all ``comment_*`` calls within function are captured,
26
+ then renders the graph using the specified visualization backend.
27
+ The graph is discarded after visualization. This function is purely for
28
+ debugging and does not persist any data.
29
+
30
+ Both synchronous and asynchronous entry functions are supported.
31
+ However, if the function you pass in opens its own ``comment_graph`` block,
32
+ anything that runs inside that inner block is written to a different graph object.
33
+ When the inner block ends, the context switches back, but ``draw_graph``
34
+ still only knows about the outer graph it opens first. As a result,
35
+ the picture you get may miss most of the nodes and edges, or look nearly empty,
36
+ even though tracing does occur.
37
+
38
+ Example::
39
+
40
+ from smartcomment import draw_graph
41
+
42
+ def my_pipeline(query: str, k: int) -> None:
43
+ ... # calls comment_op, comment_variable, etc.
44
+
45
+ draw_graph(my_pipeline, fn_kwargs={"query": "hello", "k": 5})
46
+
47
+ Args:
48
+ fn (`Callable`):
49
+ Entry-point function whose body contains ``comment_*`` calls.
50
+ fn_kwargs (`dict[str, Any] | None`, optional):
51
+ Keyword arguments forwarded to the given function.
52
+ backend (`str`, defaults to `"graphviz"`):
53
+ Visualization backend name.
54
+ **kwargs (`Any`):
55
+ Additional keyword arguments forwarded to the visualization
56
+ backend.
57
+
58
+ Returns:
59
+ `Any`:
60
+ The return value of the visualization backend.
61
+ """
62
+ with comment_graph() as graph:
63
+ if asyncio.iscoroutinefunction(fn):
64
+ asyncio.run(fn(**(fn_kwargs or {})))
65
+ else:
66
+ fn(**(fn_kwargs or {}))
67
+
68
+ return graph.visualize(backend=backend, **kwargs)
69
+
@@ -0,0 +1,12 @@
1
+ """Graph storage driver backends."""
2
+
3
+ from collections import OrderedDict
4
+ from .base import GraphDriver
5
+ from .in_memory import InMemoryGraphDriver
6
+
7
+
8
+ _DRIVER_BACKENDS: OrderedDict[str, type[GraphDriver]] = OrderedDict(
9
+ (
10
+ ("in_memory", InMemoryGraphDriver),
11
+ )
12
+ )
@@ -0,0 +1,309 @@
1
+ """Abstract base class for graph storage drivers."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from ..schema.operation import OpEdge, OpRecord
5
+ from ..schema.session import Session
6
+ from ..schema.variable import Variable
7
+ from typing import (
8
+ Any,
9
+ Literal,
10
+ Self,
11
+ )
12
+
13
+
14
+ class GraphDriver(ABC):
15
+ """Unified storage interface for execution graph data."""
16
+
17
+ @abstractmethod
18
+ def add_node(self, variable: Variable) -> None:
19
+ """Insert a variable node.
20
+
21
+ Args:
22
+ variable (`Variable`):
23
+ The variable to store.
24
+
25
+ Raises:
26
+ `ValueError`:
27
+ If a node with the same full node identifier already exists.
28
+ """
29
+ ...
30
+
31
+ @abstractmethod
32
+ def remove_node(self, full_node_id: str) -> None:
33
+ """Remove a variable node and cascade-delete its connected edges.
34
+
35
+ Args:
36
+ full_node_id (`str`):
37
+ The full node identifier to remove.
38
+ """
39
+ ...
40
+
41
+ @abstractmethod
42
+ def has_node(self, full_node_id: str) -> bool:
43
+ """Check whether a node exists in the graph.
44
+
45
+ Args:
46
+ full_node_id (`str`):
47
+ The full node identifier to check.
48
+
49
+ Returns:
50
+ `bool`:
51
+ ``True`` if the node exists, ``False`` otherwise.
52
+ """
53
+ ...
54
+
55
+ @abstractmethod
56
+ def get_node(self, full_node_id: str) -> Variable | None:
57
+ """Retrieve a single node by its full node identifier.
58
+
59
+ Args:
60
+ full_node_id (`str`):
61
+ The full node identifier to look up.
62
+
63
+ Returns:
64
+ `Variable | None`:
65
+ The variable if found.
66
+ """
67
+ ...
68
+
69
+ @abstractmethod
70
+ def get_nodes_by_name(self, full_name: str) -> list[Variable]:
71
+ """Return all versions of a variable sorted by version.
72
+
73
+ Args:
74
+ full_name (`str`):
75
+ The namespaced variable name or bare variable name when no class name is set.
76
+
77
+ Returns:
78
+ `list[Variable]`:
79
+ All versions, sorted ascending.
80
+ """
81
+ ...
82
+
83
+ @abstractmethod
84
+ def get_latest_node(self, full_name: str) -> Variable | None:
85
+ """Return the latest version of a variable.
86
+
87
+ Args:
88
+ full_name (`str`):
89
+ The namespaced variable name or bare variable name
90
+ when no class name is set.
91
+
92
+ Returns:
93
+ `Variable | None`:
94
+ The latest version if found.
95
+ """
96
+ ...
97
+
98
+ @abstractmethod
99
+ def add_edge(self, edge: OpEdge) -> None:
100
+ """Insert a directed edge.
101
+
102
+ Args:
103
+ edge (`OpEdge`):
104
+ The edge to store.
105
+
106
+ Raises:
107
+ `ValueError`:
108
+ If an edge with the same edge identifier already exists.
109
+ """
110
+ ...
111
+
112
+ @abstractmethod
113
+ def remove_edge(self, edge_id: str) -> None:
114
+ """Remove an edge.
115
+
116
+ Args:
117
+ edge_id (`str`):
118
+ The edge identifier to remove.
119
+ """
120
+ ...
121
+
122
+ @abstractmethod
123
+ def get_edge(self, edge_id: str) -> OpEdge | None:
124
+ """Retrieve a single edge by its identifier.
125
+
126
+ Args:
127
+ edge_id (`str`):
128
+ The edge identifier to look up.
129
+
130
+ Returns:
131
+ `OpEdge | None`:
132
+ The edge if found.
133
+ """
134
+ ...
135
+
136
+ @abstractmethod
137
+ def get_edges_by_node(
138
+ self,
139
+ full_node_id: str,
140
+ direction: Literal["incoming", "outgoing"] = "outgoing",
141
+ ) -> list[OpEdge]:
142
+ """Return edges connected to a node.
143
+
144
+ Args:
145
+ full_node_id (`str`):
146
+ The full node identifier to query.
147
+ direction (`Literal["incoming", "outgoing"]`, defaults to `"outgoing"`):
148
+ The direction of the edges to query.
149
+
150
+ Returns:
151
+ `list[OpEdge]`:
152
+ Matching edges.
153
+ """
154
+ ...
155
+
156
+ @abstractmethod
157
+ def get_edges_by_operation(self, op_id: str) -> list[OpEdge]:
158
+ """Return edges belonging to an operation.
159
+
160
+ Args:
161
+ op_id (`str`):
162
+ The operation identifier to query.
163
+
164
+ Returns:
165
+ `list[OpEdge]`:
166
+ Matching edges.
167
+ """
168
+ ...
169
+
170
+ @abstractmethod
171
+ def add_operation(self, op: OpRecord) -> None:
172
+ """Insert an operation record.
173
+
174
+ Args:
175
+ op (`OpRecord`):
176
+ The operation to store.
177
+
178
+ Raises:
179
+ `ValueError`:
180
+ If an operation with the same operation identifier already exists.
181
+ """
182
+ ...
183
+
184
+ @abstractmethod
185
+ def remove_operation(self, op_id: str) -> None:
186
+ """Remove an operation and cascade-delete its edges.
187
+
188
+ Args:
189
+ op_id (`str`):
190
+ The operation identifier to remove.
191
+ """
192
+ ...
193
+
194
+ @abstractmethod
195
+ def get_operation(self, op_id: str) -> OpRecord | None:
196
+ """Retrieve a single operation by its identifier.
197
+
198
+ Args:
199
+ op_id (`str`):
200
+ The operation identifier to look up.
201
+
202
+ Returns:
203
+ `OpRecord | None`:
204
+ The operation if found.
205
+ """
206
+ ...
207
+
208
+ @abstractmethod
209
+ def add_session(self, session: Session) -> None:
210
+ """Insert a session record.
211
+
212
+ Args:
213
+ session (`Session`):
214
+ The session to store.
215
+
216
+ Raises:
217
+ `ValueError`:
218
+ If a session with the same session identifier already exists.
219
+ """
220
+ ...
221
+
222
+ @abstractmethod
223
+ def remove_session(self, session_id: str) -> None:
224
+ """Remove a session and cascade-delete its operations, nodes, and edges.
225
+
226
+ Args:
227
+ session_id (`str`):
228
+ The session identifier to remove.
229
+ """
230
+ ...
231
+
232
+ @abstractmethod
233
+ def get_session(self, session_id: str) -> Session | None:
234
+ """Retrieve a single session by id.
235
+
236
+ Args:
237
+ session_id (`str`):
238
+ The session identifier to look up.
239
+
240
+ Returns:
241
+ `Session | None`:
242
+ The session if found.
243
+ """
244
+ ...
245
+
246
+ @abstractmethod
247
+ def all_nodes(self) -> list[Variable]:
248
+ """Return every variable in the graph.
249
+
250
+ Returns:
251
+ `list[Variable]`:
252
+ All stored variables.
253
+ """
254
+ ...
255
+
256
+ @abstractmethod
257
+ def all_edges(self) -> list[OpEdge]:
258
+ """Return every edge in the graph.
259
+
260
+ Returns:
261
+ `list[OpEdge]`:
262
+ All stored edges.
263
+ """
264
+ ...
265
+
266
+ @abstractmethod
267
+ def all_operations(self) -> list[OpRecord]:
268
+ """Return every operation in the graph.
269
+
270
+ Returns:
271
+ `list[OpRecord]`:
272
+ All stored operations.
273
+ """
274
+ ...
275
+
276
+ @abstractmethod
277
+ def all_sessions(self) -> list[Session]:
278
+ """Return every session in the graph.
279
+
280
+ Returns:
281
+ `list[Session]`:
282
+ All stored sessions.
283
+ """
284
+ ...
285
+
286
+ @abstractmethod
287
+ def serialize(self) -> dict[str, Any]:
288
+ """Serialize the driver state to a JSON-compatible dictionary.
289
+
290
+ Returns:
291
+ `dict[str, Any]`:
292
+ Serialized state.
293
+ """
294
+ ...
295
+
296
+ @classmethod
297
+ @abstractmethod
298
+ def deserialize(cls, data: dict[str, Any]) -> Self:
299
+ """Reconstruct a driver from serialized state.
300
+
301
+ Args:
302
+ data (`dict[str, Any]`):
303
+ Dictionary produced by corresponding `serialize` method.
304
+
305
+ Returns:
306
+ `Self`:
307
+ A new driver with restored state.
308
+ """
309
+ ...