mycorrhizal 0.1.2__py3-none-any.whl → 0.2.1__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 (41) hide show
  1. mycorrhizal/_version.py +1 -1
  2. mycorrhizal/common/__init__.py +15 -3
  3. mycorrhizal/common/cache.py +114 -0
  4. mycorrhizal/common/compilation.py +263 -0
  5. mycorrhizal/common/interface_detection.py +159 -0
  6. mycorrhizal/common/interfaces.py +3 -50
  7. mycorrhizal/common/mermaid.py +124 -0
  8. mycorrhizal/common/wrappers.py +1 -1
  9. mycorrhizal/hypha/core/builder.py +11 -1
  10. mycorrhizal/hypha/core/runtime.py +242 -107
  11. mycorrhizal/mycelium/__init__.py +174 -0
  12. mycorrhizal/mycelium/core.py +619 -0
  13. mycorrhizal/mycelium/exceptions.py +30 -0
  14. mycorrhizal/mycelium/hypha_bridge.py +1143 -0
  15. mycorrhizal/mycelium/instance.py +440 -0
  16. mycorrhizal/mycelium/pn_context.py +276 -0
  17. mycorrhizal/mycelium/runner.py +165 -0
  18. mycorrhizal/mycelium/spores_integration.py +655 -0
  19. mycorrhizal/mycelium/tree_builder.py +102 -0
  20. mycorrhizal/mycelium/tree_spec.py +197 -0
  21. mycorrhizal/rhizomorph/README.md +82 -33
  22. mycorrhizal/rhizomorph/core.py +287 -119
  23. mycorrhizal/septum/TRANSITION_REFERENCE.md +385 -0
  24. mycorrhizal/{enoki → septum}/core.py +326 -100
  25. mycorrhizal/{enoki → septum}/testing_utils.py +7 -7
  26. mycorrhizal/{enoki → septum}/util.py +44 -21
  27. mycorrhizal/spores/__init__.py +3 -3
  28. mycorrhizal/spores/core.py +149 -28
  29. mycorrhizal/spores/dsl/__init__.py +8 -8
  30. mycorrhizal/spores/dsl/hypha.py +3 -15
  31. mycorrhizal/spores/dsl/rhizomorph.py +3 -11
  32. mycorrhizal/spores/dsl/{enoki.py → septum.py} +26 -77
  33. mycorrhizal/spores/encoder/json.py +21 -12
  34. mycorrhizal/spores/extraction.py +14 -11
  35. mycorrhizal/spores/models.py +53 -20
  36. mycorrhizal-0.2.1.dist-info/METADATA +335 -0
  37. mycorrhizal-0.2.1.dist-info/RECORD +54 -0
  38. mycorrhizal-0.1.2.dist-info/METADATA +0 -198
  39. mycorrhizal-0.1.2.dist-info/RECORD +0 -39
  40. /mycorrhizal/{enoki → septum}/__init__.py +0 -0
  41. {mycorrhizal-0.1.2.dist-info → mycorrhizal-0.2.1.dist-info}/WHEEL +0 -0
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- Enoki Testing Utilities
3
+ Septum Testing Utilities
4
4
 
5
- Testing infrastructure for the class-method-based Enoki architecture.
5
+ Testing infrastructure for the class-method-based Septum architecture.
6
6
  """
7
7
 
8
8
  import unittest
@@ -13,8 +13,8 @@ import gevent
13
13
  from gevent import sleep
14
14
  import traceback
15
15
 
16
- # Import the Enoki modules
17
- from mycorrhizal.enoki import (
16
+ # Import the Septum modules
17
+ from mycorrhizal.septum import (
18
18
  State,
19
19
  StateMachine,
20
20
  StateConfiguration,
@@ -151,8 +151,8 @@ def simulate_state_lifecycle(
151
151
  return results
152
152
 
153
153
 
154
- class EnokiTestCase(unittest.TestCase):
155
- """Base test case class with assertion methods for Enoki FSM testing."""
154
+ class SeptumTestCase(unittest.TestCase):
155
+ """Base test case class with assertion methods for Septum FSM testing."""
156
156
 
157
157
  def assertStateTransition(
158
158
  self, state_class, expected_transition, context=None, **context_kwargs
@@ -328,7 +328,7 @@ class EnokiTestCase(unittest.TestCase):
328
328
  self.assertIsNotNone(timeout_result, "on_timeout should return a transition")
329
329
 
330
330
 
331
- class StateMachineTestCase(EnokiTestCase):
331
+ class StateMachineTestCase(SeptumTestCase):
332
332
  """Extended test case for testing full StateMachine behavior."""
333
333
 
334
334
  def assertFSMTransition(
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- Enoki State Machine Utilities
3
+ Septum State Machine Utilities
4
4
 
5
5
  Utility functions for FSM operations like Mermaid diagram generation.
6
6
  """
