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,399 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Spores Adapter for Hypha (Petri Nets)
4
+
5
+ Provides logging integration for Hypha transitions and places.
6
+ Extracts token 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
+ spore,
20
+ get_config,
21
+ Event,
22
+ Object,
23
+ LogRecord,
24
+ Relationship,
25
+ EventAttributeValue,
26
+ ObjectAttributeValue,
27
+ generate_event_id,
28
+ generate_object_id,
29
+ attribute_value_from_python,
30
+ object_attribute_from_python,
31
+ )
32
+ from ...spores.extraction import (
33
+ extract_attributes_from_blackboard,
34
+ extract_objects_from_blackboard,
35
+ convert_to_ocel_object,
36
+ )
37
+ from ...spores.core import _send_log_record, get_object_cache
38
+
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+
43
+ class HyphaAdapter:
44
+ """
45
+ Adapter for Hypha Petri net logging.
46
+
47
+ Provides decorators and helpers for logging:
48
+ - Transition execution with consumed/produced tokens
49
+ - Place token arrivals/departures
50
+ - Token object lifecycle tracking
51
+
52
+ Usage:
53
+ ```python
54
+ from mycorrhizal.spores.dsl import HyphaAdapter
55
+
56
+ adapter = HyphaAdapter()
57
+
58
+ @pn.net
59
+ def MyNet(builder):
60
+ @builder.transition()
61
+ @adapter.log_transition(event_type="process_item")
62
+ async def process(consumed, bb, timebase):
63
+ # Event automatically logged with:
64
+ # - token_count attribute
65
+ # - relationships to consumed tokens
66
+ yield {output: consumed[0]}
67
+
68
+ @builder.place(type=PlaceType.QUEUE)
69
+ @adapter.log_place(event_type="item_arrived")
70
+ def input_place(bb):
71
+ return None
72
+ ```
73
+ """
74
+
75
+ def __init__(self):
76
+ """Initialize the Hypha adapter."""
77
+ self._enabled = True
78
+
79
+ def enable(self):
80
+ """Enable logging for this adapter."""
81
+ self._enabled = True
82
+
83
+ def disable(self):
84
+ """Disable logging for this adapter."""
85
+ self._enabled = False
86
+
87
+ def log_transition(
88
+ self,
89
+ event_type: str,
90
+ attributes: Optional[Union[Dict[str, Any], List[str]]] = None,
91
+ log_inputs: bool = True,
92
+ log_outputs: bool = False,
93
+ ) -> Callable:
94
+ """
95
+ Decorator to log Hypha transition execution.
96
+
97
+ Automatically captures:
98
+ - Token count from consumed tokens
99
+ - Relationships to consumed/produced tokens
100
+ - Attributes from blackboard (if specified)
101
+ - Objects from blackboard with ObjectRef metadata
102
+
103
+ Args:
104
+ event_type: Type of event to log
105
+ attributes: Static attributes or param names to extract
106
+ log_inputs: Whether to log input tokens as related objects
107
+ log_outputs: Whether to log output tokens as related objects
108
+
109
+ Returns:
110
+ Decorator function
111
+ """
112
+ def decorator(func: Callable) -> Callable:
113
+ @functools.wraps(func)
114
+ async def async_wrapper(consumed: List[Any], bb: Any, timebase: Any, state: Any = None):
115
+ # Call original transition
116
+ if state is not None:
117
+ result = func(consumed, bb, timebase, state)
118
+ else:
119
+ result = func(consumed, bb, timebase)
120
+
121
+ # Handle async generator
122
+ if inspect.isasyncgen(result):
123
+ # Log before processing
124
+ await _log_transition_event(
125
+ func, consumed, bb, timebase, event_type, attributes, log_inputs, None
126
+ )
127
+
128
+ # Collect outputs
129
+ outputs = []
130
+ async for yielded in result:
131
+ outputs.append(yielded)
132
+ yield yielded
133
+
134
+ # Log outputs if requested
135
+ if log_outputs:
136
+ await _log_outputs(func, outputs, bb, timebase, event_type)
137
+ else:
138
+ # Handle coroutine
139
+ result = await result
140
+
141
+ # Log event
142
+ await _log_transition_event(
143
+ func, consumed, bb, timebase, event_type, attributes, log_inputs, result
144
+ )
145
+
146
+ # Yield the result if it's not None
147
+ if result is not None:
148
+ yield result
149
+
150
+ @functools.wraps(func)
151
+ def sync_wrapper(consumed: List[Any], bb: Any, timebase: Any, state: Any = None):
152
+ # For sync transitions, log asynchronously
153
+ if state is not None:
154
+ result = func(consumed, bb, timebase, state)
155
+ else:
156
+ result = func(consumed, bb, timebase)
157
+
158
+ # Schedule logging
159
+ asyncio.create_task(_log_transition_event(
160
+ func, consumed, bb, timebase, event_type, attributes, log_inputs, result
161
+ ))
162
+
163
+ return result
164
+
165
+ # Return appropriate wrapper
166
+ if asyncio.iscoroutinefunction(func):
167
+ return async_wrapper # type: ignore
168
+ else:
169
+ return sync_wrapper # type: ignore
170
+
171
+ return decorator
172
+
173
+ def log_place(self, event_type: str = "token_arrived") -> Callable:
174
+ """
175
+ Decorator to log place token arrivals.
176
+
177
+ Note: This requires instrumenting the place handler or using
178
+ a custom place runtime wrapper. For most cases, transition
179
+ logging provides sufficient coverage.
180
+
181
+ Args:
182
+ event_type: Type of event to log
183
+
184
+ Returns:
185
+ Decorator function
186
+ """
187
+ def decorator(func: Callable) -> Callable:
188
+ @functools.wraps(func)
189
+ async def async_wrapper(bb: Any, timebase: Any):
190
+ result = await func(bb, timebase)
191
+
192
+ # Log token arrival
193
+ config = get_config()
194
+ if config.enabled:
195
+ await _log_place_event(func, bb, timebase, event_type)
196
+
197
+ return result
198
+
199
+ @functools.wraps(func)
200
+ def sync_wrapper(bb: Any, timebase: Any):
201
+ result = func(bb, timebase)
202
+
203
+ # Schedule logging
204
+ config = get_config()
205
+ if config.enabled:
206
+ asyncio.create_task(_log_place_event(func, bb, timebase, event_type))
207
+
208
+ return result
209
+
210
+ if asyncio.iscoroutinefunction(func):
211
+ return async_wrapper # type: ignore
212
+ else:
213
+ return sync_wrapper # type: ignore
214
+
215
+ return decorator
216
+
217
+
218
+ async def _log_transition_event(
219
+ func: Callable,
220
+ consumed: List[Any],
221
+ bb: Any,
222
+ timebase: Any,
223
+ event_type: str,
224
+ attributes: Optional[Union[Dict[str, Any], List[str]]],
225
+ log_inputs: bool,
226
+ result: Any,
227
+ ) -> None:
228
+ """Log a transition execution event."""
229
+ config = get_config()
230
+ if not config.enabled:
231
+ return
232
+
233
+ try:
234
+ timestamp = datetime.now()
235
+
236
+ # Build event attributes
237
+ event_attrs = {}
238
+
239
+ # Add token count
240
+ event_attrs["token_count"] = EventAttributeValue(
241
+ name="token_count",
242
+ value=str(len(consumed)),
243
+ time=timestamp
244
+ )
245
+
246
+ # Add transition name
247
+ event_attrs["transition_name"] = EventAttributeValue(
248
+ name="transition_name",
249
+ value=func.__name__,
250
+ time=timestamp
251
+ )
252
+
253
+ # Extract from blackboard
254
+ bb_attrs = extract_attributes_from_blackboard(bb, timestamp)
255
+ event_attrs.update(bb_attrs)
256
+
257
+ # Build relationships from tokens
258
+ relationships = {}
259
+ event_objects = []
260
+
261
+ if log_inputs:
262
+ for token in consumed:
263
+ if token is not None:
264
+ obj = convert_to_ocel_object(token, qualifier="input")
265
+ if obj:
266
+ event_objects.append(obj)
267
+ relationships[obj.id] = Relationship(
268
+ object_id=obj.id,
269
+ qualifier="input"
270
+ )
271
+
272
+ # Extract objects from blackboard
273
+ bb_objects = extract_objects_from_blackboard(bb)
274
+ event_objects.extend(bb_objects)
275
+
276
+ for obj in bb_objects:
277
+ relationships[obj.id] = Relationship(
278
+ object_id=obj.id,
279
+ qualifier="context"
280
+ )
281
+
282
+ # Build event
283
+ event = Event(
284
+ id=generate_event_id(),
285
+ type=event_type,
286
+ time=timestamp,
287
+ attributes=event_attrs,
288
+ relationships=relationships
289
+ )
290
+
291
+ # Send event
292
+ await _send_log_record(LogRecord(event=event))
293
+
294
+ # Send objects to cache
295
+ cache = get_object_cache()
296
+ for obj in event_objects:
297
+ cache.contains_or_add(obj.id, obj)
298
+
299
+ except Exception as e:
300
+ logger.error(f"Failed to log transition event: {e}")
301
+
302
+
303
+ async def _log_outputs(
304
+ func: Callable,
305
+ outputs: List[Any],
306
+ bb: Any,
307
+ timebase: Any,
308
+ event_type: str,
309
+ ) -> None:
310
+ """Log output tokens as related objects."""
311
+ config = get_config()
312
+ if not config.enabled:
313
+ return
314
+
315
+ try:
316
+ timestamp = datetime.now()
317
+ event_objects = []
318
+
319
+ # Process outputs
320
+ for output in outputs:
321
+ if isinstance(output, dict):
322
+ # Dict of place_ref -> token
323
+ for place_ref, token in output.items():
324
+ if place_ref != '*' and token is not None:
325
+ obj = convert_to_ocel_object(token, qualifier="output")
326
+ if obj:
327
+ event_objects.append(obj)
328
+ elif isinstance(output, tuple) and len(output) == 2:
329
+ # (place_ref, token) tuple
330
+ place_ref, token = output
331
+ if token is not None:
332
+ obj = convert_to_ocel_object(token, qualifier="output")
333
+ if obj:
334
+ event_objects.append(obj)
335
+
336
+ # Send objects to cache
337
+ cache = get_object_cache()
338
+ for obj in event_objects:
339
+ cache.contains_or_add(obj.id, obj)
340
+
341
+ except Exception as e:
342
+ logger.error(f"Failed to log outputs: {e}")
343
+
344
+
345
+ async def _log_place_event(
346
+ func: Callable,
347
+ bb: Any,
348
+ timebase: Any,
349
+ event_type: str,
350
+ ) -> None:
351
+ """Log a place token event."""
352
+ config = get_config()
353
+ if not config.enabled:
354
+ return
355
+
356
+ try:
357
+ timestamp = datetime.now()
358
+
359
+ # Build event attributes
360
+ event_attrs = {
361
+ "place_name": EventAttributeValue(
362
+ name="place_name",
363
+ value=func.__name__,
364
+ time=timestamp
365
+ )
366
+ }
367
+
368
+ # Extract from blackboard
369
+ bb_attrs = extract_attributes_from_blackboard(bb, timestamp)
370
+ event_attrs.update(bb_attrs)
371
+
372
+ # Extract objects from blackboard
373
+ bb_objects = extract_objects_from_blackboard(bb)
374
+ relationships = {}
375
+ for obj in bb_objects:
376
+ relationships[obj.id] = Relationship(
377
+ object_id=obj.id,
378
+ qualifier="context"
379
+ )
380
+
381
+ # Build event
382
+ event = Event(
383
+ id=generate_event_id(),
384
+ type=event_type,
385
+ time=timestamp,
386
+ attributes=event_attrs,
387
+ relationships=relationships
388
+ )
389
+
390
+ # Send event
391
+ await _send_log_record(LogRecord(event=event))
392
+
393
+ # Send objects to cache
394
+ cache = get_object_cache()
395
+ for obj in bb_objects:
396
+ cache.contains_or_add(obj.id, obj)
397
+
398
+ except Exception as e:
399
+ logger.error(f"Failed to log place event: {e}")