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,484 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Spores Data Extraction
4
+
5
+ Extract attributes and objects from function context for event logging.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import inspect
11
+ import logging
12
+ from datetime import datetime
13
+ from typing import Any, Dict, List, Optional, Union, Callable, get_type_hints, Annotated, get_origin, get_args
14
+ from dataclasses import fields
15
+
16
+ from .models import (
17
+ Event, Relationship, EventAttributeValue, Object, ObjectAttributeValue,
18
+ ObjectRef, ObjectScope, EventAttr,
19
+ attribute_value_from_python, object_attribute_from_python,
20
+ generate_object_id
21
+ )
22
+
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ # ============================================================================
28
+ # Attribute Extraction
29
+ # ============================================================================
30
+
31
+ def extract_attributes_from_params(
32
+ func: Callable,
33
+ args: tuple,
34
+ kwargs: dict,
35
+ timestamp: datetime
36
+ ) -> Dict[str, EventAttributeValue]:
37
+ """
38
+ Extract event attributes from function parameters.
39
+
40
+ Args:
41
+ func: The decorated function
42
+ args: Positional arguments passed to function
43
+ kwargs: Keyword arguments passed to function
44
+ timestamp: Event timestamp for attribute values
45
+
46
+ Returns:
47
+ Dictionary of attribute name -> EventAttributeValue
48
+ """
49
+ attributes = {}
50
+
51
+ try:
52
+ # Get function signature
53
+ sig = inspect.signature(func)
54
+ bound_args = sig.bind(*args, **kwargs)
55
+ bound_args.apply_defaults()
56
+
57
+ # Extract from each parameter
58
+ for param_name, param_value in bound_args.arguments.items():
59
+ # Skip certain parameter names
60
+ if param_name in ('self', 'cls', 'bb', 'ctx', 'timebase', 'consumed'):
61
+ continue
62
+
63
+ # Extract attribute value
64
+ attr = attribute_value_from_python(param_value, timestamp)
65
+ attributes[param_name] = attr
66
+
67
+ except Exception as e:
68
+ logger.error(f"Failed to extract attributes from params: {e}")
69
+
70
+ return attributes
71
+
72
+
73
+ def extract_attributes_from_dict(
74
+ data: Dict[str, Any],
75
+ timestamp: datetime
76
+ ) -> Dict[str, EventAttributeValue]:
77
+ """
78
+ Extract event attributes from a dictionary.
79
+
80
+ Args:
81
+ data: Dictionary with attribute values
82
+ timestamp: Event timestamp for attribute values
83
+
84
+ Returns:
85
+ Dictionary of attribute name -> EventAttributeValue
86
+ """
87
+ attributes = {}
88
+
89
+ for key, value in data.items():
90
+ attr = attribute_value_from_python(value, timestamp)
91
+ attributes[key] = attr
92
+
93
+ return attributes
94
+
95
+
96
+ def extract_attributes_from_blackboard(
97
+ bb: Any,
98
+ timestamp: datetime
99
+ ) -> Dict[str, EventAttributeValue]:
100
+ """
101
+ Extract attributes from blackboard fields marked with EventAttr.
102
+
103
+ Args:
104
+ bb: Blackboard object
105
+ timestamp: Event timestamp for attribute values
106
+
107
+ Returns:
108
+ Dictionary of attribute name -> EventAttributeValue
109
+ """
110
+ attributes = {}
111
+
112
+ try:
113
+ # Get the class type hints - try __annotations__ first for Pydantic models
114
+ cls = type(bb)
115
+
116
+ # Pydantic models preserve Annotated in __annotations__
117
+ if hasattr(cls, '__annotations__'):
118
+ hints = cls.__annotations__
119
+ else:
120
+ hints = get_type_hints(cls)
121
+
122
+ for field_name, field_type in hints.items():
123
+ # Check if this is Annotated
124
+ if get_origin(field_type) is Annotated:
125
+ args = get_args(field_type)
126
+ if len(args) >= 2:
127
+ # args[0] is the actual type, args[1:] are metadata
128
+ for metadata in args[1:]:
129
+ # Check if it's an EventAttr (class or instance)
130
+ if metadata is EventAttr or isinstance(metadata, EventAttr):
131
+ # Get the value from blackboard
132
+ if hasattr(bb, field_name):
133
+ value = getattr(bb, field_name)
134
+ # EventAttr might be a class or instance
135
+ if isinstance(metadata, EventAttr):
136
+ attr_name = metadata.name or field_name
137
+ else:
138
+ attr_name = field_name
139
+ attr = attribute_value_from_python(value, timestamp)
140
+ attributes[attr_name] = attr
141
+
142
+ except Exception as e:
143
+ logger.error(f"Failed to extract attributes from blackboard: {e}")
144
+
145
+ return attributes
146
+
147
+
148
+ def evaluate_computed_attributes(
149
+ attr_spec: Dict[str, Any],
150
+ bb: Any,
151
+ timestamp: datetime
152
+ ) -> Dict[str, EventAttributeValue]:
153
+ """
154
+ Evaluate computed/callable attributes.
155
+
156
+ Args:
157
+ attr_spec: Attribute specification (can contain callables)
158
+ bb: Blackboard object for evaluation context
159
+ timestamp: Event timestamp
160
+
161
+ Returns:
162
+ Dictionary of evaluated attributes
163
+ """
164
+ attributes = {}
165
+
166
+ for key, value in attr_spec.items():
167
+ if callable(value):
168
+ # Call the function with blackboard
169
+ try:
170
+ result = value(bb)
171
+ attr = attribute_value_from_python(result, timestamp)
172
+ attributes[key] = attr
173
+ except Exception as e:
174
+ logger.error(f"Failed to compute attribute {key}: {e}")
175
+ else:
176
+ # Static value
177
+ attr = attribute_value_from_python(value, timestamp)
178
+ attributes[key] = attr
179
+
180
+ return attributes
181
+
182
+
183
+ # ============================================================================
184
+ # Object Extraction
185
+ # ============================================================================
186
+
187
+ def extract_objects_from_blackboard(
188
+ bb: Any,
189
+ object_names: Optional[List[str]] = None
190
+ ) -> List[Object]:
191
+ """
192
+ Extract OCEL objects from blackboard fields marked with ObjectRef.
193
+
194
+ Args:
195
+ bb: Blackboard object
196
+ object_names: Optional list of specific field names to extract.
197
+ If None, extracts all global-scope objects.
198
+
199
+ Returns:
200
+ List of OCEL Object instances
201
+ """
202
+ objects = []
203
+
204
+ try:
205
+ # Get the class type hints - try __annotations__ first for Pydantic models
206
+ cls = type(bb)
207
+
208
+ # Pydantic models preserve Annotated in __annotations__
209
+ if hasattr(cls, '__annotations__'):
210
+ hints = cls.__annotations__
211
+ else:
212
+ hints = get_type_hints(cls)
213
+
214
+ for field_name, field_type in hints.items():
215
+ # Check if this is Annotated
216
+ if get_origin(field_type) is Annotated:
217
+ args = get_args(field_type)
218
+ if len(args) >= 2:
219
+ # args[0] is the actual type, args[1:] are metadata
220
+ for metadata in args[1:]:
221
+ # Check if it's an ObjectRef (instance of frozen dataclass)
222
+ if isinstance(metadata, ObjectRef):
223
+ # Check if we should extract this object
224
+ if object_names is not None and field_name not in object_names:
225
+ # Only extract specified objects
226
+ if metadata.scope != ObjectScope.GLOBAL:
227
+ continue
228
+
229
+ if isinstance(metadata.scope, str):
230
+ scope = ObjectScope(metadata.scope)
231
+ else:
232
+ scope = metadata.scope
233
+
234
+ if scope == ObjectScope.EVENT and field_name not in (object_names or []):
235
+ # Event-scope but not requested
236
+ continue
237
+
238
+ # Get the object value
239
+ if hasattr(bb, field_name):
240
+ obj_value = getattr(bb, field_name)
241
+ if obj_value is None:
242
+ continue
243
+
244
+ # Convert to OCEL Object
245
+ ocel_obj = convert_to_ocel_object(
246
+ obj_value,
247
+ metadata.qualifier
248
+ )
249
+ if ocel_obj:
250
+ objects.append(ocel_obj)
251
+
252
+ except Exception as e:
253
+ logger.error(f"Failed to extract objects from blackboard: {e}")
254
+
255
+ return objects
256
+
257
+
258
+ def convert_to_ocel_object(
259
+ obj: Any,
260
+ qualifier: str = "related"
261
+ ) -> Optional[Object]:
262
+ """
263
+ Convert a Python object to an OCEL Object.
264
+
265
+ Args:
266
+ obj: The Python object to convert
267
+ qualifier: Relationship qualifier for this object
268
+
269
+ Returns:
270
+ OCEL Object instance, or None if conversion fails
271
+ """
272
+ try:
273
+ timestamp = datetime.now()
274
+
275
+ # Generate object ID
276
+ obj_id = generate_object_id(obj)
277
+
278
+ # Get object type
279
+ obj_type = getattr(obj, '_spores_object_type', type(obj).__name__)
280
+
281
+ # Extract attributes
282
+ attributes = {}
283
+
284
+ if hasattr(obj, '__dict__'):
285
+ # Extract from instance dict
286
+ for key, value in obj.__dict__.items():
287
+ if not key.startswith('_'):
288
+ attr = object_attribute_from_python(value, timestamp)
289
+ attr = attr.__class__(
290
+ name=key,
291
+ value=attr.value,
292
+ time=attr.time
293
+ )
294
+ attributes[key] = attr
295
+
296
+ elif hasattr(obj, '__dataclass_fields__'):
297
+ # Extract dataclass fields
298
+ for field_info in fields(obj):
299
+ key = field_info.name
300
+ value = getattr(obj, key)
301
+ attr = object_attribute_from_python(value, timestamp)
302
+ attr = attr.__class__(
303
+ name=key,
304
+ value=attr.value,
305
+ time=attr.time
306
+ )
307
+ attributes[key] = attr
308
+
309
+ # Create OCEL Object
310
+ return Object(
311
+ id=obj_id,
312
+ type=obj_type,
313
+ attributes=attributes,
314
+ relationships={}
315
+ )
316
+
317
+ except Exception as e:
318
+ logger.error(f"Failed to convert object to OCEL: {e}")
319
+ return None
320
+
321
+
322
+ def extract_objects_from_spec(
323
+ object_spec: Dict[str, tuple],
324
+ timestamp: datetime
325
+ ) -> List[Object]:
326
+ """
327
+ Extract objects from a manual specification.
328
+
329
+ Args:
330
+ object_spec: Dict of {name: (qualifier, object)} or {name: object}
331
+ timestamp: Timestamp for object attributes
332
+
333
+ Returns:
334
+ List of OCEL Object instances
335
+ """
336
+ objects = []
337
+
338
+ for name, spec in object_spec.items():
339
+ try:
340
+ if isinstance(spec, tuple) and len(spec) == 2:
341
+ qualifier, obj_value = spec
342
+ else:
343
+ qualifier = "related"
344
+ obj_value = spec
345
+
346
+ if obj_value is None:
347
+ continue
348
+
349
+ # Convert to OCEL Object
350
+ obj_id = generate_object_id(obj_value)
351
+ obj_type = getattr(obj_value, '_spores_object_type', type(obj_value).__name__)
352
+
353
+ # Extract attributes
354
+ attributes = {}
355
+ if hasattr(obj_value, '__dict__'):
356
+ for key, value in obj_value.__dict__.items():
357
+ if not key.startswith('_'):
358
+ attr = object_attribute_from_python(value, timestamp)
359
+ attr = attr.__class__(
360
+ name=key,
361
+ value=attr.value,
362
+ time=attr.time
363
+ )
364
+ attributes[key] = attr
365
+
366
+ ocel_obj = Object(
367
+ id=obj_id,
368
+ type=obj_type,
369
+ attributes=attributes,
370
+ relationships={}
371
+ )
372
+
373
+ objects.append(ocel_obj)
374
+
375
+ except Exception as e:
376
+ logger.error(f"Failed to extract object {name}: {e}")
377
+
378
+ return objects
379
+
380
+
381
+ # ============================================================================
382
+ # DSL-Specific Extraction
383
+ # ============================================================================
384
+
385
+ def extract_from_hypha_context(
386
+ consumed: list,
387
+ bb: Any,
388
+ timebase: Any,
389
+ timestamp: datetime
390
+ ) -> tuple[Dict[str, EventAttributeValue], List[Object]]:
391
+ """
392
+ Extract attributes and objects from Hypha transition context.
393
+
394
+ Args:
395
+ consumed: List of consumed tokens
396
+ bb: Blackboard
397
+ timebase: Timebase
398
+ timestamp: Event timestamp
399
+
400
+ Returns:
401
+ Tuple of (attributes, objects)
402
+ """
403
+ attributes = {}
404
+ objects = []
405
+
406
+ # Add token count
407
+ attributes["token_count"] = EventAttributeValue(
408
+ name="token_count",
409
+ value=str(len(consumed)),
410
+ time=timestamp
411
+ )
412
+
413
+ # Extract from blackboard
414
+ bb_attrs = extract_attributes_from_blackboard(bb, timestamp)
415
+ attributes.update(bb_attrs)
416
+
417
+ bb_objects = extract_objects_from_blackboard(bb)
418
+ objects.extend(bb_objects)
419
+
420
+ # Extract objects from tokens
421
+ for token in consumed:
422
+ obj = convert_to_ocel_object(token, qualifier="input")
423
+ if obj:
424
+ objects.append(obj)
425
+
426
+ return attributes, objects
427
+
428
+
429
+ def extract_from_rhizomorph_context(
430
+ bb: Any,
431
+ timestamp: datetime
432
+ ) -> tuple[Dict[str, EventAttributeValue], List[Object]]:
433
+ """
434
+ Extract attributes and objects from Rhizomorph action context.
435
+
436
+ Args:
437
+ bb: Blackboard
438
+ timestamp: Event timestamp
439
+
440
+ Returns:
441
+ Tuple of (attributes, objects)
442
+ """
443
+ # Extract from blackboard
444
+ attributes = extract_attributes_from_blackboard(bb, timestamp)
445
+ objects = extract_objects_from_blackboard(bb)
446
+
447
+ return attributes, objects
448
+
449
+
450
+ def extract_from_enoki_context(
451
+ ctx: Any,
452
+ timestamp: datetime
453
+ ) -> tuple[Dict[str, EventAttributeValue], List[Object]]:
454
+ """
455
+ Extract attributes and objects from Enoki state context.
456
+
457
+ Args:
458
+ ctx: SharedContext
459
+ timestamp: Event timestamp
460
+
461
+ Returns:
462
+ Tuple of (attributes, objects)
463
+ """
464
+ attributes = {}
465
+ objects = []
466
+
467
+ # Add state name if available
468
+ if hasattr(ctx, 'current_state') and ctx.current_state:
469
+ state_name = getattr(ctx.current_state, 'name', str(ctx.current_state))
470
+ attributes["state_name"] = EventAttributeValue(
471
+ name="state_name",
472
+ value=state_name,
473
+ time=timestamp
474
+ )
475
+
476
+ # Extract from common (blackboard)
477
+ if hasattr(ctx, 'common'):
478
+ bb_attrs = extract_attributes_from_blackboard(ctx.common, timestamp)
479
+ attributes.update(bb_attrs)
480
+
481
+ bb_objects = extract_objects_from_blackboard(ctx.common)
482
+ objects.extend(bb_objects)
483
+
484
+ return attributes, objects