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.
Files changed (45) hide show
  1. mycorrhizal/_version.py +1 -0
  2. mycorrhizal/common/__init__.py +15 -3
  3. mycorrhizal/common/cache.py +114 -0
  4. mycorrhizal/common/compilation.py +263 -0
  5. mycorrhizal/common/interface_detection.py +159 -0
  6. mycorrhizal/common/interfaces.py +3 -50
  7. mycorrhizal/common/mermaid.py +124 -0
  8. mycorrhizal/common/wrappers.py +1 -1
  9. mycorrhizal/hypha/core/builder.py +56 -8
  10. mycorrhizal/hypha/core/runtime.py +242 -107
  11. mycorrhizal/hypha/core/specs.py +19 -3
  12. mycorrhizal/mycelium/__init__.py +174 -0
  13. mycorrhizal/mycelium/core.py +619 -0
  14. mycorrhizal/mycelium/exceptions.py +30 -0
  15. mycorrhizal/mycelium/hypha_bridge.py +1143 -0
  16. mycorrhizal/mycelium/instance.py +440 -0
  17. mycorrhizal/mycelium/pn_context.py +276 -0
  18. mycorrhizal/mycelium/runner.py +165 -0
  19. mycorrhizal/mycelium/spores_integration.py +655 -0
  20. mycorrhizal/mycelium/tree_builder.py +102 -0
  21. mycorrhizal/mycelium/tree_spec.py +197 -0
  22. mycorrhizal/rhizomorph/README.md +82 -33
  23. mycorrhizal/rhizomorph/core.py +308 -82
  24. mycorrhizal/septum/TRANSITION_REFERENCE.md +385 -0
  25. mycorrhizal/{enoki → septum}/core.py +326 -100
  26. mycorrhizal/{enoki → septum}/testing_utils.py +7 -7
  27. mycorrhizal/{enoki → septum}/util.py +44 -21
  28. mycorrhizal/spores/__init__.py +72 -19
  29. mycorrhizal/spores/core.py +907 -75
  30. mycorrhizal/spores/dsl/__init__.py +8 -8
  31. mycorrhizal/spores/dsl/hypha.py +3 -15
  32. mycorrhizal/spores/dsl/rhizomorph.py +3 -11
  33. mycorrhizal/spores/dsl/{enoki.py → septum.py} +26 -77
  34. mycorrhizal/spores/encoder/json.py +21 -12
  35. mycorrhizal/spores/extraction.py +14 -11
  36. mycorrhizal/spores/models.py +75 -20
  37. mycorrhizal/spores/transport/__init__.py +9 -2
  38. mycorrhizal/spores/transport/base.py +36 -17
  39. mycorrhizal/spores/transport/file.py +126 -0
  40. mycorrhizal-0.2.0.dist-info/METADATA +335 -0
  41. mycorrhizal-0.2.0.dist-info/RECORD +54 -0
  42. mycorrhizal-0.1.0.dist-info/METADATA +0 -198
  43. mycorrhizal-0.1.0.dist-info/RECORD +0 -37
  44. /mycorrhizal/{enoki → septum}/__init__.py +0 -0
  45. {mycorrhizal-0.1.0.dist-info → mycorrhizal-0.2.0.dist-info}/WHEEL +0 -0
@@ -35,16 +35,16 @@ class EventAttributeValue:
35
35
  """
36
36
  An attribute value for an event.
37
37
 
38
- OCEL attributes include timestamps for versioning.
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
- time: When this attribute value was set
43
+ type: The data type ("string", "integer", "float", "boolean", "timestamp")
44
44
  """
45
45
  name: str
46
46
  value: str
47
- time: datetime
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
- time: datetime
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
- time: datetime
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, time: datetime) -> EventAttributeValue:
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 string representation
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
- time=time
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 string representation
286
+ An ObjectAttributeValue with type information
237
287
  """
238
- # Use same conversion logic
239
- event_attr = attribute_value_from_python(value, time)
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
- time=event_attr.time
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__ = ['Transport']
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
- Abstract base class for transporting encoded LogRecords.
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 abc import ABC, abstractmethod
11
- from typing import Protocol, Callable, Optional
11
+ from typing import Protocol
12
12
 
13
13
 
14
- class Transport(Protocol):
14
+ class SyncTransport(Protocol):
15
15
  """
16
- Protocol for transports that send encoded LogRecords.
16
+ Protocol for synchronous transports (blocking I/O).
17
17
 
18
- Transports handle the actual delivery of log records to consumers
19
- (e.g., HTTP POST, files, message queues, etc.).
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
- async def send(self, data: bytes, content_type: str) -> None:
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 is_async(self) -> bool:
33
- """
34
- Return True if this transport is async (non-blocking).
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
- Async transports don't block the caller when sending data.
37
- They typically use background threads, queues, or async I/O.
46
+ Examples: async file writes, async HTTP requests, message queues, etc.
47
+ """
38
48
 
39
- Returns:
40
- True if transport is async, False if synchronous
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()