mycorrhizal 0.1.0__py3-none-any.whl → 0.1.2__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.
@@ -174,6 +174,28 @@ class EventAttr:
174
174
  name: Optional[str] = None
175
175
 
176
176
 
177
+ @dataclass(frozen=True)
178
+ class SporesAttr:
179
+ """
180
+ Metadata annotation for object attributes in OCEL object logging.
181
+
182
+ Used with typing.Annotated to mark fields that should be logged when an object is logged:
183
+
184
+ class Order(BaseModel):
185
+ id: str
186
+ status: Annotated[str, SporesAttr] # Logged
187
+ total: Annotated[float, SporesAttr] # Logged
188
+ items: list[dict] # Not marked - not logged
189
+
190
+ When using @spore.log_event(relationships={...}), fields marked with SporesAttr
191
+ are automatically logged. If attributes list is provided, it overrides SporesAttr.
192
+
193
+ Attributes:
194
+ name: Optional custom name for the attribute (defaults to field name)
195
+ """
196
+ name: Optional[str] = None
197
+
198
+
177
199
  # ============================================================================
178
200
  # Attribute Value Conversion
179
201
  # ============================================================================
@@ -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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mycorrhizal
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Utilities and DSLs for modelling and implementing safe, performant, structured systems
5
5
  Author-email: Jeff Ciesielski <jeffciesielski@gmail.com>
6
6
  Requires-Python: >=3.10
@@ -1,4 +1,5 @@
1
1
  mycorrhizal/__init__.py,sha256=ajz1GSNU9xYVrFEDSz6Xwg7amWQ_yvW75tQa1ZvRIWc,3
2
+ mycorrhizal/_version.py,sha256=08TK2sYzQF2Wqm6gpxZEgWYYF9ZjbInObIzS2R2yxiE,18
2
3
  mycorrhizal/common/__init__.py,sha256=vAL2NZFFVcoGoPlIREKRG0mrgx1prVTGTlWBopJ76Sw,1450
3
4
  mycorrhizal/common/interface_builder.py,sha256=rBJzduqZ6EjD9MNGO669BcjkM9mYvsnMG0F1kd1wWaQ,6895
4
5
  mycorrhizal/common/interfaces.py,sha256=fWw6bm2ejFmKUnl2pSTiSEwTP0Id8Kowg827_Mqir-s,12261
@@ -11,18 +12,18 @@ mycorrhizal/enoki/util.py,sha256=oJ9vmHrbUdX99Rkh7bBeUHFzM5sS3yrtXOTQDA1524I,843
11
12
  mycorrhizal/hypha/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
13
  mycorrhizal/hypha/util.py,sha256=tpvjWXTdGhtVUycQElcQg4usOy-tSEqwE9KEni8R30s,906
13
14
  mycorrhizal/hypha/core/__init__.py,sha256=o6BE3fhRKjVfyIWwtNTO64kPt0udlZGyXr-ZuN_ZzF8,2518
14
- mycorrhizal/hypha/core/builder.py,sha256=3oXG235bllLql8v2IiIa9K7ska_e4cHc8tvGGWwWZzA,15210
15
+ mycorrhizal/hypha/core/builder.py,sha256=GyjDWG3ZTO_0OIhJNrty4-SaJ9RGsNIzHyOQBCNKDfc,16912
15
16
  mycorrhizal/hypha/core/runtime.py,sha256=K7EDxPTYu9lyHoolBGrrOTws7f1xmbnU2SBVVLnoD3g,34848
16
- mycorrhizal/hypha/core/specs.py,sha256=-MH4-ast3uxIWns3VpYV9vC_vqfjLCbvj61nbAOImn8,7882
17
+ mycorrhizal/hypha/core/specs.py,sha256=uWvW9x7-bSoSENIUJkYMIkJ3KxsgE9mB9PyRLPrOEjk,8429
17
18
  mycorrhizal/rhizomorph/README.md,sha256=g-G8d00VgMOPriggsNveH7HqLhnJ9HlvoIzrwmUEy_M,8134
18
19
  mycorrhizal/rhizomorph/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- mycorrhizal/rhizomorph/core.py,sha256=BmgTbnzF2fzYhjQK4iAuO85LHCsN9-dKGok6U8bDv-g,58668