@@ -16,6 +16,24 @@ class _TransitionInfo:
16
16
  transition_type: str # "normal", "push", "pop", "again", "retry", etc.
17
17
 
18
18
 
19
+ def _short_state_name(state_name: str) -> str:
20
+ """
21
+ Extract short state name from full qualified name.
22
+
23
+ Converts 'examples.septum.network_protocol_fsm.IdleState' to 'IdleState'.
24
+ Handles edge cases like '<string>.IdleState' gracefully.
25
+
26
+ Args:
27
+ state_name: Full state name with module path
28
+
29
+ Returns:
30
+ Short state name (just the class name)
31
+ """
32
+ if "." in state_name:
33
+ return state_name.rsplit(".", 1)[-1]
34
+ return state_name
35
+
36
+
19
37
  def to_mermaid(fsm) -> str:
20
38
  """
21
39
  Generate Mermaid diagram from a StateMachine instance.
@@ -24,15 +42,15 @@ def to_mermaid(fsm) -> str:
24
42
  including special handling for Push/Pop transitions.
25
43
 
26
44
  Args:
27
- fsm: A StateMachine instance (from mycorrhizal.enoki.core)
45
+ fsm: A StateMachine instance (from mycorrhizal.septum.core)
28
46
 
29
47
  Returns:
30
48
  Mermaid diagram as a string
31
49
 
32
50
  Example:
33
51
  ```python
34
- from mycorrhizal.enoki.core import StateMachine
35
- from mycorrhizal.enoki.util import to_mermaid
52
+ from mycorrhizal.septum.core import StateMachine
53
+ from mycorrhizal.septum.util import to_mermaid
36
54
 
37
55
  fsm = StateMachine(initial_state=MyState)
38
56
  await fsm.initialize()
@@ -43,7 +61,7 @@ def to_mermaid(fsm) -> str:
43
61
  ```
44
62
  """
45
63
  # Import here to avoid circular imports
46
- from mycorrhizal.enoki.core import (
64
+ from mycorrhizal.septum.core import (
47
65
  StateSpec,
48
66
  StateRef,
49
67
  LabeledTransition,
@@ -77,12 +95,12 @@ def to_mermaid(fsm) -> str:
77
95
 
78
96
  # Add initial state marker
79
97
  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)}")
98
+ lines.append(f' start((start)) --> {nid(initial_name)}')
81
99
 
82
100
  # Add error state if exists
83
101
  if fsm.error_state:
84
102
  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)))")
103
+ lines.append(f' {nid(error_name)}{{"ERROR"}}')
86
104
 
87
105
  # Collect all transitions
88
106
  transitions: Dict[str, List[_TransitionInfo]] = {}
@@ -135,8 +153,9 @@ def to_mermaid(fsm) -> str:
135
153
  # Push transitions - show all states being pushed
136
154
  for push_state in p.push_states:
137
155
  push_name = push_state.name if isinstance(push_state, StateSpec) else str(push_state)
156
+ # Use -> instead of unicode arrow for better compatibility
138
157
  transitions[state_name].append(
139
- _TransitionInfo(f"{label.name}push", push_name, "push")
158
+ _TransitionInfo(f"{label.name}->push", push_name, "push")
140
159
  )
141
160
  case StateSpec() as target_state:
142
161
  transitions[state_name].append(
@@ -182,39 +201,43 @@ def to_mermaid(fsm) -> str:
182
201
  continue
183
202
 
184
203
  state_id = nid(state_name)
204
+ short_name = _short_state_name(state_name)
185
205
 
186
206
  # Determine node shape based on state properties
207
+ # All nodes use quotes to handle special characters
187
208
  if state.config.terminal:
188
- shape = f'[{state_name} (**terminal**)]'
209
+ # Terminal state: use double parentheses and note terminal status
210
+ lines.append(f' {state_id}[["{short_name}<br/>terminal"]]')
189
211
  elif state_name == initial_name:
190
- shape = f'[{state_name}]'
212
+ # Initial state: normal shape
213
+ lines.append(f' {state_id}["{short_name}"]')
191
214
  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}]'
