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.
- smartcomment/__init__.py +49 -0
- smartcomment/analysis/__init__.py +1 -0
- smartcomment/analysis/visualization/__init__.py +43 -0
- smartcomment/analysis/visualization/_utils.py +103 -0
- smartcomment/analysis/visualization/graphviz.py +201 -0
- smartcomment/analysis/visualization/pyvis.py +155 -0
- smartcomment/api/__init__.py +18 -0
- smartcomment/api/_helpers.py +440 -0
- smartcomment/api/_mutation.py +318 -0
- smartcomment/api/public.py +971 -0
- smartcomment/debugging.py +69 -0
- smartcomment/drivers/__init__.py +12 -0
- smartcomment/drivers/base.py +309 -0
- smartcomment/drivers/in_memory.py +323 -0
- smartcomment/identity/__init__.py +6 -0
- smartcomment/identity/registry.py +214 -0
- smartcomment/logging.py +80 -0
- smartcomment/runtime/__init__.py +44 -0
- smartcomment/runtime/context.py +630 -0
- smartcomment/runtime/errors.py +117 -0
- smartcomment/runtime/graph.py +520 -0
- smartcomment/runtime/network.py +1841 -0
- smartcomment/runtime/operation.py +274 -0
- smartcomment/runtime/session.py +133 -0
- smartcomment/runtime/variable.py +237 -0
- smartcomment/schema/__init__.py +1 -0
- smartcomment/schema/base.py +134 -0
- smartcomment/schema/operation.py +96 -0
- smartcomment/schema/session.py +42 -0
- smartcomment/schema/variable.py +126 -0
- smartcomment-0.1.0.dist-info/METADATA +29 -0
- smartcomment-0.1.0.dist-info/RECORD +34 -0
- smartcomment-0.1.0.dist-info/WHEEL +5 -0
- smartcomment-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
...
|