20
+ mycorrhizal/rhizomorph/core.py,sha256=FrV7S6coulaeqhYfHmNE9x6tmlGFBYcqO04zOdpwNNs,60816
20
21
  mycorrhizal/rhizomorph/util.py,sha256=dUgKAWX5RNtzFK01mGkz10tJyeTMCqyyfnZ8Mi97LkY,1280
21
- mycorrhizal/spores/__init__.py,sha256=y-KPzgUMVXJKjClgQXmsykdWNe2g_U42pWRvdmmCqbg,2836
22
+ mycorrhizal/spores/__init__.py,sha256=VI0y_723Fo1la-nJGJweBWc8PKvpd_VmmT9EBtVrEog,4763
22
23
  mycorrhizal/spores/cache.py,sha256=0FuWHKudgyC7vlrlcJ_kmRKr7OAJuC2gxRURReMlqF4,6119
23
- mycorrhizal/spores/core.py,sha256=mfo1dQtDeyyYedIiaQ16YPhcBpIe-4wgzWXiMCjIj1I,12310
24
+ mycorrhizal/spores/core.py,sha256=KGv3X6w6pz5RiUD__7MMrYUXvDwGUfWFU_eCvDcQSUg,38624
24
25
  mycorrhizal/spores/extraction.py,sha256=YnUGABcVgZVSDCXyOQewWG62NAkdievJrXPIA_eJkxg,14918
25
- mycorrhizal/spores/models.py,sha256=P5hlBaKmK6pCyLxBwGgPB4U9ef29_9fEoNEbYXJLV6g,7964
26
+ mycorrhizal/spores/models.py,sha256=Gm_7jwPNgWhYCK5XDIUiN0DfDp4kJEkSbqxpwPdlsJE,8712
26
27
  mycorrhizal/spores/dsl/__init__.py,sha256=bkxHTkaPrDWUlU1pjYlGJOM-772M5_k7R4aZ8HaxMNM,1153
27
28
  mycorrhizal/spores/dsl/enoki.py,sha256=eVYNBp3ESZ7lK91vE8H5JWkXX14pOtf6cdAWSLiqAsE,15506
28
29
  mycorrhizal/spores/dsl/hypha.py,sha256=vcJimWbZT2MmGhj8Lbn3izF1PO9HVJe1YQi5P3EiZ6o,11959
@@ -30,8 +31,9 @@ mycorrhizal/spores/dsl/rhizomorph.py,sha256=ChG2fRQD9DR90dLKLYSQR72asC-7eLccvBwZ
30
31
  mycorrhizal/spores/encoder/__init__.py,sha256=hpp1W4xwQUG1N1zdrea0coBPtMf-WYSE77Q780TinP8,204
31
32
  mycorrhizal/spores/encoder/base.py,sha256=xFKk-f5_W_HNrOgNXyquUwhcrcz28LmeT5Y5enDVrwU,870
32
33
  mycorrhizal/spores/encoder/json.py,sha256=dyQSTXfG0VQPSNt9ed8VEgMwxFUPnLtV6u_bSRuEnNY,4747
33
- mycorrhizal/spores/transport/__init__.py,sha256=-QYmAwqaTbhrgEXr16qSYWx2f6J22d0SklPlm_Y8RWo,168
34
- mycorrhizal/spores/transport/base.py,sha256=lJ2sLlXd-ZDeMn3ujSoYTneUB3Zwlz0mpWh5yxKK6Lw,1201
35
- mycorrhizal-0.1.0.dist-info/METADATA,sha256=fKifHtnUMVeIKv13_kukZ6rtotKusA3m20VSXRr9qK0,4895
36
- mycorrhizal-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
37
- mycorrhizal-0.1.0.dist-info/RECORD,,
34
+ mycorrhizal/spores/transport/__init__.py,sha256=Tw6glCXsPK2pzsLPbb9vKOCvvT9vh-ql-xUaulKBAw0,407
35
+ mycorrhizal/spores/transport/base.py,sha256=OsyWx1J8Ig7Y7idUoI9oS-LkONwQAxAP47jL_5OVRjA,1668
36
+ mycorrhizal/spores/transport/file.py,sha256=xcMPxAGiv5q-GfcNKsF_3a-2-yxlDkhHusZli1Fsqa0,3833
37
+ mycorrhizal-0.1.2.dist-info/METADATA,sha256=c_xR3AeCwrC8jhbnimBcfu_7jmFItM8UdgAR2Wi8RYQ,4895
38
+ mycorrhizal-0.1.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
39
+ mycorrhizal-0.1.2.dist-info/RECORD,,