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.
- pyigr-2026.9.114/PKG-INFO +91 -0
- pyigr-2026.9.114/README.md +79 -0
- pyigr-2026.9.114/pyproject.toml +25 -0
- pyigr-2026.9.114/pyproject.toml.orig +25 -0
- pyigr-2026.9.114/src/pyigr/__init__.py +1 -0
- pyigr-2026.9.114/src/pyigr/connect.py +538 -0
- pyigr-2026.9.114/src/pyigr/exec/__init__.py +0 -0
- pyigr-2026.9.114/src/pyigr/exec/dask.py +44 -0
- pyigr-2026.9.114/src/pyigr/exec/marimo.py +0 -0
- pyigr-2026.9.114/src/pyigr/exec/readme.md +1 -0
- pyigr-2026.9.114/src/pyigr/exec/state.py +3 -0
- pyigr-2026.9.114/src/pyigr/struct/__init__.py +0 -0
- pyigr-2026.9.114/src/pyigr/struct/composition.py +0 -0
- pyigr-2026.9.114/src/pyigr/struct/graph.py +120 -0
- pyigr-2026.9.114/src/pyigr/vis/__init__.py +0 -0
- pyigr-2026.9.114/src/pyigr/vis/mermaid.py +109 -0
|
@@ -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
|
|
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 _
|