215
+ # Error state: use rhombus shape
216
+ lines.append(f' {state_id}{{"{short_name}"}}')
193
217
  else:
194
- shape = f'[{state_name}]'
195
-
196
- lines.append(f" {state_id}{shape}")
218
+ # Normal state
219
+ lines.append(f' {state_id}["{short_name}"]')
197
220
 
198
221
  # Add transitions
199
222
  for trans in transitions.get(state_name, []):
200
223
  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}")
224
+ # Self-loop with transition type
225
+ lines.append(f' {state_id} -->|"{trans.transition_type}"| {state_id}')
204
226
  elif trans.transition_type == "pop":
205
227
  # Pop goes to a special pop node
206
- lines.append(f" {state_id} -->|\"{trans.label or 'pop'}\"| pop((pop))")
228
+ label = trans.label if trans.label else "pop"
229
+ lines.append(f' {state_id} -->|"{label}"| pop((pop))')
207
230
  elif trans.transition_type == "push":
208
231
  # Push transition
209
232
  target_id = nid(trans.target)
210
- lines.append(f" {state_id} -->|\"{trans.label}\"| {target_id}")
233
+ lines.append(f' {state_id} -->|"{trans.label}"| {target_id}')
211
234
  else:
212
235
  # Normal transition
213
236
  target_id = nid(trans.target)
214
- lines.append(f" {state_id} -->|\"{trans.label}\"| {target_id}")
237
+ lines.append(f' {state_id} -->|"{trans.label}"| {target_id}')
215
238
 
216
239
  # Add pop node if any pop transitions exist
217
240
  if any(t.transition_type == "pop" for transs in transitions.values() for t in transs):
218
- lines.append(" pop((pop))")
241
+ lines.append(' pop((pop))')
219
242
 
220
243
  return "\n".join(lines)
@@ -61,7 +61,7 @@ Usage (DSL Adapters):
61
61
  DSL Adapters:
62
62
  HyphaAdapter - Log Petri net transitions with token relationships
63
63
  RhizomorphAdapter - Log behavior tree node execution with status
64
- EnokiAdapter - Log state machine execution and lifecycle events
64
+ SeptumAdapter - Log state machine execution and lifecycle events
65
65
 
66
66
  Annotation Types:
67
67
  EventAttr - Mark blackboard fields for automatic event attribute extraction (DSL adapters)
@@ -117,7 +117,7 @@ from .transport import SyncTransport, AsyncTransport, Transport, SyncFileTranspo
117
117
  from .dsl import (
118
118
  HyphaAdapter,
119
119
  RhizomorphAdapter,
120
- EnokiAdapter,
120
+ SeptumAdapter,
121
121
  )
122
122
 
123
123
 
@@ -173,5 +173,5 @@ __all__ = [
173
173
  # DSL Adapters
174
174
  'HyphaAdapter',
175
175
  'RhizomorphAdapter',
176
- 'EnokiAdapter',
176
+ 'SeptumAdapter',
177
177
  ]
@@ -266,11 +266,7 @@ class AsyncEventLogger(EventLogger):
266
266
 
267
267
  attr_values = {}
268
268
  for key, value in kwargs.items():
269
- attr_values[key] = EventAttributeValue(
270
- name=key,
271
- value=str(value),
272
- time=timestamp
273
- )
269
+ attr_values[key] = attribute_value_from_python(value)
274
270
 
