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
mycorrhizal/__init__.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Common utilities and interfaces for Mycorrhizal.
|
|
3
|
+
|
|
4
|
+
This module provides shared components used across all three DSL systems:
|
|
5
|
+
- Hypha (Petri Nets)
|
|
6
|
+
- Rhizomorph (Behavior Trees)
|
|
7
|
+
- Enoki (State Machines)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# Import interfaces for easy access
|
|
11
|
+
from mycorrhizal.common.interfaces import (
|
|
12
|
+
Readable,
|
|
13
|
+
Writable,
|
|
14
|
+
FieldAccessor,
|
|
15
|
+
BlackboardProtocol,
|
|
16
|
+
FieldMetadata,
|
|
17
|
+
InterfaceMetadata,
|
|
18
|
+
validate_implements,
|
|
19
|
+
get_interface_fields,
|
|
20
|
+
create_interface_from_model,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Import wrappers for easy access
|
|
24
|
+
from mycorrhizal.common.wrappers import (
|
|
25
|
+
AccessControlError,
|
|
26
|
+
ReadOnlyView,
|
|
27
|
+
WriteOnlyView,
|
|
28
|
+
ConstrainedView,
|
|
29
|
+
CompositeView,
|
|
30
|
+
create_readonly_view,
|
|
31
|
+
create_constrained_view,
|
|
32
|
+
create_view_from_protocol,
|
|
33
|
+
View,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Import interface builder for easy access
|
|
37
|
+
from mycorrhizal.common.interface_builder import (
|
|
38
|
+
blackboard_interface,
|
|
39
|
+
FieldSpec,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
# Interfaces
|
|
44
|
+
"Readable",
|
|
45
|
+
"Writable",
|
|
46
|
+
"FieldAccessor",
|
|
47
|
+
"BlackboardProtocol",
|
|
48
|
+
"FieldMetadata",
|
|
49
|
+
"InterfaceMetadata",
|
|
50
|
+
"validate_implements",
|
|
51
|
+
"get_interface_fields",
|
|
52
|
+
"create_interface_from_model",
|
|
53
|
+
|
|
54
|
+
# Wrappers
|
|
55
|
+
"AccessControlError",
|
|
56
|
+
"ReadOnlyView",
|
|
57
|
+
"WriteOnlyView",
|
|
58
|
+
"ConstrainedView",
|
|
59
|
+
"CompositeView",
|
|
60
|
+
"create_readonly_view",
|
|
61
|
+
"create_constrained_view",
|
|
62
|
+
"create_view_from_protocol",
|
|
63
|
+
"View",
|
|
64
|
+
|
|
65
|
+
# Interface Builder
|
|
66
|
+
"blackboard_interface",
|
|
67
|
+
"FieldSpec",
|
|
68
|
+
]
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interface Builder DSL for Blackboard Interfaces
|
|
3
|
+
|
|
4
|
+
This module provides a declarative DSL for defining blackboard interfaces
|
|
5
|
+
using PEP 593 Annotated types and access control markers.
|
|
6
|
+
|
|
7
|
+
Key concepts:
|
|
8
|
+
- @blackboard_interface - Decorator for defining interfaces
|
|
9
|
+
- readonly/readwrite markers for field access control
|
|
10
|
+
- Annotated types for clean, Pythonic field definitions
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Protocol, TypeVar, Generic, Type, Any, Optional, get_type_hints, Annotated, get_args
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
import inspect
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ============================================================================
|
|
19
|
+
# Type Variables
|
|
20
|
+
# ============================================================================
|
|
21
|
+
|
|
22
|
+
T = TypeVar('T')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ============================================================================
|
|
26
|
+
# Access Control Markers
|
|
27
|
+
# ============================================================================
|
|
28
|
+
|
|
29
|
+
class _AccessMarker:
|
|
30
|
+
"""Base class for access control markers"""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class readonly(_AccessMarker):
|
|
35
|
+
"""
|
|
36
|
+
Marker for readonly fields in blackboard interfaces.
|
|
37
|
+
|
|
38
|
+
Fields marked as readonly can be read but not modified through the interface.
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
```python
|
|
42
|
+
@blackboard_interface
|
|
43
|
+
class SensorInterface:
|
|
44
|
+
temperature: Annotated[float, readonly] = 20.0
|
|
45
|
+
```
|
|
46
|
+
"""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class readwrite(_AccessMarker):
|
|
51
|
+
"""
|
|
52
|
+
Marker for readwrite fields in blackboard interfaces.
|
|
53
|
+
|
|
54
|
+
Fields marked as readwrite can be both read and modified through the interface.
|
|
55
|
+
|
|
56
|
+
Example:
|
|
57
|
+
```python
|
|
58
|
+
@blackboard_interface
|
|
59
|
+
class StateInterface:
|
|
60
|
+
counter: Annotated[int, readwrite] = 0
|
|
61
|
+
```
|
|
62
|
+
"""
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ============================================================================
|
|
67
|
+
# Field Metadata
|
|
68
|
+
# ============================================================================
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class FieldSpec:
|
|
72
|
+
"""Specification for a field in an interface"""
|
|
73
|
+
name: str
|
|
74
|
+
type: type
|
|
75
|
+
readonly: bool = False
|
|
76
|
+
computed: bool = False
|
|
77
|
+
computation: Optional[callable] = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ============================================================================
|
|
81
|
+
# Decorator: blackboard_interface
|
|
82
|
+
# ============================================================================
|
|
83
|
+
|
|
84
|
+
def blackboard_interface(cls: Type[T] | None = None, *, name: str | None = None) -> Any:
|
|
85
|
+
"""
|
|
86
|
+
Decorator for defining blackboard interfaces using Annotated types.
|
|
87
|
+
|
|
88
|
+
Processes class annotations looking for Annotated types with readonly or
|
|
89
|
+
readwrite markers, and generates a Protocol class with access control metadata.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
cls: The class being decorated
|
|
93
|
+
name: Optional name for the generated interface
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
A Protocol class with type hints and metadata
|
|
97
|
+
|
|
98
|
+
Example:
|
|
99
|
+
```python
|
|
100
|
+
from typing import Annotated
|
|
101
|
+
from mycorrhizal.common.interface_builder import blackboard_interface, readonly, readwrite
|
|
102
|
+
|
|
103
|
+
@blackboard_interface
|
|
104
|
+
class TaskInterface:
|
|
105
|
+
max_tasks: Annotated[int, readonly] = 10
|
|
106
|
+
tasks_completed: Annotated[int, readwrite] = 0
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Fields without Annotated markers are treated as readwrite by default.
|
|
110
|
+
"""
|
|
111
|
+
def process_class(source_class: Type) -> Type:
|
|
112
|
+
"""Process the source class and generate a Protocol-like class"""
|
|
113
|
+
# Get the interface name
|
|
114
|
+
interface_name = name or source_class.__name__
|
|
115
|
+
|
|
116
|
+
# Extract type hints
|
|
117
|
+
try:
|
|
118
|
+
hints = get_type_hints(source_class, include_extras=True)
|
|
119
|
+
except Exception:
|
|
120
|
+
hints = {}
|
|
121
|
+
|
|
122
|
+
# Determine field metadata from Annotated types
|
|
123
|
+
readonly_fields = set()
|
|
124
|
+
readwrite_fields = set()
|
|
125
|
+
|
|
126
|
+
for field_name, field_type in hints.items():
|
|
127
|
+
if field_name.startswith('_'):
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
# Check if this is an Annotated type with access marker
|
|
131
|
+
if str(field_type).startswith('typing.Annotated['):
|
|
132
|
+
# Extract metadata from Annotated[type, *markers]
|
|
133
|
+
args = get_args(field_type)
|
|
134
|
+
if len(args) >= 2:
|
|
135
|
+
actual_type = args[0]
|
|
136
|
+
# Check metadata for readonly or readwrite marker
|
|
137
|
+
metadata = args[1:]
|
|
138
|
+
found_marker = False
|
|
139
|
+
for marker in metadata:
|
|
140
|
+
if isinstance(marker, type) and issubclass(marker, _AccessMarker):
|
|
141
|
+
if marker is readonly:
|
|
142
|
+
readonly_fields.add(field_name)
|
|
143
|
+
found_marker = True
|
|
144
|
+
break
|
|
145
|
+
elif marker is readwrite:
|
|
146
|
+
readwrite_fields.add(field_name)
|
|
147
|
+
found_marker = True
|
|
148
|
+
break
|
|
149
|
+
|
|
150
|
+
if not found_marker:
|
|
151
|
+
# No access marker found, default to readwrite
|
|
152
|
+
readwrite_fields.add(field_name)
|
|
153
|
+
else:
|
|
154
|
+
# Annotated with only type, no metadata
|
|
155
|
+
readwrite_fields.add(field_name)
|
|
156
|
+
else:
|
|
157
|
+
# Not Annotated, default to readwrite
|
|
158
|
+
readwrite_fields.add(field_name)
|
|
159
|
+
|
|
160
|
+
# Create a new class with Protocol-like behavior
|
|
161
|
+
namespace = {
|
|
162
|
+
'__annotations__': {},
|
|
163
|
+
'_readonly_fields': frozenset(readonly_fields),
|
|
164
|
+
'_readwrite_fields': frozenset(readwrite_fields),
|
|
165
|
+
'__doc__': source_class.__doc__ or f"Interface: {interface_name}",
|
|
166
|
+
'__module__': source_class.__module__,
|
|
167
|
+
'__protocol_attrs__': frozenset(), # For runtime_checkable
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# Copy field annotations (use actual types from Annotated)
|
|
171
|
+
for field_name, field_type in hints.items():
|
|
172
|
+
if not field_name.startswith('_'):
|
|
173
|
+
# Extract actual type from Annotated if present
|
|
174
|
+
if hasattr(field_type, '__origin__') and field_type.__origin__ is Annotated:
|
|
175
|
+
actual_type = field_type.__args__[0]
|
|
176
|
+
else:
|
|
177
|
+
actual_type = field_type
|
|
178
|
+
namespace['__annotations__'][field_name] = actual_type
|
|
179
|
+
|
|
180
|
+
# Create the interface class
|
|
181
|
+
interface = type(interface_name, (), namespace)
|
|
182
|
+
|
|
183
|
+
return interface
|
|
184
|
+
|
|
185
|
+
# Support @blackboard_interface and @blackboard_interface()
|
|
186
|
+
if cls is None:
|
|
187
|
+
# Called with parentheses
|
|
188
|
+
return lambda c: process_class(c)
|
|
189
|
+
else:
|
|
190
|
+
# Called without parentheses
|
|
191
|
+
return process_class(cls)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# ============================================================================
|
|
195
|
+
# Public API Exports
|
|
196
|
+
# ============================================================================
|
|
197
|
+
|
|
198
|
+
__all__ = [
|
|
199
|
+
"blackboard_interface",
|
|
200
|
+
"readonly",
|
|
201
|
+
"readwrite",
|
|
202
|
+
"FieldSpec",
|
|
203
|
+
]
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Blackboard Interface System
|
|
3
|
+
|
|
4
|
+
This module provides Protocol-based interfaces for type-safe, constrained access
|
|
5
|
+
to blackboard state across Mycorrhizal's three DSL systems (Hypha, Rhizomorph, Enoki).
|
|
6
|
+
|
|
7
|
+
Key concepts:
|
|
8
|
+
- Protocols: Structural subtyping for interface definitions
|
|
9
|
+
- Wrappers: Runtime enforcement of access constraints
|
|
10
|
+
- Composition: Combine multiple interfaces into larger ones
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Protocol, TypeVar, Generic, Any, runtime_checkable, runtime_checkable
|
|
14
|
+
from typing import Optional, get_type_hints, Union
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from functools import wraps
|
|
17
|
+
import inspect
|
|
18
|
+
import types
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ============================================================================
|
|
22
|
+
# Type Variables
|
|
23
|
+
# ============================================================================
|
|
24
|
+
|
|
25
|
+
T = TypeVar('T')
|
|
26
|
+
P = Protocol
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ============================================================================
|
|
30
|
+
# Core Protocol Definitions
|
|
31
|
+
# ============================================================================
|
|
32
|
+
|
|
33
|
+
@runtime_checkable
|
|
34
|
+
class Readable(Protocol[T]):
|
|
35
|
+
"""
|
|
36
|
+
Protocol for read-only access to a typed value.
|
|
37
|
+
|
|
38
|
+
This protocol provides structural subtyping - any class with a matching
|
|
39
|
+
`get_value()` method will satisfy this protocol.
|
|
40
|
+
|
|
41
|
+
Type Parameters:
|
|
42
|
+
T: The type of value being read
|
|
43
|
+
|
|
44
|
+
Example:
|
|
45
|
+
>>> class Config:
|
|
46
|
+
... def get_value(self) -> int:
|
|
47
|
+
... return 42
|
|
48
|
+
>>> config: Readable[int] = Config()
|
|
49
|
+
>>> isinstance(config, Readable)
|
|
50
|
+
True
|
|
51
|
+
"""
|
|
52
|
+
def get_value(self) -> T:
|
|
53
|
+
"""Get the current value"""
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@runtime_checkable
|
|
58
|
+
class Writable(Protocol[T]):
|
|
59
|
+
"""
|
|
60
|
+
Protocol for write access to a typed value.
|
|
61
|
+
|
|
62
|
+
This protocol provides structural subtyping - any class with a matching
|
|
63
|
+
`set_value()` method will satisfy this protocol.
|
|
64
|
+
|
|
65
|
+
Type Parameters:
|
|
66
|
+
T: The type of value being written
|
|
67
|
+
|
|
68
|
+
Example:
|
|
69
|
+
>>> class Storage:
|
|
70
|
+
... def set_value(self, value: int) -> None:
|
|
71
|
+
... self._value = value
|
|
72
|
+
>>> storage: Writable[int] = Storage()
|
|
73
|
+
>>> isinstance(storage, Writable)
|
|
74
|
+
True
|
|
75
|
+
"""
|
|
76
|
+
def set_value(self, value: T) -> None:
|
|
77
|
+
"""Set a new value"""
|
|
78
|
+
...
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@runtime_checkable
|
|
82
|
+
class FieldAccessor(Readable[T], Writable[T], Protocol[T]):
|
|
83
|
+
"""
|
|
84
|
+
Protocol for read/write access to a typed value.
|
|
85
|
+
|
|
86
|
+
Combines both Readable and Writable protocols for full access.
|
|
87
|
+
|
|
88
|
+
Type Parameters:
|
|
89
|
+
T: The type of value being accessed
|
|
90
|
+
|
|
91
|
+
Example:
|
|
92
|
+
>>> class Counter:
|
|
93
|
+
... def get_value(self) -> int:
|
|
94
|
+
... return self._count
|
|
95
|
+
... def set_value(self, value: int) -> None:
|
|
96
|
+
... self._count = value
|
|
97
|
+
>>> counter: FieldAccessor[int] = Counter()
|
|
98
|
+
>>> isinstance(counter, FieldAccessor)
|
|
99
|
+
True
|
|
100
|
+
"""
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@runtime_checkable
|
|
105
|
+
class BlackboardProtocol(Protocol):
|
|
106
|
+
"""
|
|
107
|
+
Base blackboard protocol that all blackboards should implement.
|
|
108
|
+
|
|
109
|
+
This protocol provides structural subtyping for blackboard-like objects.
|
|
110
|
+
Any class with matching methods will satisfy this protocol, no inheritance
|
|
111
|
+
required.
|
|
112
|
+
|
|
113
|
+
Required Methods:
|
|
114
|
+
validate(): Check if blackboard state is valid
|
|
115
|
+
to_dict(): Convert blackboard to dictionary representation
|
|
116
|
+
|
|
117
|
+
Example:
|
|
118
|
+
>>> class MyBlackboard:
|
|
119
|
+
... def validate(self) -> bool:
|
|
120
|
+
... return True
|
|
121
|
+
... def to_dict(self) -> dict[str, Any]:
|
|
122
|
+
... return {"data": 42}
|
|
123
|
+
>>> bb: BlackboardProtocol = MyBlackboard()
|
|
124
|
+
>>> isinstance(bb, BlackboardProtocol)
|
|
125
|
+
True
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def validate(self) -> bool:
|
|
129
|
+
"""
|
|
130
|
+
Validate the blackboard state.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
True if blackboard state is valid, False otherwise
|
|
134
|
+
"""
|
|
135
|
+
...
|
|
136
|
+
|
|
137
|
+
def to_dict(self) -> dict[str, Any]:
|
|
138
|
+
"""
|
|
139
|
+
Convert blackboard to dictionary representation.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Dictionary representation of blackboard state
|
|
143
|
+
"""
|
|
144
|
+
...
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ============================================================================
|
|
148
|
+
# Interface Metadata
|
|
149
|
+
# ============================================================================
|
|
150
|
+
|
|
151
|
+
@dataclass
|
|
152
|
+
class FieldMetadata:
|
|
153
|
+
"""
|
|
154
|
+
Metadata for blackboard interface fields.
|
|
155
|
+
|
|
156
|
+
Attributes:
|
|
157
|
+
name: Field name
|
|
158
|
+
type: Field type annotation
|
|
159
|
+
readonly: Whether field is read-only
|
|
160
|
+
required: Whether field is required (not Optional)
|
|
161
|
+
"""
|
|
162
|
+
name: str
|
|
163
|
+
type: type
|
|
164
|
+
readonly: bool = False
|
|
165
|
+
required: bool = True
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass
|
|
169
|
+
class InterfaceMetadata:
|
|
170
|
+
"""
|
|
171
|
+
Metadata for a blackboard interface.
|
|
172
|
+
|
|
173
|
+
Attributes:
|
|
174
|
+
name: Interface name
|
|
175
|
+
fields: Dictionary of field metadata by field name
|
|
176
|
+
description: Optional description of the interface
|
|
177
|
+
bases: Base interfaces this interface extends
|
|
178
|
+
"""
|
|
179
|
+
name: str
|
|
180
|
+
fields: dict[str, FieldMetadata] = field(default_factory=dict)
|
|
181
|
+
description: Optional[str] = None
|
|
182
|
+
bases: tuple[type, ...] = field(default_factory=tuple)
|
|
183
|
+
|
|
184
|
+
def get_readonly_fields(self) -> set[str]:
|
|
185
|
+
"""Get set of read-only field names"""
|
|
186
|
+
return {name for name, meta in self.fields.items() if meta.readonly}
|
|
187
|
+
|
|
188
|
+
def get_readwrite_fields(self) -> set[str]:
|
|
189
|
+
"""Get set of read-write field names"""
|
|
190
|
+
return {name for name, meta in self.fields.items() if not meta.readonly}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _extract_interface_metadata(
|
|
194
|
+
interface_class: type,
|
|
195
|
+
readonly_fields: set[str] | None = None,
|
|
196
|
+
readwrite_fields: set[str] | None = None
|
|
197
|
+
) -> InterfaceMetadata:
|
|
198
|
+
"""
|
|
199
|
+
Extract metadata from an interface class.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
interface_class: The interface class to analyze
|
|
203
|
+
readonly_fields: Set of field names that are read-only
|
|
204
|
+
readwrite_fields: Set of field names that are read-write
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
InterfaceMetadata with extracted information
|
|
208
|
+
"""
|
|
209
|
+
readonly_fields = readonly_fields or set()
|
|
210
|
+
readwrite_fields = readwrite_fields or set()
|
|
211
|
+
|
|
212
|
+
# Get type hints from the class
|
|
213
|
+
try:
|
|
214
|
+
hints = get_type_hints(interface_class)
|
|
215
|
+
except Exception:
|
|
216
|
+
hints = {}
|
|
217
|
+
|
|
218
|
+
# Build field metadata
|
|
219
|
+
fields = {}
|
|
220
|
+
for field_name, field_type in hints.items():
|
|
221
|
+
# Skip private attributes and methods
|
|
222
|
+
if field_name.startswith('_'):
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
# Check if field is readonly or readwrite
|
|
226
|
+
is_readonly = field_name in readonly_fields
|
|
227
|
+
|
|
228
|
+
# Check if field is required (not Optional)
|
|
229
|
+
# Handle Union types (Optional[T] is Union[T, None])
|
|
230
|
+
is_required = True
|
|
231
|
+
if hasattr(field_type, '__origin__'):
|
|
232
|
+
# Check for Union (which includes Optional)
|
|
233
|
+
if field_type.__origin__ is Union:
|
|
234
|
+
# Union[X, None] means Optional[X]
|
|
235
|
+
args = getattr(field_type, '__args__', ())
|
|
236
|
+
is_required = type(None) not in args
|
|
237
|
+
elif str(field_type).startswith('typing.Union[') or 'Optional[' in str(field_type):
|
|
238
|
+
# String-based fallback for edge cases
|
|
239
|
+
is_required = 'None' not in str(field_type)
|
|
240
|
+
|
|
241
|
+
fields[field_name] = FieldMetadata(
|
|
242
|
+
name=field_name,
|
|
243
|
+
type=field_type,
|
|
244
|
+
readonly=is_readonly,
|
|
245
|
+
required=is_required
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
return InterfaceMetadata(
|
|
249
|
+
name=interface_class.__name__,
|
|
250
|
+
fields=fields,
|
|
251
|
+
description=interface_class.__doc__,
|
|
252
|
+
bases=interface_class.__bases__
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# ============================================================================
|
|
257
|
+
# Protocol Helper Functions
|
|
258
|
+
# ============================================================================
|
|
259
|
+
|
|
260
|
+
def validate_implements(cls: type, protocol: type) -> bool:
|
|
261
|
+
"""
|
|
262
|
+
Validate that a class implements a protocol.
|
|
263
|
+
|
|
264
|
+
Uses structural subtyping to check if the class has all required
|
|
265
|
+
methods and attributes defined by the protocol.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
cls: The class to check
|
|
269
|
+
protocol: The protocol to check against
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
True if cls implements protocol, False otherwise
|
|
273
|
+
|
|
274
|
+
Example:
|
|
275
|
+
>>> class MyBlackboard:
|
|
276
|
+
... def validate(self) -> bool:
|
|
277
|
+
... return True
|
|
278
|
+
... def to_dict(self) -> dict:
|
|
279
|
+
... return {}
|
|
280
|
+
>>> validate_implements(MyBlackboard, BlackboardProtocol)
|
|
281
|
+
True
|
|
282
|
+
>>> validate_implements(str, BlackboardProtocol)
|
|
283
|
+
False
|
|
284
|
+
"""
|
|
285
|
+
if not isinstance(protocol, type) or not hasattr(protocol, '__protocol_attrs__'):
|
|
286
|
+
# Not a protocol, try anyway with isinstance
|
|
287
|
+
try:
|
|
288
|
+
return issubclass(cls, protocol) if isinstance(cls, type) else isinstance(cls, protocol)
|
|
289
|
+
except TypeError:
|
|
290
|
+
return False
|
|
291
|
+
|
|
292
|
+
# Check if protocol is runtime_checkable
|
|
293
|
+
if not hasattr(protocol, '__is_protocol__'):
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
# Use isinstance if we have an instance, otherwise check structure
|
|
297
|
+
try:
|
|
298
|
+
# For protocols, we need to check if the class has all protocol attributes
|
|
299
|
+
for attr in getattr(protocol, '__protocol_attrs__', []):
|
|
300
|
+
if not hasattr(cls, attr):
|
|
301
|
+
return False
|
|
302
|
+
return True
|
|
303
|
+
except Exception:
|
|
304
|
+
return False
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def get_interface_fields(interface: type) -> dict[str, type]:
|
|
308
|
+
"""
|
|
309
|
+
Extract field type annotations from an interface.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
interface: The interface class to extract fields from
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Dictionary mapping field names to their types
|
|
316
|
+
|
|
317
|
+
Example:
|
|
318
|
+
>>> class MyInterface(Protocol):
|
|
319
|
+
... counter: int
|
|
320
|
+
... name: str
|
|
321
|
+
>>> get_interface_fields(MyInterface)
|
|
322
|
+
{'counter': <class 'int'>, 'name': <class 'str'>}
|
|
323
|
+
"""
|
|
324
|
+
try:
|
|
325
|
+
hints = get_type_hints(interface)
|
|
326
|
+
# Filter out private attributes
|
|
327
|
+
return {
|
|
328
|
+
name: typ for name, typ in hints.items()
|
|
329
|
+
if not name.startswith('_')
|
|
330
|
+
}
|
|
331
|
+
except Exception:
|
|
332
|
+
return {}
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def create_interface_from_model(model_class: type, readonly_fields: set[str] | None = None) -> type[Protocol]:
|
|
336
|
+
"""
|
|
337
|
+
Create a Protocol interface from a Pydantic model or dataclass.
|
|
338
|
+
|
|
339
|
+
This is a utility function for automatically generating interfaces from
|
|
340
|
+
existing model classes, supporting gradual migration.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
model_class: The model class to create interface from
|
|
344
|
+
readonly_fields: Optional set of field names that should be read-only
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
A Protocol class with the same fields as the model
|
|
348
|
+
|
|
349
|
+
Example:
|
|
350
|
+
>>> from pydantic import BaseModel
|
|
351
|
+
>>> class TaskModel(BaseModel):
|
|
352
|
+
... max_tasks: int
|
|
353
|
+
... tasks_completed: int
|
|
354
|
+
>>> TaskInterface = create_interface_from_model(
|
|
355
|
+
... TaskModel,
|
|
356
|
+
... readonly_fields={'max_tasks'}
|
|
357
|
+
... )
|
|
358
|
+
>>> get_interface_fields(TaskInterface)
|
|
359
|
+
{'max_tasks': <class 'int'>, 'tasks_completed': <class 'int'>}
|
|
360
|
+
"""
|
|
361
|
+
readonly_fields = readonly_fields or set()
|
|
362
|
+
|
|
363
|
+
# Get field annotations from the model
|
|
364
|
+
try:
|
|
365
|
+
hints = get_type_hints(model_class)
|
|
366
|
+
except Exception:
|
|
367
|
+
hints = {}
|
|
368
|
+
|
|
369
|
+
# Create protocol namespace
|
|
370
|
+
namespace = {
|
|
371
|
+
'__annotations__': {},
|
|
372
|
+
'__doc__': f"Auto-generated interface from {model_class.__name__}",
|
|
373
|
+
'_readonly_fields': readonly_fields,
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
# Add field annotations
|
|
377
|
+
for field_name, field_type in hints.items():
|
|
378
|
+
if not field_name.startswith('_'):
|
|
379
|
+
namespace['__annotations__'][field_name] = field_type
|
|
380
|
+
|
|
381
|
+
# Create the Protocol class
|
|
382
|
+
interface_name = f"{model_class.__name__}Interface"
|
|
383
|
+
interface_class = type(interface_name, (Protocol,), namespace)
|
|
384
|
+
|
|
385
|
+
return interface_class
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
# ============================================================================
|
|
389
|
+
# Public API Exports
|
|
390
|
+
# ============================================================================
|
|
391
|
+
|
|
392
|
+
__all__ = [
|
|
393
|
+
# Type Variables
|
|
394
|
+
"T",
|
|
395
|
+
"P",
|
|
396
|
+
|
|
397
|
+
# Core Protocols
|
|
398
|
+
"Readable",
|
|
399
|
+
"Writable",
|
|
400
|
+
"FieldAccessor",
|
|
401
|
+
"BlackboardProtocol",
|
|
402
|
+
|
|
403
|
+
# Metadata
|
|
404
|
+
"FieldMetadata",
|
|
405
|
+
"InterfaceMetadata",
|
|
406
|
+
|
|
407
|
+
# Helper Functions
|
|
408
|
+
"validate_implements",
|
|
409
|
+
"get_interface_fields",
|
|
410
|
+
"create_interface_from_model",
|
|
411
|
+
"_extract_interface_metadata",
|
|
412
|
+
]
|