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.
Files changed (37) hide show
  1. mycorrhizal/__init__.py +3 -0
  2. mycorrhizal/common/__init__.py +68 -0
  3. mycorrhizal/common/interface_builder.py +203 -0
  4. mycorrhizal/common/interfaces.py +412 -0
  5. mycorrhizal/common/timebase.py +99 -0
  6. mycorrhizal/common/wrappers.py +532 -0
  7. mycorrhizal/enoki/__init__.py +0 -0
  8. mycorrhizal/enoki/core.py +1545 -0
  9. mycorrhizal/enoki/testing_utils.py +529 -0
  10. mycorrhizal/enoki/util.py +220 -0
  11. mycorrhizal/hypha/__init__.py +0 -0
  12. mycorrhizal/hypha/core/__init__.py +107 -0
  13. mycorrhizal/hypha/core/builder.py +404 -0
  14. mycorrhizal/hypha/core/runtime.py +890 -0
  15. mycorrhizal/hypha/core/specs.py +234 -0
  16. mycorrhizal/hypha/util.py +38 -0
  17. mycorrhizal/rhizomorph/README.md +220 -0
  18. mycorrhizal/rhizomorph/__init__.py +0 -0
  19. mycorrhizal/rhizomorph/core.py +1729 -0
  20. mycorrhizal/rhizomorph/util.py +45 -0
  21. mycorrhizal/spores/__init__.py +124 -0
  22. mycorrhizal/spores/cache.py +208 -0
  23. mycorrhizal/spores/core.py +419 -0
  24. mycorrhizal/spores/dsl/__init__.py +48 -0
  25. mycorrhizal/spores/dsl/enoki.py +514 -0
  26. mycorrhizal/spores/dsl/hypha.py +399 -0
  27. mycorrhizal/spores/dsl/rhizomorph.py +351 -0
  28. mycorrhizal/spores/encoder/__init__.py +11 -0
  29. mycorrhizal/spores/encoder/base.py +42 -0
  30. mycorrhizal/spores/encoder/json.py +159 -0
  31. mycorrhizal/spores/extraction.py +484 -0
  32. mycorrhizal/spores/models.py +288 -0
  33. mycorrhizal/spores/transport/__init__.py +10 -0
  34. mycorrhizal/spores/transport/base.py +46 -0
  35. mycorrhizal-0.1.0.dist-info/METADATA +198 -0
  36. mycorrhizal-0.1.0.dist-info/RECORD +37 -0
  37. mycorrhizal-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Rhizomorph Behavior Tree Utilities
