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,514 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Spores Adapter for Enoki (State Machines)
4
+
5
+ Provides logging integration for Enoki state machines.
6
+ Extracts context information and automatically creates event/object logs.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import functools
13
+ import inspect
14
+ import logging
15
+ from datetime import datetime
16
+ from typing import Any, Callable, Dict, List, Optional, Union
17
+
18
+ from ...spores import (
19
+ get_config,
20
+ Event,
21
+ LogRecord,
22
+ Relationship,
23
+ EventAttributeValue,
24
+ generate_event_id,
25
+ )
26
+ from ...spores.extraction import (
27
+ extract_attributes_from_blackboard,
28
+ extract_objects_from_blackboard,
29
+ )
30
+ from ...spores.core import _send_log_record, get_object_cache
31
+ from ...enoki.core import SharedContext, TransitionType
32
+
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ class EnokiAdapter:
38
+ """
39
+ Adapter for Enoki state machine logging.
40
+
41
+ Provides decorators and helpers for logging:
42
+ - State entry/exit/timeout events
43
+ - State execution events
44
+ - Transition events
45
+ - Message/context object lifecycle tracking
46
+
47
+ Usage:
48
+ ```python
49
+ from mycorrhizal.spores.dsl import EnokiAdapter
50
+
51
+ adapter = EnokiAdapter()
52
+
53
+ @enoki.state()
54
+ def MyState():
55
+ @enoki.on_state
56
+ @adapter.log_state(event_type="state_execute")
57
+ async def on_state(ctx: SharedContext):
58
+ # Event automatically logged with:
59
+ # - state_name attribute
60
+ # - attributes from ctx.common (with EventAttr annotations)
61
+ # - relationships to objects (with ObjectRef annotations)
62
+ return Events.DONE
63
+
64
+ @enoki.on_enter
65
+ @adapter.log_state_lifecycle(event_type="state_enter")
66
+ async def on_enter(ctx: SharedContext):
67
+ pass
68
+ ```
69
+ """
70
+
71
+ def __init__(self):
72
+ """Initialize the Enoki adapter."""
73
+ self._enabled = True
74
+
75
+ def enable(self):
76
+ """Enable logging for this adapter."""
77
+ self._enabled = True
78
+
79
+ def disable(self):
80
+ """Disable logging for this adapter."""
81
+ self._enabled = False
82
+
83
+ def log_state(
84
+ self,
85
+ event_type: str,
86
+ attributes: Optional[Dict[str, Any]] = None,
87
+ log_state_name: bool = True,
88
+ log_transition: bool = True,
89
+ ) -> Callable:
90
+ """
91
+ Decorator to log Enoki state execution.
92
+
93
+ Automatically captures:
94
+ - State name
95
+ - Transition result (if log_transition=True)
96
+ - Attributes from ctx.common (with EventAttr annotations)
97
+ - Objects from ctx.common (with ObjectRef annotations)
98
+ - Message information (if present in ctx)
99
+
100
+ Args:
101
+ event_type: Type of event to log
102
+ attributes: Static attributes to include
103
+ log_state_name: Whether to include state name in attributes
104
+ log_transition: Whether to include transition result
105
+
106
+ Returns:
107
+ Decorator function
108
+ """
109
+ def decorator(func: Callable) -> Callable:
110
+ @functools.wraps(func)
111
+ async def async_wrapper(ctx: SharedContext):
112
+ # Call original state handler
113
+ result = await func(ctx)
114
+
115
+ # Log event
116
+ await _log_state_event(
117
+ func, ctx, event_type, attributes, log_state_name, log_transition, result
118
+ )
119
+
120
+ return result
121
+
122
+ @functools.wraps(func)
123
+ def sync_wrapper(ctx: SharedContext):
124
+ # Call original state handler
125
+ result = func(ctx)
126
+
127
+ # Schedule logging
128
+ asyncio.create_task(_log_state_event(
129
+ func, ctx, event_type, attributes, log_state_name, log_transition, result
130
+ ))
131
+
132
+ return result
133
+
134
+ if asyncio.iscoroutinefunction(func):
135
+ return async_wrapper # type: ignore
136
+ else:
137
+ return sync_wrapper # type: ignore
138
+
139
+ return decorator
140
+
141
+ def log_state_lifecycle(
142
+ self,
143
+ event_type: str,
144
+ attributes: Optional[Dict[str, Any]] = None,
145
+ ) -> Callable:
146
+ """
147
+ Decorator to log state lifecycle events (on_enter, on_leave, on_timeout).
148
+
149
+ Use this for logging lifecycle hooks rather than main state execution.
150
+
151
+ Args:
152
+ event_type: Type of event to log (e.g., "state_enter", "state_leave")
153
+ attributes: Static attributes to include
154
+
155
+ Returns:
156
+ Decorator function
157
+ """
158
+ def decorator(func: Callable) -> Callable:
159
+ @functools.wraps(func)
160
+ async def async_wrapper(ctx: SharedContext):
161
+ # Log before execution
162
+ config = get_config()
163
+ if config.enabled:
164
+ await _log_lifecycle_event(func, ctx, event_type, attributes, "enter")
165
+
166
+ # Call original lifecycle handler
167
+ result = await func(ctx)
168
+
169
+ return result
170
+
171
+ @functools.wraps(func)
172
+ def sync_wrapper(ctx: SharedContext):
173
+ # Schedule logging
174
+ config = get_config()
175
+ if config.enabled:
176
+ asyncio.create_task(_log_lifecycle_event(
177
+ func, ctx, event_type, attributes, "enter"
178
+ ))
179
+
180
+ return func(ctx)
181
+
182
+ if asyncio.iscoroutinefunction(func):
183
+ return async_wrapper # type: ignore
184
+ else:
185
+ return sync_wrapper # type: ignore
186
+
187
+ return decorator
188
+
189
+
190
+ async def _log_state_event(
191
+ func: Callable,
192
+ ctx: SharedContext,
193
+ event_type: str,
194
+ attributes: Optional[Dict[str, Any]],
195
+ log_state_name: bool,
196
+ log_transition: bool,
197
+ result: Any,
198
+ ) -> None:
199
+ """Log a state execution event."""
200
+ config = get_config()
201
+ if not config.enabled:
202
+ return
203
+
204
+ try:
205
+ timestamp = datetime.now()
206
+
207
+ # Build event attributes
208
+ event_attrs = {}
209
+
210
+ # Add state name if requested
211
+ if log_state_name:
212
+ # Try to get state name from function or context
213
+ state_name = func.__name__
214
+ if hasattr(ctx, 'current_state') and ctx.current_state:
215
+ state_name = getattr(ctx.current_state, 'name', state_name)
216
+
217
+ event_attrs["state_name"] = EventAttributeValue(
218
+ name="state_name",
219
+ value=state_name,
220
+ time=timestamp
221
+ )
222
+
223
+ # Add transition result if requested
224
+ if log_transition and isinstance(result, TransitionType):
225
+ event_attrs["transition"] = EventAttributeValue(
226
+ name="transition",
227
+ value=result.name,
228
+ time=timestamp
229
+ )
230
+
231
+ # Extract from ctx.common (blackboard)
232
+ if hasattr(ctx, 'common') and ctx.common:
233
+ bb_attrs = extract_attributes_from_blackboard(ctx.common, timestamp)
234
+ event_attrs.update(bb_attrs)
235
+
236
+ # Add message if present
237
+ if hasattr(ctx, 'msg') and ctx.msg is not None:
238
+ event_attrs["message_type"] = EventAttributeValue(
239
+ name="message_type",
240
+ value=type(ctx.msg).__name__,
241
+ time=timestamp
242
+ )
243
+
244
+ # Extract objects from ctx.common
245
+ relationships = {}
246
+ event_objects = []
247
+
248
+ if hasattr(ctx, 'common') and ctx.common:
249
+ bb_objects = extract_objects_from_blackboard(ctx.common)
250
+ event_objects.extend(bb_objects)
251
+
252
+ for obj in bb_objects:
253
+ relationships[obj.id] = Relationship(
254
+ object_id=obj.id,
255
+ qualifier="context"
256
+ )
257
+
258
+ # Build event
259
+ event = Event(
260
+ id=generate_event_id(),
261
+ type=event_type,
262
+ time=timestamp,
263
+ attributes=event_attrs,
264
+ relationships=relationships
265
+ )
266
+
267
+ # Send event
268
+ await _send_log_record(LogRecord(event=event))
269
+
270
+ # Send objects to cache
271
+ cache = get_object_cache()
272
+ for obj in event_objects:
273
+ cache.contains_or_add(obj.id, obj)
274
+
275
+ except Exception as e:
276
+ logger.error(f"Failed to log state event: {e}")
277
+
278
+
279
+ async def _log_lifecycle_event(
280
+ func: Callable,
281
+ ctx: SharedContext,
282
+ event_type: str,
283
+ attributes: Optional[Dict[str, Any]],
284
+ phase: str,
285
+ ) -> None:
286
+ """Log a state lifecycle event."""
287
+ config = get_config()
288
+ if not config.enabled:
289
+ return
290
+
291
+ try:
292
+ timestamp = datetime.now()
293
+
294
+ # Build event attributes
295
+ event_attrs = {
296
+ "lifecycle_method": EventAttributeValue(
297
+ name="lifecycle_method",
298
+ value=func.__name__,
299
+ time=timestamp
300
+ ),
301
+ "phase": EventAttributeValue(
302
+ name="phase",
303
+ value=phase,
304
+ time=timestamp
305
+ )
306
+ }
307
+
308
+ # Add state name
309
+ state_name = func.__name__
310
+ if hasattr(ctx, 'current_state') and ctx.current_state:
311
+ state_name = getattr(ctx.current_state, 'name', state_name)
312
+
313
+ event_attrs["state_name"] = EventAttributeValue(
314
+ name="state_name",
315
+ value=state_name,
316
+ time=timestamp
317
+ )
318
+
319
+ # Add static attributes
320
+ if attributes:
321
+ for key, value in attributes.items():
322
+ if callable(value):
323
+ if hasattr(ctx, 'common') and ctx.common:
324
+ result = value(ctx.common)
325
+ event_attrs[key] = EventAttributeValue(
326
+ name=key,
327
+ value=str(result),
328
+ time=timestamp
329
+ )
330
+ else:
331
+ event_attrs[key] = EventAttributeValue(
332
+ name=key,
333
+ value=str(value),
334
+ time=timestamp
335
+ )
336
+
337
+ # Extract from ctx.common
338
+ if hasattr(ctx, 'common') and ctx.common:
339
+ bb_attrs = extract_attributes_from_blackboard(ctx.common, timestamp)
340
+ event_attrs.update(bb_attrs)
341
+
342
+ # Extract objects from ctx.common
343
+ relationships = {}
344
+ event_objects = []
345
+
346
+ if hasattr(ctx, 'common') and ctx.common:
347
+ bb_objects = extract_objects_from_blackboard(ctx.common)
348
+ event_objects.extend(bb_objects)
349
+
350
+ for obj in bb_objects:
351
+ relationships[obj.id] = Relationship(
352
+ object_id=obj.id,
353
+ qualifier="context"
354
+ )
355
+
356
+ # Build event
357
+ event = Event(
358
+ id=generate_event_id(),
359
+ type=event_type,
360
+ time=timestamp,
361
+ attributes=event_attrs,
362
+ relationships=relationships
363
+ )
364
+
365
+ # Send event
366
+ await _send_log_record(LogRecord(event=event))
367
+
368
+ # Send objects to cache
369
+ cache = get_object_cache()
370
+ for obj in event_objects:
371
+ cache.contains_or_add(obj.id, obj)
372
+
373
+ except Exception as e:
374
+ logger.error(f"Failed to log lifecycle event: {e}")
375
+
376
+
377
+ def log_message(
378
+ event_type: str = "message_received",
379
+ attributes: Optional[Dict[str, Any]] = None,
380
+ ) -> Callable:
381
+ """
382
+ Decorator to log message handling events.
383
+
384
+ Use this for logging when messages are processed by states.
385
+
386
+ Args:
387
+ event_type: Type of event to log
388
+ attributes: Static attributes to include
389
+
390
+ Returns:
391
+ Decorator function
392
+ """
393
+ def decorator(func: Callable) -> Callable:
394
+ @functools.wraps(func)
395
+ async def async_wrapper(ctx: SharedContext):
396
+ # Log message
397
+ config = get_config()
398
+ if config.enabled and hasattr(ctx, 'msg') and ctx.msg is not None:
399
+ await _log_message_event(func, ctx, event_type, attributes)
400
+
401
+ return await func(ctx)
402
+
403
+ @functools.wraps(func)
404
+ def sync_wrapper(ctx: SharedContext):
405
+ # Schedule logging
406
+ config = get_config()
407
+ if config.enabled and hasattr(ctx, 'msg') and ctx.msg is not None:
408
+ asyncio.create_task(_log_message_event(
409
+ func, ctx, event_type, attributes
410
+ ))
411
+
412
+ return func(ctx)
413
+
414
+ if asyncio.iscoroutinefunction(func):
415
+ return async_wrapper # type: ignore
416
+ else:
417
+ return sync_wrapper # type: ignore
418
+
419
+ return decorator
420
+
421
+
422
+ async def _log_message_event(
423
+ func: Callable,
424
+ ctx: SharedContext,
425
+ event_type: str,
426
+ attributes: Optional[Dict[str, Any]],
427
+ ) -> None:
428
+ """Log a message event."""
429
+ config = get_config()
430
+ if not config.enabled:
431
+ return
432
+
433
+ try:
434
+ timestamp = datetime.now()
435
+
436
+ # Build event attributes
437
+ event_attrs = {
438
+ "message_type": EventAttributeValue(
439
+ name="message_type",
440
+ value=type(ctx.msg).__name__,
441
+ time=timestamp
442
+ ),
443
+ "handler": EventAttributeValue(
444
+ name="handler",
445
+ value=func.__name__,
446
+ time=timestamp
447
+ )
448
+ }
449
+
450
+ # Add state name
451
+ if hasattr(ctx, 'current_state') and ctx.current_state:
452
+ state_name = getattr(ctx.current_state, 'name', 'unknown')
453
+ event_attrs["state_name"] = EventAttributeValue(
454
+ name="state_name",
455
+ value=state_name,
456
+ time=timestamp
457
+ )
458
+
459
+ # Add static attributes
460
+ if attributes:
461
+ for key, value in attributes.items():
462
+ if callable(value):
463
+ if hasattr(ctx, 'common') and ctx.common:
464
+ result = value(ctx.common)
465
+ event_attrs[key] = EventAttributeValue(
466
+ name=key,
467
+ value=str(result),
468
+ time=timestamp
469
+ )
470
+ else:
471
+ event_attrs[key] = EventAttributeValue(
472
+ name=key,
473
+ value=str(value),
474
+ time=timestamp
475
+ )
476
+
477
+ # Extract from ctx.common
478
+ if hasattr(ctx, 'common') and ctx.common:
479
+ bb_attrs = extract_attributes_from_blackboard(ctx.common, timestamp)
480
+ event_attrs.update(bb_attrs)
481
+
482
+ # Extract objects from ctx.common
483
+ relationships = {}
484
+ event_objects = []
485
+
486
+ if hasattr(ctx, 'common') and ctx.common:
487
+ bb_objects = extract_objects_from_blackboard(ctx.common)
488
+ event_objects.extend(bb_objects)
489
+
490
+ for obj in bb_objects:
491
+ relationships[obj.id] = Relationship(
492
+ object_id=obj.id,
493
+ qualifier="context"
494
+ )
495
+
496
+ # Build event
497
+ event = Event(
498
+ id=generate_event_id(),
499
+ type=event_type,
500
+ time=timestamp,
501
+ attributes=event_attrs,
502
+ relationships=relationships
503
+ )
504
+
505
+ # Send event
506
+ await _send_log_record(LogRecord(event=event))
507
+
508
+ # Send objects to cache
509
+ cache = get_object_cache()
510
+ for obj in event_objects:
511
+ cache.contains_or_add(obj.id, obj)
512
+
513
+ except Exception as e:
514
+ logger.error(f"Failed to log message event: {e}")