pyigr 2026.9.114__tar.gz

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,91 @@
1
+ Metadata-Version: 2.3
2
+ Name: pyigr
3
+ Version: 2026.9.114
4
+ Summary: Add your description here
5
+ Requires-Dist: networkx
6
+ Requires-Dist: dask ; extra == 'exec-dask'
7
+ Requires-Dist: dask[distributed] ; extra == 'exec-daskdist'
8
+ Requires-Python: >=3.14
9
+ Provides-Extra: exec-dask
10
+ Provides-Extra: exec-daskdist
11
+ Description-Content-Type: text/markdown
12
+
13
+ Python in a Graph
14
+
15
+ # Why?
16
+
17
+ My (author) motivation is to be able to generally describe systems that respond to change.
18
+
19
+
20
+ Related (but not the same):
21
+ - reactive programming libraries:
22
+ Doesn't focus on a 'state'
23
+ - dynamical systems [pathsim](https://docs.pathsim.org/):
24
+ This library doesn't, at the face of it, look like it can do what pathsim does,
25
+ but I think sim descriptions could be mapped somehow.
26
+
27
+ DAG exec:
28
+ - pipefunc
29
+ state with no deps. useful as is but you can farm out tasks as you wish.
30
+
31
+
32
+ # How?
33
+
34
+ Rules/functions are repeatedly applied to a 'state' (dict)
35
+ until there are no more changes.
36
+ w
37
+ ## 1. Specify
38
+
39
+ Rules initializer:
40
+ ```python
41
+ def __init__(self, state: state = {}, *, log: bool = False): ...
42
+ ```
43
+ Function registration:
44
+ ```python
45
+ def register(self, argmap: argmap = {}): ...
46
+ ```
47
+
48
+ ```python
49
+ import state_rules.main as rm
50
+
51
+ r = rm.Rules({'x':1}, log=True)
52
+ @r.register({
53
+ # input
54
+ 'x': 'x' # created by default from func sig if not specified
55
+ # output
56
+ 'return': 'x', # default is funcname.
57
+ # for multiple outputs,
58
+ # can be a dict that updates state: return: { }
59
+ })
60
+ def f(x):
61
+ return x+x # {'x': x+x, 'x+x':x+x } # will be inserted to state
62
+ ```
63
+
64
+ ## 2. Run
65
+
66
+ The run function signature:
67
+ ```python
68
+ def run(self, maxiter=10, *, stopping: Callable[[state], bool] | None = None): ...
69
+ ```
70
+
71
+ ```python
72
+ r.run(5)
73
+ r.log
74
+ ```
75
+ ```python
76
+ [
77
+ Iteration(i=0, state={'x': 1})
78
+ Iteration(i=1, state={'x': 2})
79
+ Iteration(i=2, state={'x': 4})
80
+ Iteration(i=3, state={'x': 8})
81
+ Iteration(i=4, state={'x': 16})
82
+ Iteration(i=5, state={'x': 32})
83
+ ]
84
+ ```
85
+
86
+ # Tips
87
+
88
+ - The state is a (flat) dictionary but you can use a fancy dotted dict if you want more structure.
89
+ Then, use use a function to get at a key.
90
+ - Cache function calls (yourself) as functions will get repeatedly called with the same input.
91
+ - Use `stopping` critereon to early stop before `maxiter`.
@@ -0,0 +1,79 @@
1
+ Python in a Graph
2
+
3
+ # Why?
4
+
5
+ My (author) motivation is to be able to generally describe systems that respond to change.
6
+
7
+
8
+ Related (but not the same):
9
+ - reactive programming libraries:
10
+ Doesn't focus on a 'state'
11
+ - dynamical systems [pathsim](https://docs.pathsim.org/):
12
+ This library doesn't, at the face of it, look like it can do what pathsim does,
13
+ but I think sim descriptions could be mapped somehow.
14
+
15
+ DAG exec:
16
+ - pipefunc
17
+ state with no deps. useful as is but you can farm out tasks as you wish.
18
+
19
+
20
+ # How?
21
+
22
+ Rules/functions are repeatedly applied to a 'state' (dict)
23
+ until there are no more changes.
24
+ w
25
+ ## 1. Specify
26
+
27
+ Rules initializer:
28
+ ```python
29
+ def __init__(self, state: state = {}, *, log: bool = False): ...
30
+ ```
31
+ Function registration:
32
+ ```python
33
+ def register(self, argmap: argmap = {}): ...
34
+ ```
35
+
36
+ ```python
37
+ import state_rules.main as rm
38
+
39
+ r = rm.Rules({'x':1}, log=True)
40
+ @r.register({
41
+ # input
42
+ 'x': 'x' # created by default from func sig if not specified
43
+ # output
44
+ 'return': 'x', # default is funcname.
45
+ # for multiple outputs,
46
+ # can be a dict that updates state: return: { }
47
+ })
48
+ def f(x):
49
+ return x+x # {'x': x+x, 'x+x':x+x } # will be inserted to state
50
+ ```
51
+
52
+ ## 2. Run
53
+
54
+ The run function signature:
55
+ ```python
56
+ def run(self, maxiter=10, *, stopping: Callable[[state], bool] | None = None): ...
57
+ ```
58
+
59
+ ```python
60
+ r.run(5)
61
+ r.log
62
+ ```
63
+ ```python
64
+ [
65
+ Iteration(i=0, state={'x': 1})
66
+ Iteration(i=1, state={'x': 2})
67
+ Iteration(i=2, state={'x': 4})
68
+ Iteration(i=3, state={'x': 8})
69
+ Iteration(i=4, state={'x': 16})
70
+ Iteration(i=5, state={'x': 32})
71
+ ]
72
+ ```
73
+
74
+ # Tips
75
+
76
+ - The state is a (flat) dictionary but you can use a fancy dotted dict if you want more structure.
77
+ Then, use use a function to get at a key.
78
+ - Cache function calls (yourself) as functions will get repeatedly called with the same input.
79
+ - Use `stopping` critereon to early stop before `maxiter`.
@@ -0,0 +1,25 @@
1
+ [project]
2
+ name = "pyigr"
3
+ version = "2026.9.114"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ dependencies = ["networkx"]
8
+
9
+ [project.optional-dependencies]
10
+ exec-dask = ["dask"]
11
+ exec-daskdist = ["dask[distributed]"]
12
+
13
+ [dependency-groups]
14
+ dev = [
15
+ "fire",
16
+ "marimo",
17
+ "pyzmq",
18
+ "ipdb",
19
+ "python-box",
20
+ "wrapt",
21
+ ]
22
+
23
+ [build-system]
24
+ requires = ["uv_build"]
25
+ build-backend = "uv_build"
@@ -0,0 +1,25 @@
1
+ [project]
2
+ name = "pyigr"
3
+ version = "2026.9.114"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ dependencies = [
8
+ 'networkx' # v3 has no deps
9
+ ]
10
+ [project.optional-dependencies]
11
+ exec-dask = ['dask']
12
+ exec-daskdist = ['dask[distributed]']
13
+
14
+ [dependency-groups]
15
+ dev = [
16
+ 'fire',
17
+ "marimo", "pyzmq",'ipdb',
18
+ # examples
19
+ 'python-box', 'wrapt',
20
+ ]
21
+
22
+
23
+ [build-system]
24
+ requires = ["uv_build"]
25
+ build-backend = "uv_build"
@@ -0,0 +1 @@
1
+ from .struct.graph import Rules
@@ -0,0 +1,538 @@
1
+ # this seems like a 'low' level primitive
2
+ # (to build on)
3
+ from typing import Any, Self, Callable, Iterable
4
+
5
+ class types:
6
+ state_key = int | str # hashable?
7
+ state = dict[state_key, Any]
8
+ type var = str
9
+ type argpos = int
10
+ from typing import Any, Literal
11
+ returnkey = Literal['return']
12
+ multioutkeys = tuple | list | set | frozenset
13
+ argmap = dict[var | argpos | returnkey , state_key | multioutkeys ]
14
+
15
+
16
+ class Data:
17
+ def dataclass(c):
18
+ from dataclasses import dataclass
19
+ return dataclass(frozen=True)(c)
20
+
21
+ @dataclass
22
+ class F: # just represents a copy to be 1:1 with Block
23
+ # use wrapt?
24
+ from typing import Callable
25
+ f: Callable
26
+ i: int
27
+ #def __repr__(self) -> str: # cant use in dict key if this is here! why?!
28
+ from functools import cached_property
29
+ @cached_property
30
+ def name(self):
31
+ f = self.f
32
+ _ = repr(f)
33
+ _ = _.strip('"').strip("'")
34
+ if _.startswith('<') and _.endswith('>'):
35
+ mod = (f"{f.__module__}.") if (f.__module__ != '__main__') else ''
36
+ return f"{mod}{f.__name__}"
37
+ else:
38
+ return _
39
+
40
+ from functools import cached_property
41
+ @cached_property
42
+ def parameters(self):
43
+ from inspect import signature
44
+ return signature(self.f).parameters
45
+ # cannot set another name for cached_property
46
+ # params = parameters
47
+ @cached_property
48
+ def params(self): return self.parameters
49
+
50
+ @property
51
+ def signature(self):
52
+ from inspect import signature
53
+ return signature(self.f)
54
+ sig = signature
55
+
56
+
57
+ def __call__(self, *p, **k):
58
+ return self.f(*p, **k)
59
+
60
+
61
+ @property
62
+ def __name__(self): return self.f.__name__
63
+ @property
64
+ def __module__(self): return self.f.__module__
65
+
66
+ @dataclass
67
+ class Arg:
68
+ fidx: int
69
+ name: types.var
70
+
71
+ from collections import namedtuple as _
72
+ IO = _('IO', ['input', 'output'])
73
+ del _
74
+
75
+ ##
76
+ # def uniqe_var. part of uuid is probably unique enough for a repr
77
+ ##
78
+
79
+
80
+ class Rules:
81
+ def __init__(self,
82
+ state: types.state = {}, *,
83
+ name = None,
84
+ log: bool=False):
85
+ self.state = state
86
+ self.funcs = []
87
+ self.log = [] if log is True else False
88
+ self.ops = [] # tracking ops on this. should make it easier to convert
89
+ self.name = name
90
+
91
+ def __repr__(self):
92
+ # the arrow thing is for when this can be viewed as a 'function'
93
+ name = self.name if self.name else self.__class__.__name__
94
+ if self.name:
95
+ _ = map(set, self.io)
96
+ i,o = map(lambda _: '{}' if not _ else repr(_), _)
97
+ _ = f"{name}({i}→{o})"
98
+ return _
99
+ else:
100
+ return repr(super().__init__())
101
+
102
+ def _add_op(self, selfop, **kwargs):
103
+ # chk args
104
+ from copy import deepcopy as cp
105
+ try:
106
+ kwargs = cp(kwargs)
107
+ except: # idk
108
+ from copy import copy
109
+ kwargs = copy(kwargs)
110
+ # check that all needed args are mapped
111
+ from inspect import signature as sig
112
+ sig(selfop).bind(**kwargs)
113
+ self.ops.append(
114
+ (selfop, kwargs)
115
+ )
116
+
117
+ def add_func(self, f, argmap: types.argmap = {}):
118
+ self._add_op(self.add_func, f=f, argmap=argmap)
119
+ f = Data.F(f, len(self.funcs)) #
120
+ if not argmap:
121
+ argmap = {p:p for p in f.parameters}
122
+ # replace pos with kwargs
123
+ for i, p in enumerate(f.parameters):
124
+ if (i in argmap) and (p in argmap):
125
+ raise KeyError(f'conflicting arguments: positional {i} and keyword {p} refer to the same argument.')
126
+ else:
127
+ if p not in argmap:
128
+ argmap[p] = p
129
+ if i in argmap:
130
+ argmap.pop(i)
131
+ for a in argmap:
132
+ if isinstance(a, int):
133
+ raise KeyError(f'positional argument {a} is not mapped.')
134
+ # just try to, to raise exception if issue
135
+ f.signature.bind(**{a:None for a in argmap if a!='return'})
136
+
137
+ if 'return' not in argmap:
138
+ argmap['return'] = f"{f.name}[{f.i}]" # f'{f.__module__}.{f.__name__}()'
139
+
140
+ _ = self.FMap(
141
+ f = f,
142
+ argmap = {fa:sk for fa,sk in argmap.items() if (fa != 'return') },
143
+ return_statekeys = {
144
+ argmap['return'],} if not isinstance(argmap['return'], types.multioutkeys)
145
+ else argmap['return'] ,)
146
+ self.funcs.append(_)
147
+ return _
148
+ register_func = add_func
149
+ class FMap:
150
+ def __init__(self, *, f, argmap, return_statekeys):
151
+ self.f, self.argmap, self.return_statekeys = f, argmap, return_statekeys
152
+ def __repr__(self):
153
+ from types import SimpleNamespace as NS
154
+ _ = NS(f=self.f,
155
+ argmap=self.argmap,
156
+ return_statekeys=self.return_statekeys)
157
+ _ = repr(_)
158
+ _ = _.replace('namespace', self.__class__.__name__)
159
+ return _
160
+ def register(self, argmap: types.argmap = {}, ):
161
+ """decorator """
162
+ if callable(argmap): # case when no (parens) used @register
163
+ f = argmap
164
+ argmap = {} # the default
165
+ self.add_func(f)
166
+ return f
167
+ else:
168
+ def decorator(f, argmap=argmap):
169
+ self.add_func(f, argmap=argmap)
170
+ return f
171
+ return decorator
172
+
173
+
174
+ def data(self: Self):
175
+ rules = self
176
+ for i, f in enumerate(rules.funcs):
177
+ fn = f.f
178
+ oz = f.return_statekeys
179
+ yield Data.Block(i=i,
180
+ f = fn,
181
+ iz = frozenset(f.argmap.keys()),
182
+ oz = frozenset(oz))
183
+ for arg, statekey in f.argmap.items():
184
+ yield Data.VarMap(i = i,
185
+ f = fn,
186
+ arg = arg,
187
+ state_key = statekey
188
+ )
189
+ for k,v in rules.state.items():
190
+ yield Data.State(k=k,v=v)
191
+
192
+ def graph(self: Self)-> Data.Graph:
193
+ """networkx-compatible data structure"""
194
+ # intent to be 'data'/serialization
195
+ trm = Data.Graph.terms
196
+ Node = Data.Graph.Node
197
+ Graph = Data.Graph
198
+ Arg = Data.Arg
199
+ def nodes(rules=self):
200
+ self = rules
201
+ for k,v in self.state.items():
202
+ yield k,\
203
+ {trm.types.type: trm.types.state.state,
204
+ trm.label: str(k),
205
+ trm.types.state.value: v}
206
+ for fi, fb in enumerate(self.funcs):
207
+ yield Node(fb.f, trm.types.f.function),\
208
+ {trm.types.type: trm.types.f.function,
209
+ trm.label: fb.f.name,
210
+ trm.types.f.i: fi,
211
+ trm.value: fb.f.f,
212
+ }
213
+ # inputs
214
+ for farg, statekey in fb.argmap.items():
215
+ if statekey not in self.state:
216
+ yield statekey,\
217
+ {trm.types.type: trm.types.state.state,
218
+ trm.label: str(statekey) }
219
+ yield Node( Arg(fi, farg), trm.types.f.arg),\
220
+ {trm.types.type: trm.types.f.arg,
221
+ trm.label: str(farg),
222
+ trm.value: farg,
223
+ }
224
+ # outputs
225
+ for rsk in fb.return_statekeys:
226
+ if rsk not in self.state:
227
+ yield rsk,\
228
+ {trm.types.type: trm.types.state.state}
229
+
230
+
231
+ def edges(rules=self):
232
+ ed = {} # edge dict
233
+ def add(src, dst, attribs={}, ed=ed):
234
+ if src not in ed:
235
+ ed[src] = {}
236
+ #assert(dst not in ed[src])
237
+ ed[src][dst] = attribs
238
+ return ed
239
+
240
+ for fi, fb in enumerate(rules.funcs):
241
+ # state -> arg
242
+ for farg, statekey in fb.argmap.items():
243
+ src = statekey
244
+ dst = Node(Arg(fi, farg), trm.types.f. arg)
245
+ add(src, dst,
246
+ {trm.types.type: trm.types.f.binding.input,
247
+ trm.types.f.function: fb.f.f },)
248
+ # func -> state
249
+ for rsk in fb.return_statekeys:
250
+ src = Node(fb.f, trm.types.f. function)
251
+ dst = rsk
252
+ add(src, dst,
253
+ {trm.types.type: trm.types.f.binding.output,
254
+ trm.types.f.function: fb.f.f})
255
+ return ed
256
+
257
+ return Graph(
258
+ nodes={n:a for n,a in nodes()},
259
+ edges=edges())
260
+ #data = graph
261
+
262
+
263
+
264
+ def __add__(self, other: Self): return self.add(other)
265
+ def add(self, other):
266
+ self._add_op(self.add, other=other)
267
+
268
+ common = frozenset(self.state) & frozenset(other.state)
269
+ for c in common:
270
+ if self.state[c] != other.state[c]:
271
+ raise ValueError(f'state clash for key {c}: {self.state[c]}!={other.state[c]}')
272
+ from copy import deepcopy as copy
273
+ new = copy(self)
274
+ new.log = [] # clear this though
275
+ new.ops = []
276
+ new.funcs.extend(other.funcs)
277
+ new.state.update(other.state)
278
+ return new
279
+
280
+ # running
281
+
282
+ def _apply(self, state: types.state):
283
+ s = state
284
+ for fm in self.funcs:
285
+ try:# can be binded?
286
+ _ = {a:s[sk] for a,sk in fm.argmap.items() }
287
+ except KeyError:
288
+ continue
289
+ _ = fm.f.f(**_) # take the inner f for performance
290
+ # special case
291
+ # the intent is to not output
292
+ # could skip func app but could be a useful thing
293
+ if not fm.return_statekeys:
294
+ continue
295
+ elif isinstance(_, dict):
296
+ for sk in fm.return_statekeys:
297
+ assert(sk in _)
298
+ s.update(_)
299
+ else: # make one
300
+ _ = dict.fromkeys(fm.return_statekeys, _)
301
+ s.update(_)
302
+ yield fm, s
303
+
304
+ def _chk_binding(self):
305
+ returns = set()
306
+ for fm in self.funcs:
307
+ returns.update(fm.return_statekeys)
308
+ for fm in self.funcs:
309
+ for a,sk in fm.argmap.items():
310
+ if (sk in self.state) or (sk in returns): ...
311
+ else: raise KeyError(f'{fm.f.name}{a} will not be bound.')
312
+ def _chk_flow(self):
313
+ # no circles
314
+ # chk distinct inputs outputs
315
+ ...
316
+
317
+
318
+ @property
319
+ def io(self): # io?
320
+ # self._chk_binding() need to?
321
+ _ = (fb.argmap.values() for fb in self.funcs)
322
+ fins = []
323
+ for os in _: fins.extend(os)
324
+ fins = frozenset(fins)
325
+ _ = (fb.return_statekeys for fb in self.funcs)
326
+ fouts = []
327
+ for iz in _: fouts.extend(iz)
328
+ fouts = frozenset(fouts)
329
+ return Data.IO(
330
+ input=fins - fouts,
331
+ output = fouts - fins) # neat
332
+
333
+
334
+ def run(self,
335
+ maxiter = 10, *,
336
+ stopping: Callable[[types.state], bool] | None = None,
337
+ check: set|list|tuple|frozenset = ('binding',), # 'flow'),
338
+ print_log:bool = False, # TODO: print i
339
+ cache=True, # starting to think this a good default TODO
340
+ ):
341
+ for chk in check: getattr(self, '_chk_'+chk)()
342
+ i = self.i = 0
343
+ from types import SimpleNamespace as NS
344
+ class Iteration(NS): pass
345
+
346
+ from copy import deepcopy as copy
347
+ # shallow vs deep copy? deep more general. shallow for simple objects.
348
+ # maybe no performance loss if state is shallow.
349
+ if self.log is not False:
350
+ self.log.append(Iteration(i=i, state=copy(self.state)))
351
+ if stopping is not None:
352
+ if stopping(self.state): return self.state
353
+
354
+ # this could just use python's setters and getters.
355
+ # but i think there's more control with the below
356
+ while True:
357
+ if i >= maxiter:
358
+ from warnings import warn
359
+ warn('Reached iteration limit!')
360
+ break
361
+ # alt. is to 'old*hash*' == new*hash* to potentially avoid copying
362
+ oldstate = copy(self.state)
363
+ s = self.state
364
+ for f,s in self._apply(self.state):
365
+ if self.log is not False:
366
+ self.log.append(
367
+ Iteration(i=i+1,
368
+ state=copy(s),
369
+ rule=f,) )
370
+ if stopping is not None:
371
+ if stopping(s): return s
372
+ self.state = newstate = s
373
+
374
+ if newstate == oldstate: # b/c of this, have to copy
375
+ break
376
+ else:
377
+ i = i+1
378
+ newstate = oldstate
379
+ continue
380
+
381
+ return self.state
382
+
383
+ def __call__(self, *,
384
+ _maxiter=999, _stopping=None,
385
+ _check = {'binding', 'flow' },
386
+ _print_log=False,
387
+ **state) -> types.state:
388
+ """treat the machine as a function:
389
+ Keyword arguments will update the state.
390
+ If a dictionary with the key 'state' is passed,
391
+ its value will update the state.
392
+ (So to have a 'state' key with a dictionary, you can nest it: (state={'x': 3, 'state': 5})
393
+ """
394
+ # should cache functions?
395
+ if 'state' in state:
396
+ assert(isinstance(state['state'], types.state))
397
+ self.state.update(state.pop('state'))
398
+ else:
399
+ self.state.update(**state)
400
+ _ = self.run(
401
+ maxiter=_maxiter, stopping=_stopping,
402
+ check=_check,
403
+ print_log=_print_log)
404
+ return self.state
405
+
406
+
407
+
408
+ class Graph:
409
+ """
410
+ networkx compatible data
411
+ """
412
+ # but keep the state values separate
413
+
414
+ class types:
415
+ from dataclasses import dataclass
416
+ @dataclass(frozen=True)
417
+ class Node: # need to uniquify
418
+ from typing import Any
419
+ obj: Any
420
+ type: str # != 'state' for convenience
421
+
422
+
423
+ class terms:
424
+ class types:
425
+ type = 'type'
426
+ class f:
427
+ function = 'function'
428
+ i = 'i' # func idx
429
+ arg = 'arg'
430
+ class binding:
431
+ binding = 'binding'
432
+ input = 'input'
433
+ output = 'output'
434
+ class state:
435
+ state = 'state'
436
+ value = 'value'
437
+ value = 'value'
438
+ label = 'label'
439
+
440
+ def __init__(self, rules: Rules):
441
+ self.rules = rules
442
+ self.graph = self.types.DiGraph()
443
+ self.nodes = self.graph.nodes
444
+ self.edges = self.graph.edges
445
+
446
+
447
+ @property
448
+ def state(self) -> dict:
449
+ return {
450
+ s:self.nodes[self.terms.types.state.value]
451
+ for s in self.nodes
452
+ if self.terms.types.state.value in self.nodes }
453
+
454
+
455
+ def add_state(self, k, v: Any=None):
456
+ attrs = {self.terms.types.state.value: v} if v is not None
457
+ self.graph.add_node(k, )
458
+ return k, attrs
459
+
460
+
461
+ def add_argmap(self, fm: Rules.FMap):
462
+ ...
463
+
464
+
465
+ def xgraph(self: Self)-> Data.Graph:
466
+ """networkx-compatible data structure"""
467
+ # intent to be 'data'/serialization
468
+ trm = Data.Graph.terms
469
+ Node = Data.Graph.Node
470
+ Graph = Data.Graph
471
+ Arg = Data.Arg
472
+ def nodes(rules=self):
473
+ self = rules
474
+ for k,v in self.state.items():
475
+ yield k,\
476
+ {trm.types.type: trm.types.state.state,
477
+ trm.label: str(k),
478
+ trm.types.state.value: v}
479
+ for fi, fb in enumerate(self.funcs):
480
+ yield Node(fb.f, trm.types.f.function),\
481
+ {trm.types.type: trm.types.f.function,
482
+ trm.label: fb.f.name,
483
+ trm.types.f.i: fi,
484
+ trm.value: fb.f.f,
485
+ }
486
+ # inputs
487
+ for farg, statekey in fb.argmap.items():
488
+ if statekey not in self.state:
489
+ yield statekey,\
490
+ {trm.types.type: trm.types.state.state,
491
+ trm.label: str(statekey) }
492
+ yield Node( Arg(fi, farg), trm.types.f.arg),\
493
+ {trm.types.type: trm.types.f.arg,
494
+ trm.label: str(farg),
495
+ trm.value: farg,
496
+ }
497
+ # outputs
498
+ for rsk in fb.return_statekeys:
499
+ if rsk not in self.state:
500
+ yield rsk,\
501
+ {trm.types.type: trm.types.state.state}
502
+
503
+
504
+ def edges(rules=self):
505
+ ed = {} # edge dict
506
+ def add(src, dst, attribs={}, ed=ed):
507
+ if src not in ed:
508
+ ed[src] = {}
509
+ #assert(dst not in ed[src])
510
+ ed[src][dst] = attribs
511
+ return ed
512
+
513
+ for fi, fb in enumerate(rules.funcs):
514
+ # state -> arg
515
+ for farg, statekey in fb.argmap.items():
516
+ src = statekey
517
+ dst = Node(Arg(fi, farg), trm.types.f. arg)
518
+ add(src, dst,
519
+ {trm.types.type: trm.types.f.binding.input,
520
+ trm.types.f.function: fb.f.f },)
521
+ # func -> state
522
+ for rsk in fb.return_statekeys:
523
+ src = Node(fb.f, trm.types.f. function)
524
+ dst = rsk
525
+ add(src, dst,
526
+ {trm.types.type: trm.types.f.binding.output,
527
+ trm.types.f.function: fb.f.f})
528
+ return ed
529
+
530
+ return Graph(
531
+ nodes={n:a for n,a in nodes()},
532
+ edges=edges())
533
+
534
+ # class Running
535
+ # run
536
+
537
+ # class Tasks:
538
+ # for topo sort.
File without changes
@@ -0,0 +1,44 @@
1
+ # meant to create more executable forms
2
+ from ..rules import Rules as _
3
+ class Rules(_):
4
+
5
+ def __call__(self, **k):
6
+ self.state.update(k)
7
+
8
+ def set_return(self):
9
+ allouts
10
+
11
+
12
+
13
+ def tasks(rules: Rules):
14
+ from dask.task_spec import Task, DataNode, TaskRef
15
+ # why none?
16
+ _ = {k: DataNode(None, v) for k,v in rules.state.items()}
17
+ def order_args(f, argmap):
18
+ from inspect import signature as sig
19
+ _ = sig(f).parameters
20
+ _ = (argmap[p] for p in _)
21
+ _ = map(TaskRef, _)
22
+ _ = tuple(_)
23
+ return _
24
+ # assuming not split
25
+ _ = _ | \
26
+ {fm.return_statekey: Task(i, fm.f, *order_args(fm.f, fm.argmap) ) for i,fm in enumerate(rules.funcs) }
27
+
28
+ def split_output(): ...
29
+
30
+ return _
31
+
32
+
33
+
34
+ def test(rules: Rules):
35
+ ts = tasks(rules)
36
+ #_ = Task(None, f, 3,4 )
37
+ #from dask.distributed import Client
38
+ from dask.threaded import get
39
+ #c = Client(processes=False)
40
+ return get(ts, ['x', 'y', 'f' ] )
41
+ #return c.get(_, 'x' )
42
+ _ = _()
43
+ return _
44
+ #for k,v
File without changes
@@ -0,0 +1 @@
1
+ meant for exec targets
@@ -0,0 +1,3 @@
1
+ # just print out vars
2
+ # typing from graph
3
+
File without changes
File without changes
@@ -0,0 +1,120 @@
1
+ from ..rules import Rules as _Rules, Data as data
2
+
3
+ class Rules(_Rules):
4
+ def mermaid(self, log_idx=-1):
5
+ return mermaid(self, log_idx=log_idx)
6
+
7
+ def _display_(self):
8
+ _ = mermaid(self)
9
+ from marimo import mermaid as md
10
+ _ = md(_)
11
+ return _
12
+
13
+
14
+ def networkx(rules: _Rules):
15
+ from networkx import DiGraph
16
+ _ = DiGraph()
17
+ rd = rules.graph()
18
+ _.add_nodes_from(rd.nodes.items())
19
+ # cant add directly??
20
+ #_.add_edges_from(rd.edges)
21
+ for s in rd.edges:
22
+ for d in rd.edges[s]:
23
+ _.add_edge(s, d, **rd.edges[s][d] if rd.edges[s][d] else {})
24
+ return _
25
+
26
+
27
+ def frepr(f):
28
+ try:
29
+ f = f.f
30
+ except:
31
+ f = f
32
+ _ = repr(f)
33
+ _ = _.strip('"').strip("'")
34
+ if _.startswith('<') and _.endswith('>'):
35
+ mod = (f"{f.__module__}.") if (f.__module__ != '__main__') else ''
36
+ return f"{mod}{f.__name__}"
37
+ else:
38
+ return _
39
+
40
+
41
+ def value_repr(v):
42
+ _ = str(v)
43
+ _ = _.strip('"').strip('"')
44
+ ml = 20
45
+ if len(_)>ml:
46
+ _ = _[:ml]
47
+ _ = _+'...'
48
+ return _
49
+
50
+
51
+ def mermaid(rules: Rules, log_idx=-1):
52
+ if log_idx == -1:
53
+ state = rules.state
54
+ else:
55
+ state = rules.log[log_idx].state
56
+ # really wanted svelte flow
57
+ #---
58
+ # title: repr(rules)
59
+ # ---
60
+ # flowchart TD
61
+ # input
62
+ # A((A)) -->|i1|f
63
+ # A((A)) -->|i2|f
64
+ # output
65
+ # f -->o1((o1))
66
+ # f -->o2((o2))
67
+ from functools import cache
68
+ @cache
69
+ def part(n, type, id=id, ):
70
+ v = val
71
+ if type == 'f':
72
+ return f"{type}{id(n)}[\\{ frepr(n) }/]"
73
+ if type in {'s', 'o'}:
74
+ return f'so{id(n)}@{{shape: stadium, label: "{frepr(n)+val(n, type, )}" }}'
75
+ if type == 'i':
76
+ return f"|{repr(n).strip('"').strip("'")}|"
77
+ raise Exception('not handled')
78
+
79
+ def val(n, type, ):
80
+ if type =='f':
81
+ return ''
82
+ elif n not in state:
83
+ return ''
84
+ v = state[n]
85
+ v = value_repr(v)
86
+ v = v.replace("'", "\\'")
87
+ v = '='+v
88
+ return v
89
+
90
+ def parts(r: data.Block | data.VarMap | _Rules):
91
+ if isinstance(r, data.Block):
92
+ block = r
93
+ for o in block.oz:
94
+ yield f"{part((block.f), 'f')}-->{part(o, 'o')}"
95
+ if not block.iz:
96
+ yield part((block.f), 'f')
97
+ elif isinstance(r, data.VarMap):
98
+ vm = r
99
+ yield f"{part(vm.state_key, 's')}-->{part(vm.arg, 'i')}{part(vm.f, 'f')}"
100
+ elif isinstance(r, data.State):
101
+ if not rules.funcs:
102
+ yield f"{part(r.k, 's')}"
103
+ else:
104
+ assert(isinstance(r, _Rules))
105
+ for d in r.data():
106
+ yield from parts(d)
107
+
108
+ _ = parts(rules)
109
+ _ = '\n'.join(_)
110
+ _ = f"""
111
+ ---
112
+ title: {repr(rules).strip('"').strip("'").strip('<').strip('>')}
113
+ ---
114
+ flowchart TD
115
+ {_}
116
+ """
117
+ _ = (l.strip() for l in _.split('\n') if l.strip())
118
+ _ = '\n'.join(_)
119
+ return _
120
+
File without changes
@@ -0,0 +1,109 @@
1
+ from ..rules import Rules
2
+
3
+
4
+ def mermaid(rules: Rules, log_idx=-1):
5
+ if log_idx == -1 and (len(rules.log)==0):
6
+ state = rules.state
7
+ else:
8
+ state = rules.log[log_idx].state
9
+ # really wanted svelte flow
10
+ #---
11
+ # title: repr(rules)
12
+ # ---
13
+ # flowchart TD
14
+ # input
15
+ # A((A)) -->|i1|f
16
+ # A((A)) -->|i2|f
17
+ # output
18
+ # f -->o1((o1))
19
+ # f -->o2((o2))
20
+
21
+ def repr(o):
22
+ for n in {'name', 'label', }:
23
+ if hasattr(o, n):
24
+ return getattr(o, n)
25
+ return o.__repr__()
26
+
27
+
28
+ from ..rules import Data
29
+ terms = Data.Graph.terms
30
+ from functools import cache
31
+ @cache
32
+ def part(n, type, id=id, label=None ):
33
+ if label is None:
34
+ label = repr(n).strip('"').strip("'")
35
+ if type == terms.types.f.function:
36
+ return f'{type}{id(n)}[\\"{ label }"/]'
37
+ if type in {terms.types.state.state, }:
38
+ return f'{type}{id(n)}@{{shape: stadium, label: "{label+val(n, type, )}" }}'
39
+ if type == terms.types.f.arg :
40
+ return f'{type}{id(n)}@{{shape: flip-tri, label: "{label }" }}'
41
+ raise Exception('not handled')
42
+
43
+ def val(n, type, ):
44
+ if type == terms.types.f.function:
45
+ return ''
46
+ elif n not in state:
47
+ return ''
48
+ v = state[n]
49
+ v = value_repr(v)
50
+ v = v.replace("'", "\\'")
51
+ v = '='+v
52
+ return v
53
+
54
+
55
+ g = networkx(rules)
56
+ def data():
57
+ from types import SimpleNamespace as ns
58
+ for ne, d in g.nodes.items():
59
+ yield ns(t='n', ne=ne, d=d) #
60
+ for ne, d in g.edges.items():
61
+ yield ns(t='e', ne=ne, d=d)
62
+
63
+ def parts():
64
+ def type(n):
65
+ _ = g.nodes[n][terms.types.type]
66
+ return _
67
+
68
+ for d in data():
69
+ # nodes
70
+ if d.t == 'n':
71
+ yield part(d.ne, d.d['type'] )
72
+ else:# edges
73
+ assert(d.t == 'e')
74
+ src, dst = d.ne
75
+ st, dt = type(src), type(dst)
76
+ if d.d[terms.types.type] == terms.types.f.binding.input:
77
+ yield f"{part(src, st)}-->{part(dst, dt )}"
78
+ #if d.d[terms.types.type] == terms.types.f.binding.input:
79
+ # yield f"{part(src, st)}-->"{part(dst, dt )}"
80
+
81
+ # if isinstance(r, data.Block):
82
+ # block = r
83
+ # for o in block.oz:
84
+ # yield f"{part((block.f), 'f')}-->{part(o, 'o')}"
85
+ # if not block.iz:
86
+ # yield part((block.f), 'f')
87
+ # elif isinstance(r, data.VarMap):
88
+ # vm = r
89
+ # yield f"{part(vm.state_key, 's')}-->{part(vm.arg, 'i')}{part(vm.f, 'f')}"
90
+ # elif isinstance(r, data.State):
91
+ # if not rules.funcs:
92
+ # yield f"{part(r.k, 's')}"
93
+ # else:
94
+ # assert(isinstance(r, _Rules))
95
+ # for d in r.data():
96
+ # yield from parts(d)
97
+
98
+ _ = parts()
99
+ _ = '\n'.join(_)
100
+ _ = f"""
101
+ ---
102
+ title: {repr(rules).strip('"').strip("'").strip('<').strip('>')}
103
+ ---
104
+ flowchart TD
105
+ {_}
106
+ """
107
+ _ = (l.strip() for l in _.split('\n') if l.strip())
108
+ _ = '\n'.join(_)
109
+ return _