mycorrhizal 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.
- mycorrhizal/__init__.py +3 -0
- mycorrhizal/common/__init__.py +68 -0
- mycorrhizal/common/interface_builder.py +203 -0
- mycorrhizal/common/interfaces.py +412 -0
- mycorrhizal/common/timebase.py +99 -0
- mycorrhizal/common/wrappers.py +532 -0
- mycorrhizal/enoki/__init__.py +0 -0
- mycorrhizal/enoki/core.py +1545 -0
- mycorrhizal/enoki/testing_utils.py +529 -0
- mycorrhizal/enoki/util.py +220 -0
- mycorrhizal/hypha/__init__.py +0 -0
- mycorrhizal/hypha/core/__init__.py +107 -0
- mycorrhizal/hypha/core/builder.py +404 -0
- mycorrhizal/hypha/core/runtime.py +890 -0
- mycorrhizal/hypha/core/specs.py +234 -0
- mycorrhizal/hypha/util.py +38 -0
- mycorrhizal/rhizomorph/README.md +220 -0
- mycorrhizal/rhizomorph/__init__.py +0 -0
- mycorrhizal/rhizomorph/core.py +1729 -0
- mycorrhizal/rhizomorph/util.py +45 -0
- mycorrhizal/spores/__init__.py +124 -0
- mycorrhizal/spores/cache.py +208 -0
- mycorrhizal/spores/core.py +419 -0
- mycorrhizal/spores/dsl/__init__.py +48 -0
- mycorrhizal/spores/dsl/enoki.py +514 -0
- mycorrhizal/spores/dsl/hypha.py +399 -0
- mycorrhizal/spores/dsl/rhizomorph.py +351 -0
- mycorrhizal/spores/encoder/__init__.py +11 -0
- mycorrhizal/spores/encoder/base.py +42 -0
- mycorrhizal/spores/encoder/json.py +159 -0
- mycorrhizal/spores/extraction.py +484 -0
- mycorrhizal/spores/models.py +288 -0
- mycorrhizal/spores/transport/__init__.py +10 -0
- mycorrhizal/spores/transport/base.py +46 -0
- mycorrhizal-0.1.0.dist-info/METADATA +198 -0
- mycorrhizal-0.1.0.dist-info/RECORD +37 -0
- mycorrhizal-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Enoki State Machine Utilities
|
|
4
|
+
|
|
5
|
+
Utility functions for FSM operations like Mermaid diagram generation.
|
|
6
|
+
"""
|
|
7
|
+
from typing import Dict, List, Set, Optional, Union
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class _TransitionInfo:
|
|
13
|
+
"""Information about a transition for mermaid diagram"""
|
|
14
|
+
label: str
|
|
15
|
+
target: str
|
|
16
|
+
transition_type: str # "normal", "push", "pop", "again", "retry", etc.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def to_mermaid(fsm) -> str:
|
|
20
|
+
"""
|
|
21
|
+
Generate Mermaid diagram from a StateMachine instance.
|
|
22
|
+
|
|
23
|
+
This creates a flowchart showing all states and their transitions,
|
|
24
|
+
including special handling for Push/Pop transitions.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
fsm: A StateMachine instance (from mycorrhizal.enoki.core)
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Mermaid diagram as a string
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
```python
|
|
34
|
+
from mycorrhizal.enoki.core import StateMachine
|
|
35
|
+
from mycorrhizal.enoki.util import to_mermaid
|
|
36
|
+
|
|
37
|
+
fsm = StateMachine(initial_state=MyState)
|
|
38
|
+
await fsm.initialize()
|
|
39
|
+
|
|
40
|
+
# Generate diagram
|
|
41
|
+
mermaid_diagram = to_mermaid(fsm)
|
|
42
|
+
print(mermaid_diagram)
|
|
43
|
+
```
|
|
44
|
+
"""
|
|
45
|
+
# Import here to avoid circular imports
|
|
46
|
+
from mycorrhizal.enoki.core import (
|
|
47
|
+
StateSpec,
|
|
48
|
+
StateRef,
|
|
49
|
+
LabeledTransition,
|
|
50
|
+
Push,
|
|
51
|
+
Pop,
|
|
52
|
+
Again,
|
|
53
|
+
Retry,
|
|
54
|
+
Unhandled,
|
|
55
|
+
Repeat,
|
|
56
|
+
Restart,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
lines: List[str] = ["flowchart TD"]
|
|
60
|
+
node_ids: Dict[str, str] = {}
|
|
61
|
+
counter = 0
|
|
62
|
+
|
|
63
|
+
# Get all validated states from the registry
|
|
64
|
+
validation_result = fsm.registry.validate_all_states(
|
|
65
|
+
fsm.initial_state, fsm.error_state
|
|
66
|
+
)
|
|
67
|
+
state_names: Set[str] = validation_result.discovered_states
|
|
68
|
+
|
|
69
|
+
def nid(state_name: str) -> str:
|
|
70
|
+
"""Get or create a node ID for a state"""
|
|
71
|
+
nonlocal counter
|
|
72
|
+
if state_name in node_ids:
|
|
73
|
+
return node_ids[state_name]
|
|
74
|
+
counter += 1
|
|
75
|
+
node_ids[state_name] = f"S{counter}"
|
|
76
|
+
return node_ids[state_name]
|
|
77
|
+
|
|
78
|
+
# Add initial state marker
|
|
79
|
+
initial_name = fsm.initial_state.name if isinstance(fsm.initial_state, StateSpec) else str(fsm.initial_state)
|
|
80
|
+
lines.append(f" start((start)) --> {nid(initial_name)}")
|
|
81
|
+
|
|
82
|
+
# Add error state if exists
|
|
83
|
+
if fsm.error_state:
|
|
84
|
+
error_name = fsm.error_state.name if isinstance(fsm.error_state, StateSpec) else str(fsm.error_state)
|
|
85
|
+
lines.append(f" {nid(error_name)}(((ERROR)))")
|
|
86
|
+
|
|
87
|
+
# Collect all transitions
|
|
88
|
+
transitions: Dict[str, List[_TransitionInfo]] = {}
|
|
89
|
+
|
|
90
|
+
for state_name in state_names:
|
|
91
|
+
state = fsm.registry.resolve_state(state_name)
|
|
92
|
+
if not state:
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
transitions[state_name] = []
|
|
96
|
+
|
|
97
|
+
# Get the transition list
|
|
98
|
+
raw_transitions = state.get_transitions()
|
|
99
|
+
|
|
100
|
+
# Handle single transition
|
|
101
|
+
if isinstance(raw_transitions, (Again, Retry, Unhandled, Repeat, Restart, Pop)):
|
|
102
|
+
raw_transitions = [raw_transitions]
|
|
103
|
+
elif not isinstance(raw_transitions, list):
|
|
104
|
+
raw_transitions = list(raw_transitions) if raw_transitions else []
|
|
105
|
+
|
|
106
|
+
for trans in raw_transitions:
|
|
107
|
+
match trans:
|
|
108
|
+
case LabeledTransition(label, target):
|
|
109
|
+
match target:
|
|
110
|
+
case Again():
|
|
111
|
+
transitions[state_name].append(
|
|
112
|
+
_TransitionInfo(label.name, state_name, "again")
|
|
113
|
+
)
|
|
114
|
+
case Retry():
|
|
115
|
+
transitions[state_name].append(
|
|
116
|
+
_TransitionInfo(label.name, state_name, "retry")
|
|
117
|
+
)
|
|
118
|
+
case Unhandled():
|
|
119
|
+
transitions[state_name].append(
|
|
120
|
+
_TransitionInfo(label.name, state_name, "unhandled")
|
|
121
|
+
)
|
|
122
|
+
case Repeat():
|
|
123
|
+
transitions[state_name].append(
|
|
124
|
+
_TransitionInfo(label.name, state_name, "repeat")
|
|
125
|
+
)
|
|
126
|
+
case Restart():
|
|
127
|
+
transitions[state_name].append(
|
|
128
|
+
_TransitionInfo(label.name, state_name, "restart")
|
|
129
|
+
)
|
|
130
|
+
case Pop():
|
|
131
|
+
transitions[state_name].append(
|
|
132
|
+
_TransitionInfo(label.name, "__pop__", "pop")
|
|
133
|
+
)
|
|
134
|
+
case Push() as p:
|
|
135
|
+
# Push transitions - show all states being pushed
|
|
136
|
+
for push_state in p.push_states:
|
|
137
|
+
push_name = push_state.name if isinstance(push_state, StateSpec) else str(push_state)
|
|
138
|
+
transitions[state_name].append(
|
|
139
|
+
_TransitionInfo(f"{label.name} → push", push_name, "push")
|
|
140
|
+
)
|
|
141
|
+
case StateSpec() as target_state:
|
|
142
|
+
transitions[state_name].append(
|
|
143
|
+
_TransitionInfo(label.name, target_state.name, "normal")
|
|
144
|
+
)
|
|
145
|
+
case StateRef() as ref:
|
|
146
|
+
transitions[state_name].append(
|
|
147
|
+
_TransitionInfo(label.name, ref.state, "normal")
|
|
148
|
+
)
|
|
149
|
+
case _:
|
|
150
|
+
# Unknown transition type
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
case Again():
|
|
154
|
+
transitions[state_name].append(
|
|
155
|
+
_TransitionInfo("", state_name, "again")
|
|
156
|
+
)
|
|
157
|
+
case Retry():
|
|
158
|
+
transitions[state_name].append(
|
|
159
|
+
_TransitionInfo("", state_name, "retry")
|
|
160
|
+
)
|
|
161
|
+
case Unhandled():
|
|
162
|
+
transitions[state_name].append(
|
|
163
|
+
_TransitionInfo("", state_name, "unhandled")
|
|
164
|
+
)
|
|
165
|
+
case Repeat():
|
|
166
|
+
transitions[state_name].append(
|
|
167
|
+
_TransitionInfo("", state_name, "repeat")
|
|
168
|
+
)
|
|
169
|
+
case Restart():
|
|
170
|
+
transitions[state_name].append(
|
|
171
|
+
_TransitionInfo("", state_name, "restart")
|
|
172
|
+
)
|
|
173
|
+
case Pop():
|
|
174
|
+
transitions[state_name].append(
|
|
175
|
+
_TransitionInfo("", "__pop__", "pop")
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Generate nodes and edges
|
|
179
|
+
for state_name in state_names:
|
|
180
|
+
state = fsm.registry.resolve_state(state_name)
|
|
181
|
+
if not state:
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
state_id = nid(state_name)
|
|
185
|
+
|
|
186
|
+
# Determine node shape based on state properties
|
|
187
|
+
if state.config.terminal:
|
|
188
|
+
shape = f'[{state_name} (**terminal**)]'
|
|
189
|
+
elif state_name == initial_name:
|
|
190
|
+
shape = f'[{state_name}]'
|
|
191
|
+
elif fsm.error_state and state_name == (fsm.error_state.name if isinstance(fsm.error_state, StateSpec) else str(fsm.error_state)):
|
|
192
|
+
shape = f'[{state_name}]'
|
|
193
|
+
else:
|
|
194
|
+
shape = f'[{state_name}]'
|
|
195
|
+
|
|
196
|
+
lines.append(f" {state_id}{shape}")
|
|
197
|
+
|
|
198
|
+
# Add transitions
|
|
199
|
+
for trans in transitions.get(state_name, []):
|
|
200
|
+
if trans.transition_type in ("again", "retry", "repeat", "restart", "unhandled"):
|
|
201
|
+
# Self-loop
|
|
202
|
+
label = f'"{trans.transition_type}"' if trans.transition_type else ""
|
|
203
|
+
lines.append(f" {state_id} -->|{label}| {state_id}")
|
|
204
|
+
elif trans.transition_type == "pop":
|
|
205
|
+
# Pop goes to a special pop node
|
|
206
|
+
lines.append(f" {state_id} -->|\"{trans.label or 'pop'}\"| pop((pop))")
|
|
207
|
+
elif trans.transition_type == "push":
|
|
208
|
+
# Push transition
|
|
209
|
+
target_id = nid(trans.target)
|
|
210
|
+
lines.append(f" {state_id} -->|\"{trans.label}\"| {target_id}")
|
|
211
|
+
else:
|
|
212
|
+
# Normal transition
|
|
213
|
+
target_id = nid(trans.target)
|
|
214
|
+
lines.append(f" {state_id} -->|\"{trans.label}\"| {target_id}")
|
|
215
|
+
|
|
216
|
+
# Add pop node if any pop transitions exist
|
|
217
|
+
if any(t.transition_type == "pop" for transs in transitions.values() for t in transs):
|
|
218
|
+
lines.append(" pop((pop))")
|
|
219
|
+
|
|
220
|
+
return "\n".join(lines)
|
|
File without changes
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Hypha - Decorator-based Colored Petri Net Framework
|
|
4
|
+
|
|
5
|
+
A DSL for defining and executing colored Petri nets with support for asyncio,
|
|
6
|
+
various place types, guards, and hierarchical subnet composition.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from mycorrhizal.hypha.core import pn, Runner, PlaceType
|
|
10
|
+
|
|
11
|
+
@pn.net
|
|
12
|
+
def ProcessingNet(builder):
|
|
13
|
+
# Define places
|
|
14
|
+
pending = builder.place("pending", type=PlaceType.QUEUE)
|
|
15
|
+
processed = builder.place("processed", type=PlaceType.QUEUE)
|
|
16
|
+
|
|
17
|
+
# Define transitions
|
|
18
|
+
@builder.transition()
|
|
19
|
+
async def process(consumed, bb, timebase):
|
|
20
|
+
for token in consumed:
|
|
21
|
+
result = await handle(token)
|
|
22
|
+
yield {processed: result}
|
|
23
|
+
|
|
24
|
+
# Wire the net
|
|
25
|
+
builder.arc(pending, process).arc(processed)
|
|
26
|
+
|
|
27
|
+
# Run the Petri net
|
|
28
|
+
runner = Runner(ProcessingNet, bb=blackboard)
|
|
29
|
+
await runner.start(timebase)
|
|
30
|
+
|
|
31
|
+
Key Classes:
|
|
32
|
+
NetBuilder - Builder for constructing Petri nets
|
|
33
|
+
PlaceType - Type of place (QUEUE, SET, FLAG, COUNTER)
|
|
34
|
+
PlaceSpec - Specification for a place
|
|
35
|
+
TransitionSpec - Specification for a transition
|
|
36
|
+
Runner - Runtime for executing Petri nets
|
|
37
|
+
|
|
38
|
+
Place Types:
|
|
39
|
+
QUEUE - FIFO queue, allows multiple tokens
|
|
40
|
+
SET - Unordered collection with unique tokens
|
|
41
|
+
FLAG - Boolean place (has token or not)
|
|
42
|
+
COUNTER - Numeric place with bounded capacity
|
|
43
|
+
|
|
44
|
+
Transitions:
|
|
45
|
+
Transitions consume tokens from input places and produce tokens for output places.
|
|
46
|
+
They are async functions that yield dictionaries mapping place references to tokens.
|
|
47
|
+
|
|
48
|
+
Guards:
|
|
49
|
+
Guards are conditions that must be satisfied for a transition to fire.
|
|
50
|
+
They can check token values, blackboard state, or external conditions.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
from .specs import (
|
|
54
|
+
PlaceType,
|
|
55
|
+
PlaceSpec,
|
|
56
|
+
GuardSpec,
|
|
57
|
+
TransitionSpec,
|
|
58
|
+
ArcSpec,
|
|
59
|
+
NetSpec,
|
|
60
|
+
PlaceRef,
|
|
61
|
+
TransitionRef,
|
|
62
|
+
SubnetRef,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
from .builder import (
|
|
66
|
+
NetBuilder,
|
|
67
|
+
ArcChain,
|
|
68
|
+
PetriNetDSL,
|
|
69
|
+
pn,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
from .runtime import (
|
|
73
|
+
PlaceRuntime,
|
|
74
|
+
TransitionRuntime,
|
|
75
|
+
NetRuntime,
|
|
76
|
+
Runner,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
__all__ = [
|
|
80
|
+
# Core types
|
|
81
|
+
'PlaceType',
|
|
82
|
+
'GuardSpec',
|
|
83
|
+
|
|
84
|
+
# Specification types (for type hints)
|
|
85
|
+
'PlaceSpec',
|
|
86
|
+
'TransitionSpec',
|
|
87
|
+
'ArcSpec',
|
|
88
|
+
'NetSpec',
|
|
89
|
+
|
|
90
|
+
# Reference types
|
|
91
|
+
'PlaceRef',
|
|
92
|
+
'TransitionRef',
|
|
93
|
+
'SubnetRef',
|
|
94
|
+
|
|
95
|
+
# Builder
|
|
96
|
+
'NetBuilder',
|
|
97
|
+
'ArcChain',
|
|
98
|
+
|
|
99
|
+
# Main API
|
|
100
|
+
'pn',
|
|
101
|
+
|
|
102
|
+
# Runtime
|
|
103
|
+
'PlaceRuntime',
|
|
104
|
+
'TransitionRuntime',
|
|
105
|
+
'NetRuntime',
|
|
106
|
+
'Runner',
|
|
107
|
+
]
|