4
+
5
+ Utility functions for behavior tree operations like Mermaid diagram generation.
6
+ """
7
+ from typing import Any
8
+ from types import SimpleNamespace
9
+
10
+
11
+ def to_mermaid(tree: SimpleNamespace) -> str:
12
+ """
13
+ Generate Mermaid diagram from a behavior tree namespace.
14
+
15
+ This is a utility wrapper that accesses the tree's internal to_mermaid method.
16
+ Use this when you have a tree namespace and want to generate a diagram.
17
+
18
+ Args:
19
+ tree: The behavior tree namespace (created with @bt.tree decorator)
20
+
21
+ Returns:
22
+ Mermaid diagram as a string
23
+
24
+ Example:
25
+ ```python
26
+ from mycorrhizal.rhizomorph.core import bt
27
+ from mycorrhizal.rhizomorph.util import to_mermaid
28
+
29
+ @bt.tree
30
+ def MyTree():
31
+ # ... define tree ...
32
+ pass
33
+
34
+ # Generate diagram
35
+ mermaid_diagram = to_mermaid(MyTree)
36
+ print(mermaid_diagram)
37
+ ```
38
+ """
39
+ # The tree namespace has a to_mermaid lambda attached by @bt.tree
40
+ if not hasattr(tree, "to_mermaid"):
41
+ raise ValueError(
42
+ "Tree namespace must have a 'to_mermaid' attribute. "
43
+ "Did you create this tree with @bt.tree decorator?"
44
+ )
45
+ return tree.to_mermaid()
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Spores - Event and Object Logging for Observability
4
+
5
+ OCEL (Object-Centric Event Log) compatible logging system for tracking events
6
+ and objects in Mycorrhizal systems. Provides automatic extraction of events
7
+ and objects from DSL execution.
8
+
9
+ Usage:
10
+ from mycorrhizal.spores import configure, spore, EventAttr, ObjectRef
11
+
12
+ # Configure logging
13
+ configure(
14
+ enabled=True,
15
+ object_cache_size=100,
16
+ transport=FileTransport("logs/ocel.jsonl"),
17
+ )
18
+
19
+ # Mark model fields for automatic extraction
20
+ class MissionContext(BaseModel):
21
+ mission_id: Annotated[str, EventAttr]
22
+ robot: Annotated[Robot, ObjectRef(qualifier="actor", scope=ObjectScope.GLOBAL)]
23
+
24
+ # Mark object types
25
+ @spore.object(object_type="Robot")
26
+ class Robot(BaseModel):
27
+ id: str
28
+ name: str
29
+
30
+ DSL Adapters:
31
+ HyphaAdapter - Log Petri net transitions with token relationships
32
+ RhizomorphAdapter - Log behavior tree node execution with status
33
+ EnokiAdapter - Log state machine execution and lifecycle events
34
+
35
+ Key Concepts:
36
+ Event - Something that happens at a point in time with attributes
37
+ Object - An entity with a type and attributes
38
+ Relationship - Link between events and objects (e.g., "actor", "target")
39
+ EventAttr - Mark blackboard fields for automatic event attribute extraction
40
+ ObjectRef - Mark blackboard fields as object references with scope
41
+
42
+ Object Scopes:
43
+ EVENT - Object exists only for this event (default)
44
+ GLOBAL - Object exists across all events (deduplicated by ID)
45
+ """
46
+
47
+ from .core import (
48
+ configure,
49
+ get_config,
50
+ get_object_cache,
51
+ spore,
52
+ SporesConfig,
53
+ )
54
+
55
+ from .models import (
56
+ Event,
57
+ Object,
58
+ LogRecord,
59
+ Relationship,
60
+ EventAttributeValue,
61
+ ObjectAttributeValue,
62
+ ObjectRef,
63
+ ObjectScope,
64
+ EventAttr,
65
+ generate_event_id,
66
+ generate_object_id,
67
+ attribute_value_from_python,
68
+ object_attribute_from_python,
69
+ )
70
+
71
+ from .cache import ObjectLRUCache
72
+
73
+ from .encoder import Encoder, JSONEncoder
74
+ from .transport import Transport
75
+
76
+ # DSL adapters
77
+ from .dsl import (
78
+ HyphaAdapter,
79
+ RhizomorphAdapter,
80
+ EnokiAdapter,
81
+ )
82
+
83
+
84
+ __version__ = "0.1.0"
85
+
86
+
87
+ __all__ = [
88
+ # Core API
89
+ 'configure',
90
+ 'get_config',
91
+ 'get_object_cache',
92
+ 'spore',
93
+ 'SporesConfig',
94
+
95
+ # Models
96
+ 'Event',
97
+ 'Object',
98
+ 'LogRecord',
99
+ 'Relationship',
100
+ 'EventAttributeValue',
101
+ 'ObjectAttributeValue',
102
+ 'ObjectRef',
103
+ 'ObjectScope',
104
+ 'EventAttr',
105
+ 'generate_event_id',
106
+ 'generate_object_id',
107
+ 'attribute_value_from_python',
108
+ 'object_attribute_from_python',
109
+
110
+ # Cache
111
+ 'ObjectLRUCache',
112
+
113
+ # Encoder
114
+ 'Encoder',
115
+ 'JSONEncoder',
116
+
117
+ # Transport
118
+ 'Transport',
119
+
120
+ # DSL Adapters
121
+ 'HyphaAdapter',
122
+ 'RhizomorphAdapter',
123
+ 'EnokiAdapter',
124
+ ]
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Spores Object Cache
4
+
5
+ LRU cache for tracking seen objects and logging them on eviction.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections import OrderedDict
11
+ from typing import Dict, Callable, Optional, TypeVar, Generic
12
+ from dataclasses import dataclass
13
+
14
+ from .models import Object
15
+
16
+
17
+ # Generic types for cache
18
+ K = TypeVar('K') # Key type (object ID)
19
+ V = TypeVar('V') # Value type (Object)
20
+
21
+
22
+ @dataclass
23
+ class CacheEntry:
24
+ """
25
+ An entry in the object cache.
26
+
27
+ Attributes:
28
+ object: The OCEL Object
29
+ sight_count: How many times we've seen this object
30
+ first_sight_time: When we first saw the object
31
+ """
32
+ object: Object
33
+ sight_count: int = 1
34
+ first_sight_time: Optional[float] = None
35
+
36
+
37
+ class ObjectLRUCache(Generic[K, V]):
38
+ """
39
+ LRU cache with eviction callback for object tracking.
40
+
41
+ When an object is first seen, it should be logged via on_first_sight.
42
+ When an object is evicted from the cache, it's logged via on_evict.
43
+
44
+ This ensures OCEL consumers see object evolution:
45
+ - First sight: Initial object state
46
+ - Eviction: Final state before being removed from cache
47
+
48
+ Example:
49
+ ```python
50
+ def on_evict(object_id: str, obj: Object):
51
+ # Send object to transport
52
+ transport.send(LogRecord(object=obj))
53
+
54
+ cache = ObjectLRUCache(maxsize=128, on_evict=on_evict)
55
+
56
+ # Check if object exists, add if not
57
+ if not cache.contains_or_add(object_id, object):
58
+ # Object not in cache, already logged by on_first_sight
59
+ pass
60
+ ```
61
+
62
+ Args:
63
+ maxsize: Maximum number of objects to cache
64
+ on_evict: Callback when object is evicted (object_id, object) -> None
65
+ on_first_sight: Optional callback when object is first seen (object_id, object) -> None
66
+ """
67
+
68
+ def __init__(
69
+ self,
70
+ maxsize: int = 128,
71
+ on_evict: Optional[Callable[[K, Object], None]] = None,
72
+ on_first_sight: Optional[Callable[[K, Object], None]] = None
73
+ ):
74
+ self.maxsize = maxsize
75
+ self.on_evict = on_evict
76
+ self.on_first_sight = on_first_sight
77
+ self._cache: OrderedDict[K, CacheEntry] = OrderedDict()
78
+
79
+ def _evict_if_needed(self) -> None:
80
+ """Evict oldest entry if cache is full."""
81
+ if len(self._cache) >= self.maxsize:
82
+ # FIFO from OrderedDict (oldest first)
83
+ object_id, entry = self._cache.popitem(last=False)
84
+ if self.on_evict:
85
+ self.on_evict(object_id, entry.object)
86
+
87
+ def get(self, key: K) -> Optional[Object]:
88
+ """
89
+ Get an object from the cache without updating its position.
90
+
91
+ Args:
92
+ key: The object ID
93
+
94
+ Returns:
95
+ The Object if found, None otherwise
96
+ """
97
+ entry = self._cache.get(key)
98
+ return entry.object if entry else None
99
+
100
+ def get_and_touch(self, key: K) -> Optional[Object]:
101
+ """
102
+ Get an object from the cache and mark it as recently used.
103
+
104
+ Args:
105
+ key: The object ID
106
+
107
+ Returns:
108
+ The Object if found, None otherwise
109
+ """
110
+ entry = self._cache.get(key)
111
+ if entry:
112
+ # Move to end (most recently used)
113
+ self._cache.move_to_end(key)
114
+ return entry.object
115
+ return None
116
+
117
+ def contains_or_add(self, key: K, obj: Object) -> bool:
118
+ """
119
+ Check if key exists, add if not.
120
+
121
+ This is the primary method for object tracking:
122
+ - If key exists: mark as recently used, return True
123
+ - If key doesn't exist: add object, potentially evict, call on_first_sight, return False
124
+
125
+ Args:
126
+ key: The object ID
127
+ obj: The OCEL Object to add if not present
128
+
129
+ Returns:
130
+ True if object was already in cache, False if it was just added
131
+ """
132
+ if key in self._cache:
133
+ # Already seen, mark as recently used
134
+ self._cache.move_to_end(key)
135
+ entry = self._cache[key]
136
+ entry.sight_count += 1
137
+ return True
138
+ else:
139
+ # First sight
140
+ self._evict_if_needed()
141
+
142
+ entry = CacheEntry(object=obj)
143
+ self._cache[key] = entry
144
+
145
+ if self.on_first_sight:
146
+ self.on_first_sight(key, obj)
147
+
148
+ return False
149
+
150
+ def add(self, key: K, obj: Object) -> None:
151
+ """
152
+ Add an object to the cache (or update if exists).
153
+
154
+ Args:
155
+ key: The object ID
156
+ obj: The OCEL Object
157
+ """
158
+ if key in self._cache:
159
+ # Update existing entry
160
+ entry = self._cache[key]
161
+ entry.object = obj
162
+ entry.sight_count += 1
163
+ self._cache.move_to_end(key)
164
+ else:
165
+ # New entry
166
+ self._evict_if_needed()
167
+
168
+ entry = CacheEntry(object=obj)
169
+ self._cache[key] = entry
170
+
171
+ if self.on_first_sight:
172
+ self.on_first_sight(key, obj)
173
+
174
+ def remove(self, key: K) -> Optional[Object]:
175
+ """
176
+ Remove an object from the cache.
177
+
178
+ Args:
179
+ key: The object ID
180
+
181
+ Returns:
182
+ The removed Object, or None if key wasn't in cache
183
+ """
184
+ return self._cache.pop(key, None)
185
+
186
+ def clear(self) -> None:
187
+ """Clear all objects from the cache without logging."""
188
+ self._cache.clear()
189
+
190
+ def __len__(self) -> int:
191
+ """Return the number of objects in the cache."""
192
+ return len(self._cache)
193
+
194
+ def __contains__(self, key: K) -> bool:
195
+ """Check if key is in cache."""
196
+ return key in self._cache
197
+
198
+ def keys(self) -> list[K]:
199
+ """Return all keys in the cache."""
200
+ return list(self._cache.keys())
201
+
202
+ def values(self) -> list[Object]:
203
+ """Return all objects in the cache."""
204
+ return [entry.object for entry in self._cache.values()]
205
+
206
+ def items(self) -> list[tuple[K, Object]]:
207
+ """Return all (key, object) pairs in the cache."""
208
+ return [(key, entry.object) for key, entry in self._cache.items()]