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.
- mycorrhizal/__init__.py +3 -0
- mycorrhizal/common/__init__.py +68 -0
- mycorrhizal/common/interface_builder.py +203 -0
- mycorrhizal/common/interfaces.py +412 -0
- mycorrhizal/common/timebase.py +99 -0
- mycorrhizal/common/wrappers.py +532 -0
- mycorrhizal/enoki/__init__.py +0 -0
- mycorrhizal/enoki/core.py +1545 -0
- mycorrhizal/enoki/testing_utils.py +529 -0
- mycorrhizal/enoki/util.py +220 -0
- mycorrhizal/hypha/__init__.py +0 -0
- mycorrhizal/hypha/core/__init__.py +107 -0
- mycorrhizal/hypha/core/builder.py +404 -0
- mycorrhizal/hypha/core/runtime.py +890 -0
- mycorrhizal/hypha/core/specs.py +234 -0
- mycorrhizal/hypha/util.py +38 -0
- mycorrhizal/rhizomorph/README.md +220 -0
- mycorrhizal/rhizomorph/__init__.py +0 -0
- mycorrhizal/rhizomorph/core.py +1729 -0
- mycorrhizal/rhizomorph/util.py +45 -0
- mycorrhizal/spores/__init__.py +124 -0
- mycorrhizal/spores/cache.py +208 -0
- mycorrhizal/spores/core.py +419 -0
- mycorrhizal/spores/dsl/__init__.py +48 -0
- mycorrhizal/spores/dsl/enoki.py +514 -0
- mycorrhizal/spores/dsl/hypha.py +399 -0
- mycorrhizal/spores/dsl/rhizomorph.py +351 -0
- mycorrhizal/spores/encoder/__init__.py +11 -0
- mycorrhizal/spores/encoder/base.py +42 -0
- mycorrhizal/spores/encoder/json.py +159 -0
- mycorrhizal/spores/extraction.py +484 -0
- mycorrhizal/spores/models.py +288 -0
- mycorrhizal/spores/transport/__init__.py +10 -0
- mycorrhizal/spores/transport/base.py +46 -0
- mycorrhizal-0.1.0.dist-info/METADATA +198 -0
- mycorrhizal-0.1.0.dist-info/RECORD +37 -0
- 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
|
+
]
|