mycorrhizal 0.1.0__py3-none-any.whl → 0.2.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/_version.py +1 -0
- mycorrhizal/common/__init__.py +15 -3
- mycorrhizal/common/cache.py +114 -0
- mycorrhizal/common/compilation.py +263 -0
- mycorrhizal/common/interface_detection.py +159 -0
- mycorrhizal/common/interfaces.py +3 -50
- mycorrhizal/common/mermaid.py +124 -0
- mycorrhizal/common/wrappers.py +1 -1
- mycorrhizal/hypha/core/builder.py +56 -8
- mycorrhizal/hypha/core/runtime.py +242 -107
- mycorrhizal/hypha/core/specs.py +19 -3
- mycorrhizal/mycelium/__init__.py +174 -0
- mycorrhizal/mycelium/core.py +619 -0
- mycorrhizal/mycelium/exceptions.py +30 -0
- mycorrhizal/mycelium/hypha_bridge.py +1143 -0
- mycorrhizal/mycelium/instance.py +440 -0
- mycorrhizal/mycelium/pn_context.py +276 -0
- mycorrhizal/mycelium/runner.py +165 -0
- mycorrhizal/mycelium/spores_integration.py +655 -0
- mycorrhizal/mycelium/tree_builder.py +102 -0
- mycorrhizal/mycelium/tree_spec.py +197 -0
- mycorrhizal/rhizomorph/README.md +82 -33
- mycorrhizal/rhizomorph/core.py +308 -82
- mycorrhizal/septum/TRANSITION_REFERENCE.md +385 -0
- mycorrhizal/{enoki → septum}/core.py +326 -100
- mycorrhizal/{enoki → septum}/testing_utils.py +7 -7
- mycorrhizal/{enoki → septum}/util.py +44 -21
- mycorrhizal/spores/__init__.py +72 -19
- mycorrhizal/spores/core.py +907 -75
- mycorrhizal/spores/dsl/__init__.py +8 -8
- mycorrhizal/spores/dsl/hypha.py +3 -15
- mycorrhizal/spores/dsl/rhizomorph.py +3 -11
- mycorrhizal/spores/dsl/{enoki.py → septum.py} +26 -77
- mycorrhizal/spores/encoder/json.py +21 -12
- mycorrhizal/spores/extraction.py +14 -11
- mycorrhizal/spores/models.py +75 -20
- mycorrhizal/spores/transport/__init__.py +9 -2
- mycorrhizal/spores/transport/base.py +36 -17
- mycorrhizal/spores/transport/file.py +126 -0
- mycorrhizal-0.2.0.dist-info/METADATA +335 -0
- mycorrhizal-0.2.0.dist-info/RECORD +54 -0
- mycorrhizal-0.1.0.dist-info/METADATA +0 -198
- mycorrhizal-0.1.0.dist-info/RECORD +0 -37
- /mycorrhizal/{enoki → septum}/__init__.py +0 -0
- {mycorrhizal-0.1.0.dist-info → mycorrhizal-0.2.0.dist-info}/WHEEL +0 -0
mycorrhizal/spores/models.py
CHANGED
|
@@ -35,16 +35,16 @@ class EventAttributeValue:
|
|
|
35
35
|
"""
|
|
36
36
|
An attribute value for an event.
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
Event attributes don't have timestamps because the event time itself is sufficient.
|
|
39
39
|
|
|
40
40
|
Attributes:
|
|
41
41
|
name: The attribute name
|
|
42
42
|
value: The string representation of the value
|
|
43
|
-
|
|
43
|
+
type: The data type ("string", "integer", "float", "boolean", "timestamp")
|
|
44
44
|
"""
|
|
45
45
|
name: str
|
|
46
46
|
value: str
|
|
47
|
-
|
|
47
|
+
type: str
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
@dataclass(frozen=True)
|
|
@@ -57,11 +57,13 @@ class ObjectAttributeValue:
|
|
|
57
57
|
Attributes:
|
|
58
58
|
name: The attribute name
|
|
59
59
|
value: The string representation of the value
|
|
60
|
+
type: The data type ("string", "integer", "float", "boolean", "timestamp")
|
|
60
61
|
time: When this attribute value was set
|
|
61
62
|
"""
|
|
62
63
|
name: str
|
|
63
64
|
value: str
|
|
64
|
-
|
|
65
|
+
type: str
|
|
66
|
+
time: Optional[datetime] = None
|
|
65
67
|
|
|
66
68
|
|
|
67
69
|
@dataclass
|
|
@@ -71,14 +73,16 @@ class Event:
|
|
|
71
73
|
|
|
72
74
|
Attributes:
|
|
73
75
|
id: Unique event identifier
|
|
74
|
-
type: Event type/category
|
|
76
|
+
type: Event type/category (event class)
|
|
77
|
+
activity: Optional semantic activity label (defaults to type if null)
|
|
75
78
|
time: When the event occurred
|
|
76
79
|
attributes: Event attributes (name -> EventAttributeValue)
|
|
77
80
|
relationships: Objects related to this event (qualifier -> Relationship)
|
|
78
81
|
"""
|
|
79
82
|
id: str
|
|
80
83
|
type: str
|
|
81
|
-
|
|
84
|
+
activity: Optional[str] = None
|
|
85
|
+
time: datetime = field(default_factory=datetime.now)
|
|
82
86
|
attributes: Dict[str, EventAttributeValue] = field(default_factory=dict)
|
|
83
87
|
relationships: Dict[str, Relationship] = field(default_factory=dict)
|
|
84
88
|
|
|
@@ -174,23 +178,71 @@ class EventAttr:
|
|
|
174
178
|
name: Optional[str] = None
|
|
175
179
|
|
|
176
180
|
|
|
181
|
+
@dataclass(frozen=True)
|
|
182
|
+
class SporesAttr:
|
|
183
|
+
"""
|
|
184
|
+
Metadata annotation for object attributes in OCEL object logging.
|
|
185
|
+
|
|
186
|
+
Used with typing.Annotated to mark fields that should be logged when an object is logged:
|
|
187
|
+
|
|
188
|
+
class Order(BaseModel):
|
|
189
|
+
id: str
|
|
190
|
+
status: Annotated[str, SporesAttr] # Logged
|
|
191
|
+
total: Annotated[float, SporesAttr] # Logged
|
|
192
|
+
items: list[dict] # Not marked - not logged
|
|
193
|
+
|
|
194
|
+
When using @spore.log_event(relationships={...}), fields marked with SporesAttr
|
|
195
|
+
are automatically logged. If attributes list is provided, it overrides SporesAttr.
|
|
196
|
+
|
|
197
|
+
Attributes:
|
|
198
|
+
name: Optional custom name for the attribute (defaults to field name)
|
|
199
|
+
"""
|
|
200
|
+
name: Optional[str] = None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ============================================================================
|
|
204
|
+
# Type Inference
|
|
205
|
+
# ============================================================================
|
|
206
|
+
|
|
207
|
+
def infer_type(value: Any) -> str:
|
|
208
|
+
"""
|
|
209
|
+
Infer the OCEL type for a Python value.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
value: The Python value to infer type for
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
One of: "string", "integer", "float", "boolean", "timestamp"
|
|
216
|
+
"""
|
|
217
|
+
if isinstance(value, bool):
|
|
218
|
+
return "boolean"
|
|
219
|
+
elif isinstance(value, int):
|
|
220
|
+
return "integer"
|
|
221
|
+
elif isinstance(value, float):
|
|
222
|
+
return "float"
|
|
223
|
+
elif isinstance(value, datetime):
|
|
224
|
+
return "timestamp"
|
|
225
|
+
else:
|
|
226
|
+
return "string"
|
|
227
|
+
|
|
228
|
+
|
|
177
229
|
# ============================================================================
|
|
178
230
|
# Attribute Value Conversion
|
|
179
231
|
# ============================================================================
|
|
180
232
|
|
|
181
|
-
def attribute_value_from_python(value: Any
|
|
233
|
+
def attribute_value_from_python(value: Any) -> EventAttributeValue:
|
|
182
234
|
"""
|
|
183
235
|
Convert a Python value to an OCEL EventAttributeValue.
|
|
184
236
|
|
|
185
|
-
All values are converted to strings per OCEL specification.
|
|
186
|
-
|
|
187
237
|
Args:
|
|
188
238
|
value: The Python value to convert
|
|
189
|
-
time: The timestamp for this attribute value
|
|
190
239
|
|
|
191
240
|
Returns:
|
|
192
|
-
An EventAttributeValue with
|
|
241
|
+
An EventAttributeValue with type information
|
|
193
242
|
"""
|
|
243
|
+
# Infer type
|
|
244
|
+
attr_type = infer_type(value)
|
|
245
|
+
|
|
194
246
|
# Handle None
|
|
195
247
|
if value is None:
|
|
196
248
|
str_value = "null"
|
|
@@ -218,30 +270,33 @@ def attribute_value_from_python(value: Any, time: datetime) -> EventAttributeVal
|
|
|
218
270
|
return EventAttributeValue(
|
|
219
271
|
name="", # Name set by caller
|
|
220
272
|
value=str_value,
|
|
221
|
-
|
|
273
|
+
type=attr_type
|
|
222
274
|
)
|
|
223
275
|
|
|
224
276
|
|
|
225
|
-
def object_attribute_from_python(value: Any, time: datetime) -> ObjectAttributeValue:
|
|
277
|
+
def object_attribute_from_python(value: Any, time: Optional[datetime] = None) -> ObjectAttributeValue:
|
|
226
278
|
"""
|
|
227
279
|
Convert a Python value to an OCEL ObjectAttributeValue.
|
|
228
280
|
|
|
229
|
-
Same conversion rules as attribute_value_from_python but for objects.
|
|
230
|
-
|
|
231
281
|
Args:
|
|
232
282
|
value: The Python value to convert
|
|
233
|
-
time: The timestamp for this attribute value
|
|
283
|
+
time: The timestamp for this attribute value (optional)
|
|
234
284
|
|
|
235
285
|
Returns:
|
|
236
|
-
An ObjectAttributeValue with
|
|
286
|
+
An ObjectAttributeValue with type information
|
|
237
287
|
"""
|
|
238
|
-
# Use same
|
|
239
|
-
event_attr = attribute_value_from_python(value
|
|
288
|
+
# Use same type inference logic
|
|
289
|
+
event_attr = attribute_value_from_python(value)
|
|
290
|
+
|
|
291
|
+
# Default to current time if not provided
|
|
292
|
+
if time is None:
|
|
293
|
+
time = datetime.now()
|
|
240
294
|
|
|
241
295
|
return ObjectAttributeValue(
|
|
242
296
|
name=event_attr.name,
|
|
243
297
|
value=event_attr.value,
|
|
244
|
-
|
|
298
|
+
type=event_attr.type,
|
|
299
|
+
time=time
|
|
245
300
|
)
|
|
246
301
|
|
|
247
302
|
|
|
@@ -5,6 +5,13 @@ Spores Transports
|
|
|
5
5
|
Transports for sending OCEL LogRecords to various destinations.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from .base import Transport
|
|
8
|
+
from .base import SyncTransport, AsyncTransport, Transport
|
|
9
|
+
from .file import SyncFileTransport, AsyncFileTransport
|
|
9
10
|
|
|
10
|
-
__all__ = [
|
|
11
|
+
__all__ = [
|
|
12
|
+
'SyncTransport',
|
|
13
|
+
'AsyncTransport',
|
|
14
|
+
'Transport', # Backward compatibility alias for AsyncTransport
|
|
15
|
+
'SyncFileTransport',
|
|
16
|
+
'AsyncFileTransport',
|
|
17
|
+
]
|
|
@@ -2,26 +2,28 @@
|
|
|
2
2
|
"""
|
|
3
3
|
Spores Transport Interface
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Protocols for transporting encoded LogRecords.
|
|
6
|
+
Separate sync and async protocols for clarity.
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
9
|
from __future__ import annotations
|
|
9
10
|
|
|
10
|
-
from
|
|
11
|
-
from typing import Protocol, Callable, Optional
|
|
11
|
+
from typing import Protocol
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
class
|
|
14
|
+
class SyncTransport(Protocol):
|
|
15
15
|
"""
|
|
16
|
-
Protocol for transports
|
|
16
|
+
Protocol for synchronous transports (blocking I/O).
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
Sync transports use blocking send() - they write data immediately
|
|
19
|
+
and block until complete. Use these with get_spore_sync().
|
|
20
|
+
|
|
21
|
+
Examples: file writes, blocking HTTP requests, etc.
|
|
20
22
|
"""
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
def send(self, data: bytes, content_type: str) -> None:
|
|
23
25
|
"""
|
|
24
|
-
Send encoded data to the transport destination.
|
|
26
|
+
Send encoded data to the transport destination (blocking).
|
|
25
27
|
|
|
26
28
|
Args:
|
|
27
29
|
data: The encoded log record data
|
|
@@ -29,18 +31,35 @@ class Transport(Protocol):
|
|
|
29
31
|
"""
|
|
30
32
|
...
|
|
31
33
|
|
|
32
|
-
def
|
|
33
|
-
"""
|
|
34
|
-
|
|
34
|
+
def close(self) -> None:
|
|
35
|
+
"""Close the transport and release resources."""
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AsyncTransport(Protocol):
|
|
40
|
+
"""
|
|
41
|
+
Protocol for asynchronous transports (async I/O).
|
|
42
|
+
|
|
43
|
+
Async transports use non-blocking async send() - they await completion.
|
|
44
|
+
Use these with get_spore_async().
|
|
35
45
|
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
Examples: async file writes, async HTTP requests, message queues, etc.
|
|
47
|
+
"""
|
|
38
48
|
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
async def send(self, data: bytes, content_type: str) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Send encoded data to the transport destination (async).
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
data: The encoded log record data
|
|
55
|
+
content_type: MIME content type (e.g., "application/json")
|
|
41
56
|
"""
|
|
42
57
|
...
|
|
43
58
|
|
|
44
|
-
def close(self) -> None:
|
|
59
|
+
async def close(self) -> None:
|
|
45
60
|
"""Close the transport and release resources."""
|
|
46
61
|
...
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# Backward compatibility alias
|
|
65
|
+
Transport = AsyncTransport
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Spores File Transports
|
|
4
|
+
|
|
5
|
+
File-based transports for OCEL log records.
|
|
6
|
+
Includes both synchronous (blocking I/O) and asynchronous (async I/O) implementations.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import threading
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SyncFileTransport:
|
|
18
|
+
"""
|
|
19
|
+
Synchronous file transport using blocking I/O.
|
|
20
|
+
|
|
21
|
+
Writes log records to a file in JSONL format (one JSON object per line).
|
|
22
|
+
Thread-safe with a lock for concurrent writes.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, filepath: str | Path):
|
|
26
|
+
"""
|
|
27
|
+
Initialize the file transport.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
filepath: Path to the log file. Will be created if it doesn't exist.
|
|
31
|
+
"""
|
|
32
|
+
self.filepath = Path(filepath)
|
|
33
|
+
self.filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
self._file = open(self.filepath, 'a', encoding='utf-8')
|
|
35
|
+
self._lock = threading.Lock()
|
|
36
|
+
|
|
37
|
+
def send(self, data: bytes, content_type: str) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Write data to file (blocking call).
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
data: Encoded log record data
|
|
43
|
+
content_type: MIME content type (e.g., "application/json")
|
|
44
|
+
"""
|
|
45
|
+
with self._lock:
|
|
46
|
+
self._file.write(data.decode('utf-8') + '\n')
|
|
47
|
+
self._file.flush()
|
|
48
|
+
|
|
49
|
+
def close(self) -> None:
|
|
50
|
+
"""Close the file handle."""
|
|
51
|
+
with self._lock:
|
|
52
|
+
if self._file and not self._file.closed:
|
|
53
|
+
self._file.close()
|
|
54
|
+
self._file = None
|
|
55
|
+
|
|
56
|
+
def __enter__(self):
|
|
57
|
+
"""Context manager support."""
|
|
58
|
+
return self
|
|
59
|
+
|
|
60
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
61
|
+
"""Context manager support."""
|
|
62
|
+
self.close()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class AsyncFileTransport:
|
|
66
|
+
"""
|
|
67
|
+
Asynchronous file transport using async I/O.
|
|
68
|
+
|
|
69
|
+
Writes log records to a file in JSONL format (one JSON object per line).
|
|
70
|
+
Uses asyncio.to_thread() for non-blocking async file I/O.
|
|
71
|
+
|
|
72
|
+
This is the async version of SyncFileTransport - use it with get_spore_async().
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(self, filepath: str | Path):
|
|
76
|
+
"""
|
|
77
|
+
Initialize the async file transport.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
filepath: Path to the log file. Will be created if it doesn't exist.
|
|
81
|
+
"""
|
|
82
|
+
self.filepath = Path(filepath)
|
|
83
|
+
self.filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
84
|
+
# Open file in append mode, will be managed by asyncio.to_thread
|
|
85
|
+
self._file = open(self.filepath, 'a', encoding='utf-8')
|
|
86
|
+
self._lock = asyncio.Lock()
|
|
87
|
+
|
|
88
|
+
async def send(self, data: bytes, content_type: str) -> None:
|
|
89
|
+
"""
|
|
90
|
+
Write data to file asynchronously.
|
|
91
|
+
|
|
92
|
+
Uses asyncio.to_thread() to run blocking I/O in a thread pool,
|
|
93
|
+
avoiding the need for external dependencies like aiofiles.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
data: Encoded log record data
|
|
97
|
+
content_type: MIME content type (e.g., "application/json")
|
|
98
|
+
"""
|
|
99
|
+
async with self._lock:
|
|
100
|
+
# Run blocking file I/O in thread pool
|
|
101
|
+
await asyncio.to_thread(self._write_sync, data)
|
|
102
|
+
|
|
103
|
+
def _write_sync(self, data: bytes) -> None:
|
|
104
|
+
"""
|
|
105
|
+
Synchronous write helper - runs in thread pool.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
data: Encoded log record data
|
|
109
|
+
"""
|
|
110
|
+
self._file.write(data.decode('utf-8') + '\n')
|
|
111
|
+
self._file.flush()
|
|
112
|
+
|
|
113
|
+
async def close(self) -> None:
|
|
114
|
+
"""Close the file handle asynchronously."""
|
|
115
|
+
async with self._lock:
|
|
116
|
+
if self._file and not self._file.closed:
|
|
117
|
+
await asyncio.to_thread(self._file.close)
|
|
118
|
+
self._file = None
|
|
119
|
+
|
|
120
|
+
async def __aenter__(self):
|
|
121
|
+
"""Async context manager support."""
|
|
122
|
+
return self
|
|
123
|
+
|
|
124
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
125
|
+
"""Async context manager support."""
|
|
126
|
+
await self.close()
|