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,99 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from time import monotonic
|
|
4
|
+
import asyncio
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Timebase(ABC):
|
|
8
|
+
@abstractmethod
|
|
9
|
+
def now(self) -> float:
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
async def sleep(self, duration: float):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
def advance(self):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
def reset(self):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
def set(self, val: float):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class WallClock(Timebase):
|
|
27
|
+
def now(self) -> float:
|
|
28
|
+
return datetime.now().timestamp()
|
|
29
|
+
|
|
30
|
+
async def sleep(self, duration: float):
|
|
31
|
+
await asyncio.sleep(duration)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class UTCClock(Timebase):
|
|
35
|
+
def now(self) -> float:
|
|
36
|
+
return datetime.now(timezone.utc).timestamp()
|
|
37
|
+
|
|
38
|
+
async def sleep(self, duration: float):
|
|
39
|
+
await asyncio.sleep(duration)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class MonotonicClock(Timebase):
|
|
43
|
+
def now(self) -> float:
|
|
44
|
+
return monotonic()
|
|
45
|
+
|
|
46
|
+
async def sleep(self, duration: float):
|
|
47
|
+
await asyncio.sleep(duration)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class CycleClock(Timebase):
|
|
51
|
+
def __init__(self, stepsize: float = 1):
|
|
52
|
+
super().__init__()
|
|
53
|
+
self.cycles: float = 0
|
|
54
|
+
self._stepsize = stepsize
|
|
55
|
+
self._advance_evt = asyncio.Event()
|
|
56
|
+
|
|
57
|
+
async def sleep(self, duration: float):
|
|
58
|
+
target = self.cycles + duration
|
|
59
|
+
while self.cycles < target:
|
|
60
|
+
await self._advance_evt.wait()
|
|
61
|
+
|
|
62
|
+
def now(self) -> float:
|
|
63
|
+
return self.cycles
|
|
64
|
+
|
|
65
|
+
def advance(self):
|
|
66
|
+
self.cycles += self._stepsize
|
|
67
|
+
|
|
68
|
+
# Pulse the event to wake up sleepers
|
|
69
|
+
self._advance_evt.set()
|
|
70
|
+
self._advance_evt.clear()
|
|
71
|
+
|
|
72
|
+
def reset(self):
|
|
73
|
+
self.cycles = 0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class DictatedClock(Timebase):
|
|
77
|
+
def __init__(self, initial: float):
|
|
78
|
+
super().__init__()
|
|
79
|
+
self.value = initial
|
|
80
|
+
self._initial = initial
|
|
81
|
+
self._set_evt = asyncio.Event()
|
|
82
|
+
|
|
83
|
+
async def sleep(self, duration: float):
|
|
84
|
+
target = self.value + duration
|
|
85
|
+
while self.value < target:
|
|
86
|
+
await self._set_evt.wait()
|
|
87
|
+
|
|
88
|
+
def now(self) -> float:
|
|
89
|
+
return self.value
|
|
90
|
+
|
|
91
|
+
def reset(self):
|
|
92
|
+
self.value = self._initial
|
|
93
|
+
|
|
94
|
+
def set(self, val: float):
|
|
95
|
+
self.value = val
|
|
96
|
+
|
|
97
|
+
# Pulse the event to wake up sleepers
|
|
98
|
+
self._set_evt.set()
|
|
99
|
+
self._set_evt.clear()
|
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Blackboard Wrapper Classes for Constrained Access
|
|
3
|
+
|
|
4
|
+
This module provides wrapper classes that enforce interface constraints at runtime,
|
|
5
|
+
enabling type-safe, constrained access to blackboard state.
|
|
6
|
+
|
|
7
|
+
Key concepts:
|
|
8
|
+
- ReadOnlyView: Enforces read-only access to specified fields
|
|
9
|
+
- WriteOnlyView: Enforces write-only access to specified fields
|
|
10
|
+
- ConstrainedView: Provides access to subset of fields with permissions
|
|
11
|
+
- CompositeView: Combines multiple views into single interface
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import TypeVar, Generic, Type, Protocol, Any, Set, Dict, Optional
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
|
|
17
|
+
from mycorrhizal.common.interfaces import (
|
|
18
|
+
InterfaceMetadata,
|
|
19
|
+
_extract_interface_metadata,
|
|
20
|
+
get_interface_fields,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ============================================================================
|
|
25
|
+
# Type Variables
|
|
26
|
+
# ============================================================================
|
|
27
|
+
|
|
28
|
+
T = TypeVar('T')
|
|
29
|
+
P = TypeVar('P')
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ============================================================================
|
|
33
|
+
# Exceptions
|
|
34
|
+
# ============================================================================
|
|
35
|
+
|
|
36
|
+
class AccessControlError(Exception):
|
|
37
|
+
"""Raised when access control is violated"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, message: str, field_name: str, operation: str):
|
|
40
|
+
self.field_name = field_name
|
|
41
|
+
self.operation = operation
|
|
42
|
+
super().__init__(message)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ============================================================================
|
|
46
|
+
# Base Wrapper Class
|
|
47
|
+
# ============================================================================
|
|
48
|
+
|
|
49
|
+
class BaseWrapper:
|
|
50
|
+
"""
|
|
51
|
+
Base class for all wrapper classes.
|
|
52
|
+
|
|
53
|
+
Provides common functionality for attribute access interception
|
|
54
|
+
and error handling.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
__slots__ = ('_bb', '_allowed_fields', '_private_attrs')
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
bb: Any,
|
|
62
|
+
allowed_fields: Set[str],
|
|
63
|
+
private_attrs: Optional[Dict[str, Any]] = None
|
|
64
|
+
):
|
|
65
|
+
"""
|
|
66
|
+
Initialize the wrapper.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
bb: The underlying blackboard object
|
|
70
|
+
allowed_fields: Set of field names that can be accessed
|
|
71
|
+
private_attrs: Optional dict of private attributes for the wrapper itself
|
|
72
|
+
"""
|
|
73
|
+
object.__setattr__(self, '_bb', bb)
|
|
74
|
+
object.__setattr__(self, '_allowed_fields', allowed_fields)
|
|
75
|
+
object.__setattr__(self, '_private_attrs', private_attrs or {})
|
|
76
|
+
|
|
77
|
+
def __getattr__(self, name: str) -> Any:
|
|
78
|
+
"""Get attribute from underlying blackboard"""
|
|
79
|
+
if name in object.__getattribute__(self, '_allowed_fields'):
|
|
80
|
+
bb = object.__getattribute__(self, '_bb')
|
|
81
|
+
try:
|
|
82
|
+
return getattr(bb, name)
|
|
83
|
+
except AttributeError:
|
|
84
|
+
# Re-raise with more helpful message
|
|
85
|
+
raise AttributeError(
|
|
86
|
+
f"Field '{name}' does not exist on the underlying blackboard"
|
|
87
|
+
) from None
|
|
88
|
+
raise AttributeError(
|
|
89
|
+
f"Field '{name}' not accessible through this interface"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
93
|
+
"""Set attribute on underlying blackboard (to be overridden by subclasses)"""
|
|
94
|
+
# Default implementation allows all subclasses to override
|
|
95
|
+
if name.startswith('_') or name in ('_bb', '_allowed_fields', '_private_attrs'):
|
|
96
|
+
object.__setattr__(self, name, value)
|
|
97
|
+
else:
|
|
98
|
+
raise AttributeError(
|
|
99
|
+
f"Cannot set field '{name}' through this interface"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def __dir__(self) -> list[str]:
|
|
103
|
+
"""Return list of accessible attributes"""
|
|
104
|
+
allowed = object.__getattribute__(self, '_allowed_fields')
|
|
105
|
+
return sorted(list(allowed))
|
|
106
|
+
|
|
107
|
+
def __repr__(self) -> str:
|
|
108
|
+
"""Return string representation"""
|
|
109
|
+
cls_name = self.__class__.__name__
|
|
110
|
+
allowed = object.__getattribute__(self, '_allowed_fields')
|
|
111
|
+
return f"<{cls_name} fields={sorted(allowed)}>"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ============================================================================
|
|
115
|
+
# Read-Only View
|
|
116
|
+
# ============================================================================
|
|
117
|
+
|
|
118
|
+
class ReadOnlyView(BaseWrapper, Generic[T]):
|
|
119
|
+
"""
|
|
120
|
+
Read-only view of specific blackboard fields.
|
|
121
|
+
|
|
122
|
+
Provides read access to specified fields while preventing any modifications.
|
|
123
|
+
Useful for creating safe, immutable views of shared state.
|
|
124
|
+
|
|
125
|
+
Type Parameters:
|
|
126
|
+
T: The type of the interface being viewed
|
|
127
|
+
|
|
128
|
+
Example:
|
|
129
|
+
>>> class Blackboard:
|
|
130
|
+
... def __init__(self):
|
|
131
|
+
... self.config_value = 42
|
|
132
|
+
... self.mutable_state = 0
|
|
133
|
+
>>> bb = Blackboard()
|
|
134
|
+
>>> view = ReadOnlyView(bb, {'config_value'})
|
|
135
|
+
>>> view.config_value # Read works
|
|
136
|
+
42
|
|
137
|
+
>>> view.config_value = 100 # Write raises
|
|
138
|
+
AttributeError: Field 'config_value' is read-only
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
__slots__ = ()
|
|
142
|
+
|
|
143
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
144
|
+
"""Prevent all modifications"""
|
|
145
|
+
if name.startswith('_') or name in ('_bb', '_allowed_fields', '_private_attrs'):
|
|
146
|
+
object.__setattr__(self, name, value)
|
|
147
|
+
else:
|
|
148
|
+
raise AccessControlError(
|
|
149
|
+
f"Field '{name}' is read-only and cannot be modified",
|
|
150
|
+
field_name=name,
|
|
151
|
+
operation='write'
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ============================================================================
|
|
156
|
+
# Write-Only View
|
|
157
|
+
# ============================================================================
|
|
158
|
+
|
|
159
|
+
class WriteOnlyView(BaseWrapper, Generic[T]):
|
|
160
|
+
"""
|
|
161
|
+
Write-only view of specific blackboard fields.
|
|
162
|
+
|
|
163
|
+
Provides write access to specified fields while preventing reads.
|
|
164
|
+
Useful for output interfaces and sinks.
|
|
165
|
+
|
|
166
|
+
Type Parameters:
|
|
167
|
+
T: The type of the interface being viewed
|
|
168
|
+
|
|
169
|
+
Example:
|
|
170
|
+
>>> class Blackboard:
|
|
171
|
+
... def __init__(self):
|
|
172
|
+
... self.output_value = None
|
|
173
|
+
>>> bb = Blackboard()
|
|
174
|
+
>>> view = WriteOnlyView(bb, {'output_value'})
|
|
175
|
+
>>> view.output_value = 42 # Write works
|
|
176
|
+
>>> value = view.output_value # Read raises
|
|
177
|
+
AttributeError: Field 'output_value' is write-only (cannot read)
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
__slots__ = ()
|
|
181
|
+
|
|
182
|
+
def __getattr__(self, name: str) -> Any:
|
|
183
|
+
"""Prevent reads"""
|
|
184
|
+
allowed = object.__getattribute__(self, '_allowed_fields')
|
|
185
|
+
if name in allowed:
|
|
186
|
+
raise AccessControlError(
|
|
187
|
+
f"Field '{name}' is write-only and cannot be read",
|
|
188
|
+
field_name=name,
|
|
189
|
+
operation='read'
|
|
190
|
+
)
|
|
191
|
+
raise AttributeError(
|
|
192
|
+
f"Field '{name}' not accessible through this interface"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
196
|
+
"""Allow writes to specified fields"""
|
|
197
|
+
if name.startswith('_') or name in ('_bb', '_allowed_fields', '_private_attrs'):
|
|
198
|
+
object.__setattr__(self, name, value)
|
|
199
|
+
elif name in object.__getattribute__(self, '_allowed_fields'):
|
|
200
|
+
bb = object.__getattribute__(self, '_bb')
|
|
201
|
+
setattr(bb, name, value)
|
|
202
|
+
else:
|
|
203
|
+
raise AttributeError(
|
|
204
|
+
f"Field '{name}' not accessible through this interface"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ============================================================================
|
|
209
|
+
# Constrained View
|
|
210
|
+
# ============================================================================
|
|
211
|
+
|
|
212
|
+
class ConstrainedView(BaseWrapper, Generic[T, P]):
|
|
213
|
+
"""
|
|
214
|
+
Constrained access view for interface-based access.
|
|
215
|
+
|
|
216
|
+
Provides access to a subset of fields with specified permissions.
|
|
217
|
+
Read-only fields cannot be modified, read-write fields can be both read and written.
|
|
218
|
+
|
|
219
|
+
Type Parameters:
|
|
220
|
+
T: The type of the interface being viewed
|
|
221
|
+
P: The Protocol type that defines the interface
|
|
222
|
+
|
|
223
|
+
Example:
|
|
224
|
+
>>> from mycorrhizal.common.wrappers import ConstrainedView
|
|
225
|
+
>>> class Blackboard:
|
|
226
|
+
... def __init__(self):
|
|
227
|
+
... self.max_tasks = 10
|
|
228
|
+
... self.tasks_completed = 0
|
|
229
|
+
>>> bb = Blackboard()
|
|
230
|
+
>>> view = ConstrainedView(
|
|
231
|
+
... bb,
|
|
232
|
+
... allowed_fields={'max_tasks', 'tasks_completed'},
|
|
233
|
+
... readonly_fields={'max_tasks'}
|
|
234
|
+
... )
|
|
235
|
+
>>> view.max_tasks # Read works
|
|
236
|
+
10
|
|
237
|
+
>>> view.max_tasks = 20 # Write raises (read-only)
|
|
238
|
+
AttributeError: Field 'max_tasks' is read-only
|
|
239
|
+
>>> view.tasks_completed = 5 # Write works (read-write)
|
|
240
|
+
>>> view.tasks_completed # Read works
|
|
241
|
+
5
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
__slots__ = ('_readonly_fields',)
|
|
245
|
+
|
|
246
|
+
def __init__(
|
|
247
|
+
self,
|
|
248
|
+
bb: Any,
|
|
249
|
+
allowed_fields: Set[str],
|
|
250
|
+
readonly_fields: Set[str] | None = None,
|
|
251
|
+
private_attrs: Optional[Dict[str, Any]] = None
|
|
252
|
+
):
|
|
253
|
+
"""
|
|
254
|
+
Initialize the constrained view.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
bb: The underlying blackboard object
|
|
258
|
+
allowed_fields: Set of field names that can be accessed
|
|
259
|
+
readonly_fields: Set of field names that are read-only
|
|
260
|
+
private_attrs: Optional dict of private attributes for the wrapper itself
|
|
261
|
+
"""
|
|
262
|
+
readonly_fields = readonly_fields or set()
|
|
263
|
+
super().__init__(bb, allowed_fields, private_attrs)
|
|
264
|
+
object.__setattr__(self, '_readonly_fields', readonly_fields)
|
|
265
|
+
|
|
266
|
+
def __getattr__(self, name: str) -> Any:
|
|
267
|
+
"""Get attribute from underlying blackboard"""
|
|
268
|
+
allowed = object.__getattribute__(self, '_allowed_fields')
|
|
269
|
+
if name in allowed:
|
|
270
|
+
bb = object.__getattribute__(self, '_bb')
|
|
271
|
+
return getattr(bb, name)
|
|
272
|
+
raise AttributeError(
|
|
273
|
+
f"Field '{name}' not accessible through this interface"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
277
|
+
"""Set attribute with read-only enforcement"""
|
|
278
|
+
if name.startswith('_') or name in ('_bb', '_allowed_fields', '_readonly_fields', '_private_attrs'):
|
|
279
|
+
object.__setattr__(self, name, value)
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
allowed = object.__getattribute__(self, '_allowed_fields')
|
|
283
|
+
if name not in allowed:
|
|
284
|
+
raise AttributeError(
|
|
285
|
+
f"Field '{name}' not accessible through this interface"
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
readonly = object.__getattribute__(self, '_readonly_fields')
|
|
289
|
+
if name in readonly:
|
|
290
|
+
raise AccessControlError(
|
|
291
|
+
f"Field '{name}' is read-only and cannot be modified",
|
|
292
|
+
field_name=name,
|
|
293
|
+
operation='write'
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
bb = object.__getattribute__(self, '_bb')
|
|
297
|
+
setattr(bb, name, value)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# ============================================================================
|
|
301
|
+
# Composite View
|
|
302
|
+
# ============================================================================
|
|
303
|
+
|
|
304
|
+
class CompositeView(BaseWrapper, Generic[T]):
|
|
305
|
+
"""
|
|
306
|
+
Combines multiple views into a single interface.
|
|
307
|
+
|
|
308
|
+
Allows composing multiple constrained views into one, enabling
|
|
309
|
+
complex access patterns by combining simpler views.
|
|
310
|
+
|
|
311
|
+
Type Parameters:
|
|
312
|
+
T: The type of the composite interface
|
|
313
|
+
|
|
314
|
+
Example:
|
|
315
|
+
>>> config_view = ReadOnlyView(bb, {'max_tasks'})
|
|
316
|
+
>>> state_view = ConstrainedView(bb, {'tasks_completed'}, readonly_fields=set())
|
|
317
|
+
>>> composite = CompositeView.combine([config_view, state_view])
|
|
318
|
+
>>> composite.max_tasks # Read from config_view
|
|
319
|
+
10
|
|
320
|
+
>>> composite.tasks_completed = 5 # Write through state_view
|
|
321
|
+
>>> composite.max_tasks = 20 # Raises (read-only)
|
|
322
|
+
AttributeError: Field 'max_tasks' is read-only
|
|
323
|
+
"""
|
|
324
|
+
|
|
325
|
+
__slots__ = ('_views',)
|
|
326
|
+
|
|
327
|
+
def __init__(self, bb: Any, views: list[BaseWrapper]):
|
|
328
|
+
"""
|
|
329
|
+
Initialize composite view from multiple views.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
bb: The underlying blackboard object
|
|
333
|
+
views: List of wrapper objects to compose
|
|
334
|
+
"""
|
|
335
|
+
# Collect all allowed fields from all views
|
|
336
|
+
allowed_fields: Set[str] = set()
|
|
337
|
+
private_attrs: Dict[str, Any] = {'_views': views}
|
|
338
|
+
|
|
339
|
+
for view in views:
|
|
340
|
+
allowed_fields.update(view._allowed_fields)
|
|
341
|
+
|
|
342
|
+
super().__init__(bb, allowed_fields, private_attrs)
|
|
343
|
+
object.__setattr__(self, '_views', views)
|
|
344
|
+
|
|
345
|
+
@classmethod
|
|
346
|
+
def combine(cls, views: list[BaseWrapper]) -> 'CompositeView[T]':
|
|
347
|
+
"""
|
|
348
|
+
Combine multiple views into a composite view.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
views: List of wrapper objects to combine
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
A new CompositeView that combines all input views
|
|
355
|
+
|
|
356
|
+
Raises:
|
|
357
|
+
ValueError: If views reference different blackboards
|
|
358
|
+
"""
|
|
359
|
+
if not views:
|
|
360
|
+
raise ValueError("Cannot combine empty list of views")
|
|
361
|
+
|
|
362
|
+
# Get the blackboard from the first view
|
|
363
|
+
bb = views[0]._bb
|
|
364
|
+
|
|
365
|
+
# Verify all views reference the same blackboard
|
|
366
|
+
for view in views:
|
|
367
|
+
if view._bb is not bb:
|
|
368
|
+
raise ValueError("All views must reference the same blackboard")
|
|
369
|
+
|
|
370
|
+
return cls(bb, views)
|
|
371
|
+
|
|
372
|
+
def __getattr__(self, name: str) -> Any:
|
|
373
|
+
"""Delegate read to first view that has this field"""
|
|
374
|
+
views = object.__getattribute__(self, '_views')
|
|
375
|
+
for view in views:
|
|
376
|
+
if name in view._allowed_fields:
|
|
377
|
+
return getattr(view, name)
|
|
378
|
+
|
|
379
|
+
raise AttributeError(
|
|
380
|
+
f"Field '{name}' not accessible through any composed view"
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
384
|
+
"""Delegate write to first view that has this field"""
|
|
385
|
+
if name.startswith('_') or name in ('_bb', '_allowed_fields', '_private_attrs', '_views'):
|
|
386
|
+
object.__setattr__(self, name, value)
|
|
387
|
+
return
|
|
388
|
+
|
|
389
|
+
views = object.__getattribute__(self, '_views')
|
|
390
|
+
for view in views:
|
|
391
|
+
if name in view._allowed_fields:
|
|
392
|
+
setattr(view, name, value)
|
|
393
|
+
return
|
|
394
|
+
|
|
395
|
+
raise AttributeError(
|
|
396
|
+
f"Field '{name}' not accessible through any composed view"
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
# ============================================================================
|
|
401
|
+
# Factory Functions
|
|
402
|
+
# ============================================================================
|
|
403
|
+
|
|
404
|
+
def create_readonly_view(bb: Any, fields: tuple[str, ...]) -> ReadOnlyView:
|
|
405
|
+
"""
|
|
406
|
+
Create a read-only view.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
bb: The blackboard to wrap
|
|
410
|
+
fields: Tuple of field names for read-only access
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
A ReadOnlyView instance
|
|
414
|
+
|
|
415
|
+
Example:
|
|
416
|
+
>>> view = create_readonly_view(bb, ('config', 'max_tasks'))
|
|
417
|
+
"""
|
|
418
|
+
return ReadOnlyView(bb, set(fields))
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def create_constrained_view(
|
|
422
|
+
bb: Any,
|
|
423
|
+
allowed_fields: tuple[str, ...],
|
|
424
|
+
readonly_fields: tuple[str, ...] = ()
|
|
425
|
+
) -> ConstrainedView:
|
|
426
|
+
"""
|
|
427
|
+
Create a constrained view.
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
bb: The blackboard to wrap
|
|
431
|
+
allowed_fields: Tuple of field names that can be accessed
|
|
432
|
+
readonly_fields: Tuple of field names that are read-only
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
A ConstrainedView instance
|
|
436
|
+
|
|
437
|
+
Example:
|
|
438
|
+
>>> view = create_constrained_view(
|
|
439
|
+
... bb,
|
|
440
|
+
... ('max_tasks', 'tasks_completed'),
|
|
441
|
+
... ('max_tasks',)
|
|
442
|
+
... )
|
|
443
|
+
"""
|
|
444
|
+
return ConstrainedView(bb, set(allowed_fields), set(readonly_fields))
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def create_view_from_protocol(
|
|
448
|
+
bb: Any,
|
|
449
|
+
protocol: Type[P],
|
|
450
|
+
readonly_fields: Set[str] | None = None
|
|
451
|
+
) -> ConstrainedView:
|
|
452
|
+
"""
|
|
453
|
+
Create a constrained view from a Protocol interface.
|
|
454
|
+
|
|
455
|
+
Extracts field information from the Protocol and creates a view
|
|
456
|
+
that enforces the interface constraints.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
bb: The blackboard to wrap
|
|
460
|
+
protocol: The Protocol class defining the interface
|
|
461
|
+
readonly_fields: Optional set of field names that should be read-only.
|
|
462
|
+
If not provided, will try to extract from protocol metadata.
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
A ConstrainedView instance
|
|
466
|
+
|
|
467
|
+
Example:
|
|
468
|
+
>>> class TaskInterface(Protocol):
|
|
469
|
+
... max_tasks: int
|
|
470
|
+
... tasks_completed: int
|
|
471
|
+
>>> view = create_view_from_protocol(bb, TaskInterface, {'max_tasks'})
|
|
472
|
+
"""
|
|
473
|
+
# Extract fields from protocol
|
|
474
|
+
fields = get_interface_fields(protocol)
|
|
475
|
+
allowed_fields = set(fields.keys())
|
|
476
|
+
|
|
477
|
+
# If readonly_fields not provided, try to extract from protocol
|
|
478
|
+
if readonly_fields is None:
|
|
479
|
+
readonly_fields = getattr(protocol, '_readonly_fields', set())
|
|
480
|
+
|
|
481
|
+
return ConstrainedView(bb, allowed_fields, readonly_fields)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
# ============================================================================
|
|
485
|
+
# View Factory for Protocol-based Views
|
|
486
|
+
# ============================================================================
|
|
487
|
+
|
|
488
|
+
def View(bb: Any, protocol: Type[P]) -> Any:
|
|
489
|
+
"""
|
|
490
|
+
Factory function for creating typed views from protocols.
|
|
491
|
+
|
|
492
|
+
This provides a clean, type-safe API for creating constrained views
|
|
493
|
+
with full type hint support.
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
bb: The blackboard to wrap
|
|
497
|
+
protocol: The Protocol class defining the interface
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
A wrapper that implements the protocol
|
|
501
|
+
|
|
502
|
+
Example:
|
|
503
|
+
>>> class ConfigInterface(Protocol):
|
|
504
|
+
... max_tasks: int
|
|
505
|
+
... interval: float
|
|
506
|
+
>>> view: ConfigInterface = View(bb, ConfigInterface)
|
|
507
|
+
>>> # Type checker knows view has max_tasks and interval
|
|
508
|
+
"""
|
|
509
|
+
return create_view_from_protocol(bb, protocol)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
# ============================================================================
|
|
513
|
+
# Public API Exports
|
|
514
|
+
# ============================================================================
|
|
515
|
+
|
|
516
|
+
__all__ = [
|
|
517
|
+
# Exceptions
|
|
518
|
+
"AccessControlError",
|
|
519
|
+
|
|
520
|
+
# Wrapper Classes
|
|
521
|
+
"BaseWrapper",
|
|
522
|
+
"ReadOnlyView",
|
|
523
|
+
"WriteOnlyView",
|
|
524
|
+
"ConstrainedView",
|
|
525
|
+
"CompositeView",
|
|
526
|
+
|
|
527
|
+
# Factory Functions
|
|
528
|
+
"create_readonly_view",
|
|
529
|
+
"create_constrained_view",
|
|
530
|
+
"create_view_from_protocol",
|
|
531
|
+
"View",
|
|
532
|
+
]
|
|
File without changes
|