275
271
  event = Event(
276
272
  id=generate_event_id(),
@@ -293,11 +289,7 @@ class AsyncEventLogger(EventLogger):
293
289
 
294
290
  attr_values = {}
295
291
  for key, value in kwargs.items():
296
- attr_values[key] = ObjectAttributeValue(
297
- name=key,
298
- value=str(value),
299
- time=timestamp
300
- )
292
+ attr_values[key] = object_attribute_from_python(value, time=timestamp)
301
293
 
302
294
  obj = Object(
303
295
  id=obj_id,
@@ -346,7 +338,7 @@ class AsyncEventLogger(EventLogger):
346
338
  source, obj_type, attrs = rel_spec
347
339
 
348
340
  # Resolve object from source
349
- obj = self._resolve_source(source, context)
341
+ obj = self._resolve_source(source, context, obj_type)
350
342
  if obj is None:
351
343
  continue
352
344
 
@@ -392,14 +384,84 @@ class AsyncEventLogger(EventLogger):
392
384
 
393
385
  return context
394
386
 
395
- def _resolve_source(self, source: str, context: Dict[str, Any]) -> Any:
396
- """Resolve source for async version."""
387
+ def _resolve_source(self, source: str, context: Dict[str, Any], obj_type: str | None = None) -> Any:
388
+ """Resolve source for async version.
389
+
390
+ If source is "bb" (blackboard) and obj_type is provided, extract the field
391
+ of that type from the blackboard instead of returning the entire blackboard.
392
+ """
397
393
  if source == "return" or source == "ret":
398
394
  return context.get("return")
399
395
  elif source == "self":
400
396
  return context.get("self")
401
397
  else:
402
- return context.get(source)
398
+ obj = context.get(source)
399
+
400
+ # If source is blackboard and we need a specific type, extract that field
401
+ if obj_type and source == "bb" and hasattr(obj, '__annotations__'):
402
+ return self._find_object_by_type(obj, obj_type)
403
+
404
+ return obj
405
+
406
+ def _find_object_by_type(self, blackboard: Any, obj_type: str) -> Any:
407
+ """Find a field in the blackboard that matches the requested object type.
408
+
409
+ Scans the blackboard's fields and returns the first field whose type
410
+ annotation matches obj_type.
411
+ """
412
+ # Get the class to check annotations
413
+ obj_class = blackboard if isinstance(blackboard, type) else type(blackboard)
414
+
415
+ if not hasattr(obj_class, '__annotations__'):
416
+ return blackboard
417
+
418
+ # Check each field's type annotation
419
+ for field_name, field_type in obj_class.__annotations__.items():
420
+ # Handle Annotated types
421
+ if get_origin(field_type) is Annotated:
422
+ args = get_args(field_type)
423
+ if args:
424
+ actual_type = args[0]
425
+ # Check if type name matches (handle both str and type)
426
+ type_name = actual_type if isinstance(actual_type, str) else actual_type.__name__
427
+ if type_name == obj_type:
428
+ field_value = getattr(blackboard, field_name, None)
429
+ # Return the actual value, not None
430
+ if field_value is not None:
431
+ return field_value
432
+ # Handle Union types (e.g., Sample | None)
433
+ elif get_origin(field_type) is Union:
434
+ args = get_args(field_type)
435
+ for arg in args:
436
+ # Skip None
437
+ if arg is type(None):
438
+ continue
439
+ # Check if this arg matches our target type
440
+ if get_origin(arg) is Annotated:
441
+ annotated_args = get_args(arg)
442
+ if annotated_args:
443
+ actual_type = annotated_args[0]
444
+ type_name = actual_type if isinstance(actual_type, str) else actual_type.__name__
445
+ if type_name == obj_type:
446
+ field_value = getattr(blackboard, field_name, None)
447
+ if field_value is not None:
448
+ return field_value
449
+ else:
450
+ type_name = arg if isinstance(arg, str) else arg.__name__
451
+ if type_name == obj_type:
452
+ field_value = getattr(blackboard, field_name, None)
453
+ if field_value is not None:
454
+ return field_value
455
+ else:
456
+ # Check if type name matches
457
+ type_name = field_type if isinstance(field_type, str) else field_type.__name__
458
+ if type_name == obj_type:
459
+ field_value = getattr(blackboard, field_name, None)
460
+ if field_value is not None:
461
+ return field_value
462
+
463
+ # Fallback: return blackboard if no matching field found
464
+ return blackboard
403
465
 
404
466
  def _get_object_id(self, obj: Any) -> str | None:
405
467
  """Extract object ID for async version."""
@@ -518,11 +580,7 @@ class SyncEventLogger(EventLogger):
518
580
 
519
581
  attr_values = {}
520
582
  for key, value in kwargs.items():
521
- attr_values[key] = EventAttributeValue(
522
- name=key,
523
- value=str(value),
524
- time=timestamp
525
- )
583
+ attr_values[key] = attribute_value_from_python(value)
526
584
 
527
585
  event = Event(
528
586
  id=generate_event_id(),
@@ -550,11 +608,7 @@ class SyncEventLogger(EventLogger):
550
608
 
551
609
  attr_values = {}
552
610
  for key, value in kwargs.items():
553
- attr_values[key] = ObjectAttributeValue(
554
- name=key,
555
- value=str(value),
556
- time=timestamp
557
- )
611
+ attr_values[key] = object_attribute_from_python(value, time=timestamp)
558
612
 
559
613
  obj = Object(
560
614
  id=obj_id,
@@ -624,7 +678,7 @@ class SyncEventLogger(EventLogger):
624
678
  source, obj_type, attrs = rel_spec
625
679
 
626
680
  # Resolve object from source
627
- obj = self._resolve_source(source, context)
681
+ obj = self._resolve_source(source, context, obj_type)
628
682
  if obj is None:
629
683
  continue
630
684
 
@@ -678,7 +732,7 @@ class SyncEventLogger(EventLogger):
678
732
 
679
733
  return context
680
734
 
681
- def _resolve_source(self, source: str, context: Dict[str, Any]) -> Any:
735
+ def _resolve_source(self, source: str, context: Dict[str, Any], obj_type: str | None = None) -> Any:
682
736
  """
683
737
  Resolve an object from a source expression.
684
738
 
@@ -686,13 +740,80 @@ class SyncEventLogger(EventLogger):
686
740
  - "return" or "ret": Return value
687
741
  - "self": For methods
688
742
  - Any other string: Parameter name from context
743
+ - If source is "bb" (blackboard) and obj_type is provided, extract the field of that type
689
744
  """
690
745
  if source == "return" or source == "ret":
691
746
  return context.get("return")
692
747
  elif source == "self":
693
748
  return context.get("self")
694
749
  else:
695
- return context.get(source)
750
+ obj = context.get(source)
751
+
752
+ # If source is blackboard and we need a specific type, extract that field
753
+ if obj_type and source == "bb" and hasattr(obj, '__annotations__'):
754
+ return self._find_object_by_type(obj, obj_type)
755
+
756
+ return obj
757
+
758
+ def _find_object_by_type(self, blackboard: Any, obj_type: str) -> Any:
759
+ """Find a field in the blackboard that matches the requested object type.
760
+
761
+ Scans the blackboard's fields and returns the first field whose type
762
+ annotation matches obj_type.
763
+ """
764
+ # Get the class to check annotations
765
+ obj_class = blackboard if isinstance(blackboard, type) else type(blackboard)
766
+
767
+ if not hasattr(obj_class, '__annotations__'):
768
+ return blackboard
769
+
770
+ # Check each field's type annotation
771
+ for field_name, field_type in obj_class.__annotations__.items():
772
+ # Handle Annotated types
773
+ if get_origin(field_type) is Annotated:
774
+ args = get_args(field_type)
775
+ if args:
776
+ actual_type = args[0]
777
+ # Check if type name matches (handle both str and type)
778
+ type_name = actual_type if isinstance(actual_type, str) else actual_type.__name__
779
+ if type_name == obj_type:
780
+ field_value = getattr(blackboard, field_name, None)
781
+ # Return the actual value, not None
782
+ if field_value is not None:
783
+ return field_value
784
+ # Handle Union types (e.g., Sample | None)
785
+ elif get_origin(field_type) is Union:
786
+ args = get_args(field_type)
787
+ for arg in args:
788
+ # Skip None
789
+ if arg is type(None):
790
+ continue
791
+ # Check if this arg matches our target type
792
+ if get_origin(arg) is Annotated:
793
+ annotated_args = get_args(arg)
794
+ if annotated_args:
795
+ actual_type = annotated_args[0]
796
+ type_name = actual_type if isinstance(actual_type, str) else actual_type.__name__
797
+ if type_name == obj_type:
798
+ field_value = getattr(blackboard, field_name, None)
799
+ if field_value is not None:
800
+ return field_value
801
+ else:
802
+ type_name = arg if isinstance(arg, str) else arg.__name__
803
+ if type_name == obj_type:
804
+ field_value = getattr(blackboard, field_name, None)
805
+ if field_value is not None:
806
+ return field_value
807
+ else:
808
+ # Check if type name matches
809
+ type_name = field_type if isinstance(field_type, str) else field_type.__name__
810
+ if type_name == obj_type:
811
+ field_value = getattr(blackboard, field_name, None)
812
+ if field_value is not None:
813
+ return field_value
814
+
815
+ # Fallback: return blackboard if no matching field found
816
+ return blackboard
696
817
 
697
818
  def _get_object_id(self, obj: Any) -> str | None:
698
819
  """
@@ -1031,7 +1152,7 @@ class SporeDecorator:
1031
1152
  type=event_type,
1032
1153
  time=timestamp,
1033
1154
  attributes={
1034
- k: EventAttributeValue(name=k, value=str(v), time=timestamp)
1155
+ k: attribute_value_from_python(v)
1035
1156
  for k, v in event_attrs.items()
1036
1157
  },
1037
1158
  relationships={
@@ -5,11 +5,11 @@ Spores DSL Adapters
5
5
  DSL-specific adapters for integrating spores logging with:
6
6
  - Hypha (Petri nets)
7
7
  - Rhizomorph (Behavior trees)
8
- - Enoki (State machines)
8
+ - Septum (State machines)
9
9
 
10
10
  Usage:
11
11
  ```python
12
- from mycorrhizal.spores.dsl import HyphaAdapter, RhizomorphAdapter, EnokiAdapter
12
+ from mycorrhizal.spores.dsl import HyphaAdapter, RhizomorphAdapter, SeptumAdapter
13
13
 
14
14
  # For Hypha (Petri nets)
15
15
  hypha_adapter = HyphaAdapter()
@@ -27,11 +27,11 @@ Usage:
27
27
  async def check(bb: Blackboard) -> Status:
28
28
  return Status.SUCCESS
29
29
 
30
- # For Enoki (State machines)
31
- enoki_adapter = EnokiAdapter()
30
+ # For Septum (State machines)
31
+ septum_adapter = SeptumAdapter()
32
32
 
33
- @enoki.on_state
34
- @enoki_adapter.log_state(event_type="state_execute")
33
+ @septum.on_state
34
+ @septum_adapter.log_state(event_type="state_execute")
35
35
  async def on_state(ctx: SharedContext):
36
36
  return Events.DONE
37
37
  ```
@@ -39,10 +39,10 @@ Usage:
39
39
 
40
40
  from .hypha import HyphaAdapter
41
41
  from .rhizomorph import RhizomorphAdapter
42
- from .enoki import EnokiAdapter
42
+ from .septum import SeptumAdapter
43
43
 
44
44
  __all__ = [
45
45
  'HyphaAdapter',
46
46
  'RhizomorphAdapter',
47
- 'EnokiAdapter',
47
+ 'SeptumAdapter',
48
48
  ]
@@ -237,18 +237,10 @@ async def _log_transition_event(
237
237
  event_attrs = {}
238
238
 
239
239
  # Add token count
240
- event_attrs["token_count"] = EventAttributeValue(
241
- name="token_count",
242
- value=str(len(consumed)),
243
- time=timestamp
244
- )
240
+ event_attrs["token_count"] = attribute_value_from_python(len(consumed))
245
241
 
246
242
  # Add transition name
247
- event_attrs["transition_name"] = EventAttributeValue(
248
- name="transition_name",
249
- value=func.__name__,
250
- time=timestamp
251
- )
243
+ event_attrs["transition_name"] = attribute_value_from_python(func.__name__)
252
244
 
253
245
  # Extract from blackboard
254
246
  bb_attrs = extract_attributes_from_blackboard(bb, timestamp)
@@ -358,11 +350,7 @@ async def _log_place_event(
358
350
 
359
351
  # Build event attributes
360
352
  event_attrs = {
361
- "place_name": EventAttributeValue(
362
- name="place_name",
363
- value=func.__name__,
364
- time=timestamp
365
- )
353
+ "place_name": attribute_value_from_python(func.__name__)
366
354
  }
367
355
 
368
356
  # Extract from blackboard
@@ -20,7 +20,7 @@ from ...spores import (
20
20
  Event,
21
21
  LogRecord,
22
22
  Relationship,
23
- EventAttributeValue,
23
+ attribute_value_from_python,
24
24
  generate_event_id,
25
25
  )
26
26
  from ...spores.extraction import (
@@ -167,19 +167,11 @@ async def _log_node_event(
167
167
  event_attrs = {}
168
168
 
169
169
  # Add node name
170
- event_attrs["node_name"] = EventAttributeValue(
171
- name="node_name",
172
- value=func.__name__,
173
- time=timestamp
174
- )
170
+ event_attrs["node_name"] = attribute_value_from_python(func.__name__)
175
171
 
176
172
  # Add status if requested and result is a Status
177
173
  if log_status and isinstance(result, Status):
178
- event_attrs["status"] = EventAttributeValue(
179
- name="status",
180
- value=result.name,
181
- time=timestamp
182
- )
174
+ event_attrs["status"] = attribute_value_from_python(result.name)
183
175
 
184
176
  # Extract from blackboard
185
177
  bb_attrs = extract_attributes_from_blackboard(bb, timestamp)