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,419 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Spores Core API
4
+
5
+ Main spores module with configuration and decorator functionality.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import inspect
12
+ import functools
13
+ import logging
14
+ from datetime import datetime
15
+ from typing import (
16
+ Any, Callable, Optional, Union, Dict, List,
17
+ ParamSpec, TypeVar, overload
18
+ )
19
+ from dataclasses import dataclass
20
+
21
+ from .models import (
22
+ Event, Object, LogRecord, Relationship,
23
+ EventAttributeValue, ObjectAttributeValue,
24
+ ObjectScope, ObjectRef, EventAttr,
25
+ generate_event_id, generate_object_id,
26
+ attribute_value_from_python, object_attribute_from_python
27
+ )
28
+ from .cache import ObjectLRUCache
29
+ from .encoder import Encoder, JSONEncoder
30
+ from .transport import Transport
31
+ from . import extraction
32
+
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+ # Type variables for decorators
37
+ P = ParamSpec('P')
38
+ R = TypeVar('R')
39
+
40
+
41
+ # ============================================================================
42
+ # Global Configuration
43
+ # ============================================================================
44
+
45
+ @dataclass
46
+ class SporesConfig:
47
+ """
48
+ Global configuration for the Spores logging system.
49
+
50
+ Attributes:
51
+ enabled: Whether spores logging is enabled
52
+ object_cache_size: Maximum objects in LRU cache
53
+ encoder: Encoder instance to use
54
+ transport: Transport instance to use (required for logging)
55
+ """
56
+ enabled: bool = True
57
+ object_cache_size: int = 128
58
+ encoder: Optional[Encoder] = None
59
+ transport: Optional[Transport] = None
60
+
61
+ def __post_init__(self):
62
+ """Set default encoder if not provided."""
63
+ if self.encoder is None:
64
+ self.encoder = JSONEncoder()
65
+
66
+
67
+ # Global state
68
+ _config: Optional[SporesConfig] = None
69
+ _object_cache: Optional[ObjectLRUCache[str, Object]] = None
70
+
71
+
72
+ def configure(
73
+ enabled: bool = True,
74
+ object_cache_size: int = 128,
75
+ encoder: Optional[Encoder] = None,
76
+ transport: Optional[Transport] = None
77
+ ) -> None:
78
+ """
79
+ Configure the Spores logging system.
80
+
81
+ This should be called once at application startup.
82
+
83
+ Args:
84
+ enabled: Whether spores logging is enabled (default: True)
85
+ object_cache_size: Maximum objects in LRU cache (default: 128)
86
+ encoder: Encoder instance (defaults to JSONEncoder)
87
+ transport: Transport instance (required for logging to work)
88
+
89
+ Example:
90
+ ```python
91
+ from mycorrhizal.spores.transport import FileTransport
92
+
93
+ spore.configure(
94
+ transport=FileTransport("logs/ocel.jsonl"),
95
+ object_cache_size=256
96
+ )
97
+ ```
98
+ """
99
+ global _config, _object_cache
100
+
101
+ _config = SporesConfig(
102
+ enabled=enabled,
103
+ object_cache_size=object_cache_size,
104
+ encoder=encoder,
105
+ transport=transport
106
+ )
107
+
108
+ # Create object cache with eviction callback
109
+ def on_evict(object_id: str, obj: Object):
110
+ """Send object when evicted from cache."""
111
+ try:
112
+ loop = asyncio.get_event_loop()
113
+ if loop.is_running():
114
+ asyncio.create_task(_send_log_record(LogRecord(object=obj)))
115
+ else:
116
+ # No running loop, schedule the coroutine
117
+ asyncio.ensure_future(_send_log_record(LogRecord(object=obj)))
118
+ except RuntimeError:
119
+ # No event loop at all, ignore for now
120
+ pass
121
+
122
+ def on_first_sight(object_id: str, obj: Object):
123
+ """Send object when first seen."""
124
+ try:
125
+ loop = asyncio.get_event_loop()
126
+ if loop.is_running():
127
+ asyncio.create_task(_send_log_record(LogRecord(object=obj)))
128
+ else:
129
+ # No running loop, schedule the coroutine
130
+ asyncio.ensure_future(_send_log_record(LogRecord(object=obj)))
131
+ except RuntimeError:
132
+ # No event loop at all, ignore for now
133
+ pass
134
+
135
+ _object_cache = ObjectLRUCache(
136
+ maxsize=object_cache_size,
137
+ on_evict=on_evict,
138
+ on_first_sight=on_first_sight
139
+ )
140
+
141
+ logger.info(f"Spores configured: enabled={enabled}, cache_size={object_cache_size}")
142
+
143
+
144
+ def get_config() -> SporesConfig:
145
+ """Get the current spores configuration."""
146
+ global _config
147
+ if _config is None:
148
+ # Use default configuration
149
+ _config = SporesConfig()
150
+ return _config
151
+
152
+
153
+ def get_object_cache() -> ObjectLRUCache[str, Object]:
154
+ """Get the object cache."""
155
+ global _object_cache
156
+ if _object_cache is None:
157
+ configure() # Initialize with defaults
158
+ return _object_cache
159
+
160
+
161
+ async def _send_log_record(record: LogRecord) -> None:
162
+ """
163
+ Send a log record via the configured transport.
164
+
165
+ Args:
166
+ record: The LogRecord to send
167
+ """
168
+ config = get_config()
169
+
170
+ if not config.enabled:
171
+ return
172
+
173
+ if config.transport is None or config.encoder is None:
174
+ logger.warning("Spores not properly configured (missing transport or encoder)")
175
+ return
176
+
177
+ try:
178
+ # Encode the record
179
+ data = config.encoder.encode(record)
180
+ content_type = config.encoder.content_type()
181
+
182
+ # Send via transport
183
+ await config.transport.send(data, content_type)
184
+
185
+ except Exception as e:
186
+ logger.error(f"Failed to send log record: {e}")
187
+
188
+
189
+ # ============================================================================
190
+ # Decorators
191
+ # ============================================================================
192
+
193
+ class SporeDecorator:
194
+ """
195
+ Main decorator interface for spores logging.
196
+
197
+ Usage:
198
+ ```python
199
+ @spore.event(event_type="process_item")
200
+ async def process(bb: Context) -> Status:
201
+ return Status.SUCCESS
202
+ ```
203
+ """
204
+
205
+ def event(
206
+ self,
207
+ event_type: str,
208
+ attributes: Optional[Union[Dict[str, Any], List[str]]] = None,
209
+ objects: Optional[List[str]] = None
210
+ ) -> Callable[[Callable[P, R]], Callable[P, R]]:
211
+ """
212
+ Decorator to log events when decorated function is called.
213
+
214
+ Args:
215
+ event_type: The type/category of event
216
+ attributes: Event attributes (dict or list of param names)
217
+ objects: List of object fields to include (from interface)
218
+
219
+ Returns:
220
+ Decorator function
221
+ """
222
+ def decorator(func: Callable[P, R]) -> Callable[P, R]:
223
+ @functools.wraps(func)
224
+ async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
225
+ # Call the original function
226
+ result = await func(*args, **kwargs)
227
+
228
+ # Log the event
229
+ await self._log_event(
230
+ func, args, kwargs, event_type, attributes, objects
231
+ )
232
+
233
+ return result
234
+
235
+ @functools.wraps(func)
236
+ def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
237
+ # Call the original function
238
+ result = func(*args, **kwargs)
239
+
240
+ # Log the event asynchronously
241
+ asyncio.create_task(self._log_event(
242
+ func, args, kwargs, event_type, attributes, objects
243
+ ))
244
+
245
+ return result
246
+
247
+ # Return appropriate wrapper based on whether function is async
248
+ if asyncio.iscoroutinefunction(func):
249
+ return async_wrapper # type: ignore
250
+ else:
251
+ return sync_wrapper # type: ignore
252
+
253
+ return decorator
254
+
255
+ return decorator
256
+
257
+ async def _log_event(
258
+ self,
259
+ func: Callable,
260
+ args: tuple,
261
+ kwargs: dict,
262
+ event_type: str,
263
+ attributes: Optional[Union[Dict[str, Any], List[str]]],
264
+ objects: Optional[List[str]]
265
+ ) -> None:
266
+ """Log an event with extracted context."""
267
+ config = get_config()
268
+
269
+ if not config.enabled:
270
+ return
271
+
272
+ try:
273
+ timestamp = datetime.now()
274
+
275
+ # Extract attributes from multiple sources
276
+ event_attrs = {}
277
+
278
+ # 1. Extract from function parameters
279
+ param_attrs = extraction.extract_attributes_from_params(
280
+ func, args, kwargs, timestamp
281
+ )
282
+ event_attrs.update(param_attrs)
283
+
284
+ # 2. Extract from blackboard (if present)
285
+ bb = self._find_blackboard(args, kwargs)
286
+ if bb is not None:
287
+ bb_attrs = extraction.extract_attributes_from_blackboard(bb, timestamp)
288
+ event_attrs.update(bb_attrs)
289
+
290
+ # 3. Static/computed attributes from decorator
291
+ if isinstance(attributes, dict):
292
+ computed_attrs = extraction.evaluate_computed_attributes(
293
+ attributes, bb, timestamp
294
+ )
295
+ event_attrs.update(computed_attrs)
296
+
297
+ # Extract objects
298
+ event_objects = []
299
+
300
+ # 1. Extract from blackboard with ObjectRef metadata
301
+ if bb is not None:
302
+ bb_objects = extraction.extract_objects_from_blackboard(bb, objects)
303
+ event_objects.extend(bb_objects)
304
+
305
+ # 2. Manual object specification
306
+ if isinstance(attributes, dict):
307
+ # Check for object specifications (tuples with qualifier)
308
+ manual_objects = extraction.extract_objects_from_spec(
309
+ attributes, timestamp
310
+ )
311
+ event_objects.extend(manual_objects)
312
+
313
+ # Build relationships
314
+ relationships = {}
315
+ for obj in event_objects:
316
+ rel = Relationship(object_id=obj.id, qualifier="related")
317
+ relationships[obj.id] = rel
318
+
319
+ # Build event
320
+ event = Event(
321
+ id=generate_event_id(),
322
+ type=event_type,
323
+ time=timestamp,
324
+ attributes=event_attrs,
325
+ relationships=relationships
326
+ )
327
+
328
+ # Send event
329
+ await _send_log_record(LogRecord(event=event))
330
+
331
+ # Send objects to cache
332
+ cache = get_object_cache()
333
+ for obj in event_objects:
334
+ cache.contains_or_add(obj.id, obj)
335
+
336
+ except Exception as e:
337
+ logger.error(f"Failed to log event: {e}")
338
+
339
+ def _find_blackboard(self, args: tuple, kwargs: dict) -> Optional[Any]:
340
+ """Find blackboard in function arguments."""
341
+ # Try common parameter names
342
+ for param_name in ('bb', 'blackboard', 'ctx', 'context'):
343
+ if param_name in kwargs:
344
+ return kwargs[param_name]
345
+
346
+ # Try positional arguments
347
+ for i, arg in enumerate(args):
348
+ # Skip self
349
+ if i == 0 and inspect.ismethod(args[0] if args else None):
350
+ continue
351
+
352
+ # Check for common blackboard patterns
353
+ type_name = type(arg).__name__.lower()
354
+ if 'blackboard' in type_name or 'context' in type_name:
355
+ return arg
356
+
357
+ return None
358
+
359
+ def object(self, object_type: str) -> Callable[[type], type]:
360
+ """
361
+ Decorator to mark a class as an OCEL object type.
362
+
363
+ Args:
364
+ object_type: The OCEL object type name
365
+
366
+ Returns:
367
+ Class decorator
368
+
369
+ Example:
370
+ ```python
371
+ @spore.object(object_type="WorkItem")
372
+ class WorkItem(BaseModel):
373
+ id: str
374
+ status: str
375
+ ```
376
+ """
377
+ def decorator(cls: type) -> type:
378
+ # Store metadata on the class
379
+ cls._spores_object_type = object_type
380
+ return cls
381
+
382
+ return decorator
383
+
384
+
385
+ # Global spores decorator instance
386
+ spore = SporeDecorator()
387
+
388
+
389
+ # ============================================================================
390
+ # Module Exports
391
+ # ============================================================================
392
+
393
+ __all__ = [
394
+ 'configure',
395
+ 'get_config',
396
+ 'get_object_cache',
397
+ 'spore',
398
+ 'SporesConfig',
399
+ # Models
400
+ 'Event',
401
+ 'Object',
402
+ 'LogRecord',
403
+ 'Relationship',
404
+ 'EventAttributeValue',
405
+ 'ObjectAttributeValue',
406
+ 'ObjectRef',
407
+ 'ObjectScope',
408
+ 'EventAttr',
409
+ 'generate_event_id',
410
+ 'generate_object_id',
411
+ # Cache
412
+ 'ObjectLRUCache',
413
+ # Encoder
414
+ 'Encoder',
415
+ 'JSONEncoder',
416
+ # Transport
417
+ 'Transport',
418
+ 'HTTPTransport',
419
+ ]
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Spores DSL Adapters
4
+
5
+ DSL-specific adapters for integrating spores logging with:
6
+ - Hypha (Petri nets)
7
+ - Rhizomorph (Behavior trees)
8
+ - Enoki (State machines)
9
+
10
+ Usage:
11
+ ```python
12
+ from mycorrhizal.spores.dsl import HyphaAdapter, RhizomorphAdapter, EnokiAdapter
13
+
14
+ # For Hypha (Petri nets)
15
+ hypha_adapter = HyphaAdapter()
16
+
17
+ @pn.transition()
18
+ @hypha_adapter.log_transition(event_type="process")
19
+ async def process(consumed, bb, timebase):
20
+ yield {output: consumed[0]}
21
+
22
+ # For Rhizomorph (Behavior trees)
23
+ rhizo_adapter = RhizomorphAdapter()
24
+
25
+ @bt.action
26
+ @rhizo_adapter.log_node(event_type="check")
27
+ async def check(bb: Blackboard) -> Status:
28
+ return Status.SUCCESS
29
+
30
+ # For Enoki (State machines)
31
+ enoki_adapter = EnokiAdapter()
32
+
33
+ @enoki.on_state
34
+ @enoki_adapter.log_state(event_type="state_execute")
35
+ async def on_state(ctx: SharedContext):
36
+ return Events.DONE
37
+ ```
38
+ """
39
+
40
+ from .hypha import HyphaAdapter
41
+ from .rhizomorph import RhizomorphAdapter
42
+ from .enoki import EnokiAdapter
43
+
44
+ __all__ = [
45
+ 'HyphaAdapter',
46
+ 'RhizomorphAdapter',
47
+ 'EnokiAdapter',
48
+ ]