kailash 0.8.4__py3-none-any.whl → 0.8.5__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.
- kailash/__init__.py +1 -7
- kailash/cli/__init__.py +11 -1
- kailash/cli/validation_audit.py +570 -0
- kailash/core/actors/supervisor.py +1 -1
- kailash/core/resilience/circuit_breaker.py +71 -1
- kailash/core/resilience/health_monitor.py +172 -0
- kailash/edge/compliance.py +33 -0
- kailash/edge/consistency.py +609 -0
- kailash/edge/coordination/__init__.py +30 -0
- kailash/edge/coordination/global_ordering.py +355 -0
- kailash/edge/coordination/leader_election.py +217 -0
- kailash/edge/coordination/partition_detector.py +296 -0
- kailash/edge/coordination/raft.py +485 -0
- kailash/edge/discovery.py +63 -1
- kailash/edge/migration/__init__.py +19 -0
- kailash/edge/migration/edge_migrator.py +832 -0
- kailash/edge/monitoring/__init__.py +21 -0
- kailash/edge/monitoring/edge_monitor.py +736 -0
- kailash/edge/prediction/__init__.py +10 -0
- kailash/edge/prediction/predictive_warmer.py +591 -0
- kailash/edge/resource/__init__.py +102 -0
- kailash/edge/resource/cloud_integration.py +796 -0
- kailash/edge/resource/cost_optimizer.py +949 -0
- kailash/edge/resource/docker_integration.py +919 -0
- kailash/edge/resource/kubernetes_integration.py +893 -0
- kailash/edge/resource/platform_integration.py +913 -0
- kailash/edge/resource/predictive_scaler.py +959 -0
- kailash/edge/resource/resource_analyzer.py +824 -0
- kailash/edge/resource/resource_pools.py +610 -0
- kailash/integrations/dataflow_edge.py +261 -0
- kailash/mcp_server/registry_integration.py +1 -1
- kailash/monitoring/__init__.py +18 -0
- kailash/monitoring/alerts.py +646 -0
- kailash/monitoring/metrics.py +677 -0
- kailash/nodes/__init__.py +2 -0
- kailash/nodes/ai/semantic_memory.py +2 -2
- kailash/nodes/base.py +545 -0
- kailash/nodes/edge/__init__.py +36 -0
- kailash/nodes/edge/base.py +240 -0
- kailash/nodes/edge/cloud_node.py +710 -0
- kailash/nodes/edge/coordination.py +239 -0
- kailash/nodes/edge/docker_node.py +825 -0
- kailash/nodes/edge/edge_data.py +582 -0
- kailash/nodes/edge/edge_migration_node.py +392 -0
- kailash/nodes/edge/edge_monitoring_node.py +421 -0
- kailash/nodes/edge/edge_state.py +673 -0
- kailash/nodes/edge/edge_warming_node.py +393 -0
- kailash/nodes/edge/kubernetes_node.py +652 -0
- kailash/nodes/edge/platform_node.py +766 -0
- kailash/nodes/edge/resource_analyzer_node.py +378 -0
- kailash/nodes/edge/resource_optimizer_node.py +501 -0
- kailash/nodes/edge/resource_scaler_node.py +397 -0
- kailash/nodes/ports.py +676 -0
- kailash/runtime/local.py +344 -1
- kailash/runtime/validation/__init__.py +20 -0
- kailash/runtime/validation/connection_context.py +119 -0
- kailash/runtime/validation/enhanced_error_formatter.py +202 -0
- kailash/runtime/validation/error_categorizer.py +164 -0
- kailash/runtime/validation/metrics.py +380 -0
- kailash/runtime/validation/performance.py +615 -0
- kailash/runtime/validation/suggestion_engine.py +212 -0
- kailash/testing/fixtures.py +2 -2
- kailash/workflow/builder.py +230 -4
- kailash/workflow/contracts.py +418 -0
- kailash/workflow/edge_infrastructure.py +369 -0
- kailash/workflow/migration.py +3 -3
- kailash/workflow/type_inference.py +669 -0
- {kailash-0.8.4.dist-info → kailash-0.8.5.dist-info}/METADATA +43 -27
- {kailash-0.8.4.dist-info → kailash-0.8.5.dist-info}/RECORD +73 -27
- kailash/nexus/__init__.py +0 -21
- kailash/nexus/cli/__init__.py +0 -5
- kailash/nexus/cli/__main__.py +0 -6
- kailash/nexus/cli/main.py +0 -176
- kailash/nexus/factory.py +0 -413
- kailash/nexus/gateway.py +0 -545
- {kailash-0.8.4.dist-info → kailash-0.8.5.dist-info}/WHEEL +0 -0
- {kailash-0.8.4.dist-info → kailash-0.8.5.dist-info}/entry_points.txt +0 -0
- {kailash-0.8.4.dist-info → kailash-0.8.5.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.8.4.dist-info → kailash-0.8.5.dist-info}/top_level.txt +0 -0
kailash/nodes/ports.py
ADDED
@@ -0,0 +1,676 @@
|
|
1
|
+
"""
|
2
|
+
Type-safe input/output port system for Kailash nodes.
|
3
|
+
|
4
|
+
This module provides a type-safe port system that enables:
|
5
|
+
- Compile-time type checking with IDE support
|
6
|
+
- Runtime type validation
|
7
|
+
- Clear port declarations in node definitions
|
8
|
+
- Automatic type inference for connections
|
9
|
+
- Better developer experience with autocomplete
|
10
|
+
|
11
|
+
Design Goals:
|
12
|
+
1. Type safety: Catch type mismatches at design time
|
13
|
+
2. IDE support: Full autocomplete and type hints
|
14
|
+
3. Runtime validation: Enforce types during execution
|
15
|
+
4. Backward compatibility: Works with existing nodes
|
16
|
+
5. Performance: Minimal runtime overhead
|
17
|
+
|
18
|
+
Example Usage:
|
19
|
+
class MyNode(TypedNode):
|
20
|
+
# Input ports
|
21
|
+
text_input = InputPort[str]("text_input", description="Text to process")
|
22
|
+
count = InputPort[int]("count", default=1, description="Number of iterations")
|
23
|
+
|
24
|
+
# Output ports
|
25
|
+
result = OutputPort[str]("result", description="Processed text")
|
26
|
+
metadata = OutputPort[Dict[str, Any]]("metadata", description="Processing metadata")
|
27
|
+
|
28
|
+
def run(self, **kwargs) -> Dict[str, Any]:
|
29
|
+
text = self.text_input.get()
|
30
|
+
count = self.count.get()
|
31
|
+
|
32
|
+
# Process...
|
33
|
+
processed = text * count
|
34
|
+
|
35
|
+
return {
|
36
|
+
self.result.name: processed,
|
37
|
+
self.metadata.name: {"length": len(processed)}
|
38
|
+
}
|
39
|
+
"""
|
40
|
+
|
41
|
+
import logging
|
42
|
+
from abc import ABC, abstractmethod
|
43
|
+
from dataclasses import dataclass, field
|
44
|
+
from typing import (
|
45
|
+
Any,
|
46
|
+
Dict,
|
47
|
+
Generic,
|
48
|
+
List,
|
49
|
+
Optional,
|
50
|
+
Type,
|
51
|
+
TypeVar,
|
52
|
+
Union,
|
53
|
+
get_args,
|
54
|
+
get_origin,
|
55
|
+
get_type_hints,
|
56
|
+
)
|
57
|
+
|
58
|
+
logger = logging.getLogger(__name__)
|
59
|
+
|
60
|
+
# Type variable for generic port types
|
61
|
+
T = TypeVar("T")
|
62
|
+
|
63
|
+
|
64
|
+
@dataclass
|
65
|
+
class PortMetadata:
|
66
|
+
"""Metadata for input/output ports."""
|
67
|
+
|
68
|
+
name: str
|
69
|
+
description: str = ""
|
70
|
+
required: bool = True
|
71
|
+
default: Any = None
|
72
|
+
constraints: Dict[str, Any] = field(default_factory=dict)
|
73
|
+
examples: List[Any] = field(default_factory=list)
|
74
|
+
|
75
|
+
def to_dict(self) -> Dict[str, Any]:
|
76
|
+
"""Convert to dictionary for serialization."""
|
77
|
+
return {
|
78
|
+
"name": self.name,
|
79
|
+
"description": self.description,
|
80
|
+
"required": self.required,
|
81
|
+
"default": self.default,
|
82
|
+
"constraints": self.constraints,
|
83
|
+
"examples": self.examples,
|
84
|
+
}
|
85
|
+
|
86
|
+
|
87
|
+
class Port(Generic[T], ABC):
|
88
|
+
"""Base class for typed input/output ports."""
|
89
|
+
|
90
|
+
def __init__(
|
91
|
+
self,
|
92
|
+
name: str,
|
93
|
+
description: str = "",
|
94
|
+
required: bool = True,
|
95
|
+
default: Optional[T] = None,
|
96
|
+
constraints: Optional[Dict[str, Any]] = None,
|
97
|
+
examples: Optional[List[T]] = None,
|
98
|
+
):
|
99
|
+
"""Initialize a port.
|
100
|
+
|
101
|
+
Args:
|
102
|
+
name: Port name (should match parameter name)
|
103
|
+
description: Human-readable description
|
104
|
+
required: Whether this port is required
|
105
|
+
default: Default value if not connected
|
106
|
+
constraints: Additional validation constraints
|
107
|
+
examples: Example values for documentation
|
108
|
+
"""
|
109
|
+
self.name = name
|
110
|
+
self.metadata = PortMetadata(
|
111
|
+
name=name,
|
112
|
+
description=description,
|
113
|
+
required=required,
|
114
|
+
default=default,
|
115
|
+
constraints=constraints or {},
|
116
|
+
examples=examples or [],
|
117
|
+
)
|
118
|
+
self._type_hint: Optional[Type[T]] = None
|
119
|
+
self._value: Optional[T] = None
|
120
|
+
self._node_instance: Optional[Any] = None
|
121
|
+
|
122
|
+
# Schedule type hint extraction to happen after __orig_class__ is set
|
123
|
+
self._extract_type_hint()
|
124
|
+
|
125
|
+
def _extract_type_hint(self):
|
126
|
+
"""Extract type hint from __orig_class__ after instance creation."""
|
127
|
+
# This will be called again from a delayed context when __orig_class__ is available
|
128
|
+
if hasattr(self, "__orig_class__"):
|
129
|
+
args = get_args(self.__orig_class__)
|
130
|
+
if args:
|
131
|
+
self._type_hint = args[0]
|
132
|
+
return True
|
133
|
+
return False
|
134
|
+
|
135
|
+
def __set_name__(self, owner: Type, name: str) -> None:
|
136
|
+
"""Called when port is assigned to a class attribute."""
|
137
|
+
if self.name != name:
|
138
|
+
logger.warning(
|
139
|
+
f"Port name '{self.name}' doesn't match attribute name '{name}'. Using '{name}'."
|
140
|
+
)
|
141
|
+
self.name = name
|
142
|
+
self.metadata.name = name
|
143
|
+
|
144
|
+
# Extract type hint from class annotations
|
145
|
+
if hasattr(owner, "__annotations__") and name in owner.__annotations__:
|
146
|
+
annotation = owner.__annotations__[name]
|
147
|
+
# Extract the type argument from Port[T]
|
148
|
+
if hasattr(annotation, "__args__") and annotation.__args__:
|
149
|
+
self._type_hint = annotation.__args__[0]
|
150
|
+
elif hasattr(annotation, "__origin__"):
|
151
|
+
# Handle Generic types
|
152
|
+
origin = get_origin(annotation)
|
153
|
+
args = get_args(annotation)
|
154
|
+
if origin and args:
|
155
|
+
self._type_hint = args[0]
|
156
|
+
|
157
|
+
# If no type hint found, try to extract from the port instance itself
|
158
|
+
if self._type_hint is None and hasattr(self, "__orig_class__"):
|
159
|
+
args = get_args(self.__orig_class__)
|
160
|
+
if args:
|
161
|
+
self._type_hint = args[0]
|
162
|
+
|
163
|
+
def __get__(self, instance: Any, owner: Type = None) -> "Port[T]":
|
164
|
+
"""Descriptor protocol - return port instance bound to node."""
|
165
|
+
if instance is None:
|
166
|
+
return self
|
167
|
+
|
168
|
+
# Cache bound ports per instance to maintain state
|
169
|
+
cache_attr = f"_bound_port_{self.name}_{id(self)}"
|
170
|
+
if hasattr(instance, cache_attr):
|
171
|
+
return getattr(instance, cache_attr)
|
172
|
+
|
173
|
+
# Create a copy bound to this instance
|
174
|
+
if isinstance(self, OutputPort):
|
175
|
+
bound_port = self.__class__(
|
176
|
+
name=self.name,
|
177
|
+
description=self.metadata.description,
|
178
|
+
constraints=self.metadata.constraints,
|
179
|
+
examples=self.metadata.examples,
|
180
|
+
)
|
181
|
+
else:
|
182
|
+
bound_port = self.__class__(
|
183
|
+
name=self.name,
|
184
|
+
description=self.metadata.description,
|
185
|
+
required=self.metadata.required,
|
186
|
+
default=self.metadata.default,
|
187
|
+
constraints=self.metadata.constraints,
|
188
|
+
examples=self.metadata.examples,
|
189
|
+
)
|
190
|
+
bound_port._type_hint = self._type_hint
|
191
|
+
bound_port._node_instance = instance
|
192
|
+
|
193
|
+
# Cache the bound port
|
194
|
+
setattr(instance, cache_attr, bound_port)
|
195
|
+
return bound_port
|
196
|
+
|
197
|
+
def __set__(self, instance: Any, value: T) -> None:
|
198
|
+
"""Descriptor protocol - set port value."""
|
199
|
+
self._value = value
|
200
|
+
self._node_instance = instance
|
201
|
+
|
202
|
+
@property
|
203
|
+
def type_hint(self) -> Optional[Type[T]]:
|
204
|
+
"""Get the type hint for this port."""
|
205
|
+
return self._type_hint
|
206
|
+
|
207
|
+
def get_type_name(self) -> str:
|
208
|
+
"""Get human-readable type name."""
|
209
|
+
# Lazy type hint extraction
|
210
|
+
if self._type_hint is None:
|
211
|
+
self._extract_type_hint()
|
212
|
+
|
213
|
+
if self._type_hint:
|
214
|
+
if hasattr(self._type_hint, "__name__"):
|
215
|
+
return self._type_hint.__name__
|
216
|
+
else:
|
217
|
+
return str(self._type_hint)
|
218
|
+
return "Any"
|
219
|
+
|
220
|
+
def validate_type(self, value: Any) -> bool:
|
221
|
+
"""Validate that value matches port type."""
|
222
|
+
# Lazy type hint extraction - try again if not set yet
|
223
|
+
if self._type_hint is None:
|
224
|
+
self._extract_type_hint()
|
225
|
+
|
226
|
+
if self._type_hint is None:
|
227
|
+
return True # No type constraint
|
228
|
+
|
229
|
+
# Handle None values
|
230
|
+
if value is None:
|
231
|
+
return not self.metadata.required
|
232
|
+
|
233
|
+
# Basic type checking
|
234
|
+
try:
|
235
|
+
if isinstance(self._type_hint, type):
|
236
|
+
return isinstance(value, self._type_hint)
|
237
|
+
else:
|
238
|
+
# Handle complex types like Union, Optional, etc.
|
239
|
+
return self._check_complex_type(value, self._type_hint)
|
240
|
+
except Exception as e:
|
241
|
+
logger.warning(f"Type validation error for port '{self.name}': {e}")
|
242
|
+
return False
|
243
|
+
|
244
|
+
def _check_complex_type(self, value: Any, type_hint: Type) -> bool:
|
245
|
+
"""Check complex types like Union, Optional, List[str], etc."""
|
246
|
+
origin = get_origin(type_hint)
|
247
|
+
args = get_args(type_hint)
|
248
|
+
|
249
|
+
if origin is Union:
|
250
|
+
# Check if value matches any of the union types
|
251
|
+
return any(self._check_complex_type(value, arg) for arg in args)
|
252
|
+
elif origin is list and args:
|
253
|
+
# Check List[T] - all elements must be of type T
|
254
|
+
if not isinstance(value, list):
|
255
|
+
return False
|
256
|
+
return all(self._check_complex_type(item, args[0]) for item in value)
|
257
|
+
elif origin is dict and len(args) >= 2:
|
258
|
+
# Check Dict[K, V]
|
259
|
+
if not isinstance(value, dict):
|
260
|
+
return False
|
261
|
+
return all(
|
262
|
+
self._check_complex_type(k, args[0]) for k in value.keys()
|
263
|
+
) and all(self._check_complex_type(v, args[1]) for v in value.values())
|
264
|
+
else:
|
265
|
+
# Fallback to isinstance for basic types
|
266
|
+
try:
|
267
|
+
return isinstance(value, type_hint)
|
268
|
+
except TypeError:
|
269
|
+
# Some types can't be used with isinstance
|
270
|
+
return True
|
271
|
+
|
272
|
+
def validate_constraints(self, value: Any) -> tuple[bool, Optional[str]]:
|
273
|
+
"""Validate value against port constraints.
|
274
|
+
|
275
|
+
Returns:
|
276
|
+
Tuple of (is_valid, error_message)
|
277
|
+
"""
|
278
|
+
if not self.metadata.constraints:
|
279
|
+
return True, None
|
280
|
+
|
281
|
+
for constraint, constraint_value in self.metadata.constraints.items():
|
282
|
+
if constraint == "min_length" and hasattr(value, "__len__"):
|
283
|
+
if len(value) < constraint_value:
|
284
|
+
return (
|
285
|
+
False,
|
286
|
+
f"Value length {len(value)} is less than minimum {constraint_value}",
|
287
|
+
)
|
288
|
+
elif constraint == "max_length" and hasattr(value, "__len__"):
|
289
|
+
if len(value) > constraint_value:
|
290
|
+
return (
|
291
|
+
False,
|
292
|
+
f"Value length {len(value)} is greater than maximum {constraint_value}",
|
293
|
+
)
|
294
|
+
elif constraint == "min_value" and isinstance(value, (int, float)):
|
295
|
+
if value < constraint_value:
|
296
|
+
return (
|
297
|
+
False,
|
298
|
+
f"Value {value} is less than minimum {constraint_value}",
|
299
|
+
)
|
300
|
+
elif constraint == "max_value" and isinstance(value, (int, float)):
|
301
|
+
if value > constraint_value:
|
302
|
+
return (
|
303
|
+
False,
|
304
|
+
f"Value {value} is greater than maximum {constraint_value}",
|
305
|
+
)
|
306
|
+
elif constraint == "pattern" and isinstance(value, str):
|
307
|
+
import re
|
308
|
+
|
309
|
+
if not re.match(constraint_value, value):
|
310
|
+
return (
|
311
|
+
False,
|
312
|
+
f"Value '{value}' does not match pattern '{constraint_value}'",
|
313
|
+
)
|
314
|
+
|
315
|
+
return True, None
|
316
|
+
|
317
|
+
@abstractmethod
|
318
|
+
def get(self) -> T:
|
319
|
+
"""Get the value from this port."""
|
320
|
+
pass
|
321
|
+
|
322
|
+
def to_dict(self) -> Dict[str, Any]:
|
323
|
+
"""Convert port to dictionary for serialization."""
|
324
|
+
return {
|
325
|
+
"name": self.name,
|
326
|
+
"type": self.get_type_name(),
|
327
|
+
"metadata": self.metadata.to_dict(),
|
328
|
+
"port_type": self.__class__.__name__,
|
329
|
+
}
|
330
|
+
|
331
|
+
|
332
|
+
class InputPort(Port[T]):
|
333
|
+
"""Input port for receiving data into a node."""
|
334
|
+
|
335
|
+
def __init__(
|
336
|
+
self,
|
337
|
+
name: str,
|
338
|
+
description: str = "",
|
339
|
+
required: bool = True,
|
340
|
+
default: Optional[T] = None,
|
341
|
+
constraints: Optional[Dict[str, Any]] = None,
|
342
|
+
examples: Optional[List[T]] = None,
|
343
|
+
):
|
344
|
+
"""Initialize an input port.
|
345
|
+
|
346
|
+
Args:
|
347
|
+
name: Port name
|
348
|
+
description: Description of what this port accepts
|
349
|
+
required: Whether this port must be connected or have a value
|
350
|
+
default: Default value if not connected
|
351
|
+
constraints: Validation constraints (min_length, max_length, min_value, max_value, pattern)
|
352
|
+
examples: Example values for documentation
|
353
|
+
"""
|
354
|
+
super().__init__(name, description, required, default, constraints, examples)
|
355
|
+
|
356
|
+
def get(self) -> T:
|
357
|
+
"""Get the value from this input port.
|
358
|
+
|
359
|
+
Returns:
|
360
|
+
The input value, or default if not set
|
361
|
+
|
362
|
+
Raises:
|
363
|
+
ValueError: If port is required but no value is available
|
364
|
+
"""
|
365
|
+
if self._value is not None:
|
366
|
+
return self._value
|
367
|
+
elif self.metadata.default is not None:
|
368
|
+
return self.metadata.default
|
369
|
+
elif not self.metadata.required:
|
370
|
+
return None
|
371
|
+
else:
|
372
|
+
raise ValueError(f"Required input port '{self.name}' has no value")
|
373
|
+
|
374
|
+
def set(self, value: T) -> None:
|
375
|
+
"""Set the value for this input port.
|
376
|
+
|
377
|
+
Args:
|
378
|
+
value: Value to set
|
379
|
+
|
380
|
+
Raises:
|
381
|
+
TypeError: If value doesn't match port type
|
382
|
+
ValueError: If value doesn't meet constraints
|
383
|
+
"""
|
384
|
+
# Type validation
|
385
|
+
if not self.validate_type(value):
|
386
|
+
raise TypeError(
|
387
|
+
f"Input port '{self.name}' expects {self.get_type_name()}, got {type(value).__name__}"
|
388
|
+
)
|
389
|
+
|
390
|
+
# Constraint validation
|
391
|
+
is_valid, error = self.validate_constraints(value)
|
392
|
+
if not is_valid:
|
393
|
+
raise ValueError(f"Input port '{self.name}' constraint violation: {error}")
|
394
|
+
|
395
|
+
self._value = value
|
396
|
+
|
397
|
+
def is_connected(self) -> bool:
|
398
|
+
"""Check if this input port has a value (connected or default)."""
|
399
|
+
return self._value is not None or self.metadata.default is not None
|
400
|
+
|
401
|
+
|
402
|
+
class OutputPort(Port[T]):
|
403
|
+
"""Output port for sending data from a node."""
|
404
|
+
|
405
|
+
def __init__(
|
406
|
+
self,
|
407
|
+
name: str,
|
408
|
+
description: str = "",
|
409
|
+
constraints: Optional[Dict[str, Any]] = None,
|
410
|
+
examples: Optional[List[T]] = None,
|
411
|
+
):
|
412
|
+
"""Initialize an output port.
|
413
|
+
|
414
|
+
Args:
|
415
|
+
name: Port name
|
416
|
+
description: Description of what this port produces
|
417
|
+
constraints: Validation constraints for output values
|
418
|
+
examples: Example output values for documentation
|
419
|
+
"""
|
420
|
+
# Output ports are never required (they're always produced by the node)
|
421
|
+
super().__init__(
|
422
|
+
name,
|
423
|
+
description,
|
424
|
+
required=False,
|
425
|
+
default=None,
|
426
|
+
constraints=constraints,
|
427
|
+
examples=examples,
|
428
|
+
)
|
429
|
+
|
430
|
+
def get(self) -> T:
|
431
|
+
"""Get the value from this output port.
|
432
|
+
|
433
|
+
Returns:
|
434
|
+
The output value
|
435
|
+
|
436
|
+
Raises:
|
437
|
+
ValueError: If no value has been set
|
438
|
+
"""
|
439
|
+
if self._value is None:
|
440
|
+
raise ValueError(f"Output port '{self.name}' has no value")
|
441
|
+
return self._value
|
442
|
+
|
443
|
+
def set(self, value: T) -> None:
|
444
|
+
"""Set the value for this output port.
|
445
|
+
|
446
|
+
Args:
|
447
|
+
value: Value to set
|
448
|
+
|
449
|
+
Raises:
|
450
|
+
TypeError: If value doesn't match port type
|
451
|
+
ValueError: If value doesn't meet constraints
|
452
|
+
"""
|
453
|
+
# Type validation
|
454
|
+
if not self.validate_type(value):
|
455
|
+
raise TypeError(
|
456
|
+
f"Output port '{self.name}' expects {self.get_type_name()}, got {type(value).__name__}"
|
457
|
+
)
|
458
|
+
|
459
|
+
# Constraint validation
|
460
|
+
is_valid, error = self.validate_constraints(value)
|
461
|
+
if not is_valid:
|
462
|
+
raise ValueError(f"Output port '{self.name}' constraint violation: {error}")
|
463
|
+
|
464
|
+
self._value = value
|
465
|
+
|
466
|
+
def has_value(self) -> bool:
|
467
|
+
"""Check if this output port has been set."""
|
468
|
+
return self._value is not None
|
469
|
+
|
470
|
+
|
471
|
+
class PortRegistry:
|
472
|
+
"""Registry for managing ports in a node class."""
|
473
|
+
|
474
|
+
def __init__(self, node_class: Type):
|
475
|
+
"""Initialize port registry for a node class.
|
476
|
+
|
477
|
+
Args:
|
478
|
+
node_class: The node class to analyze
|
479
|
+
"""
|
480
|
+
self.node_class = node_class
|
481
|
+
self._input_ports: Dict[str, InputPort] = {}
|
482
|
+
self._output_ports: Dict[str, OutputPort] = {}
|
483
|
+
self._scan_ports()
|
484
|
+
|
485
|
+
def _scan_ports(self) -> None:
|
486
|
+
"""Scan the node class for port definitions."""
|
487
|
+
for name, attr in self.node_class.__dict__.items():
|
488
|
+
if isinstance(attr, InputPort):
|
489
|
+
self._input_ports[name] = attr
|
490
|
+
elif isinstance(attr, OutputPort):
|
491
|
+
self._output_ports[name] = attr
|
492
|
+
|
493
|
+
@property
|
494
|
+
def input_ports(self) -> Dict[str, InputPort]:
|
495
|
+
"""Get all input ports."""
|
496
|
+
return self._input_ports.copy()
|
497
|
+
|
498
|
+
@property
|
499
|
+
def output_ports(self) -> Dict[str, OutputPort]:
|
500
|
+
"""Get all output ports."""
|
501
|
+
return self._output_ports.copy()
|
502
|
+
|
503
|
+
def get_port_schema(self) -> Dict[str, Any]:
|
504
|
+
"""Get JSON schema for all ports."""
|
505
|
+
return {
|
506
|
+
"input_ports": {
|
507
|
+
name: port.to_dict() for name, port in self._input_ports.items()
|
508
|
+
},
|
509
|
+
"output_ports": {
|
510
|
+
name: port.to_dict() for name, port in self._output_ports.items()
|
511
|
+
},
|
512
|
+
}
|
513
|
+
|
514
|
+
def validate_input_types(self, inputs: Dict[str, Any]) -> List[str]:
|
515
|
+
"""Validate input types against port definitions.
|
516
|
+
|
517
|
+
Args:
|
518
|
+
inputs: Input values to validate
|
519
|
+
|
520
|
+
Returns:
|
521
|
+
List of validation errors (empty if all valid)
|
522
|
+
"""
|
523
|
+
errors = []
|
524
|
+
|
525
|
+
# Check required inputs
|
526
|
+
for name, port in self._input_ports.items():
|
527
|
+
if (
|
528
|
+
port.metadata.required
|
529
|
+
and name not in inputs
|
530
|
+
and port.metadata.default is None
|
531
|
+
):
|
532
|
+
errors.append(f"Required input port '{name}' is missing")
|
533
|
+
continue
|
534
|
+
|
535
|
+
if name in inputs:
|
536
|
+
value = inputs[name]
|
537
|
+
|
538
|
+
# Type validation
|
539
|
+
if not port.validate_type(value):
|
540
|
+
errors.append(
|
541
|
+
f"Input port '{name}' expects {port.get_type_name()}, got {type(value).__name__}"
|
542
|
+
)
|
543
|
+
|
544
|
+
# Constraint validation
|
545
|
+
is_valid, error = port.validate_constraints(value)
|
546
|
+
if not is_valid:
|
547
|
+
errors.append(f"Input port '{name}': {error}")
|
548
|
+
|
549
|
+
return errors
|
550
|
+
|
551
|
+
def validate_output_types(self, outputs: Dict[str, Any]) -> List[str]:
|
552
|
+
"""Validate output types against port definitions.
|
553
|
+
|
554
|
+
Args:
|
555
|
+
outputs: Output values to validate
|
556
|
+
|
557
|
+
Returns:
|
558
|
+
List of validation errors (empty if all valid)
|
559
|
+
"""
|
560
|
+
errors = []
|
561
|
+
|
562
|
+
for name, value in outputs.items():
|
563
|
+
if name in self._output_ports:
|
564
|
+
port = self._output_ports[name]
|
565
|
+
|
566
|
+
# Type validation
|
567
|
+
if not port.validate_type(value):
|
568
|
+
errors.append(
|
569
|
+
f"Output port '{name}' expects {port.get_type_name()}, got {type(value).__name__}"
|
570
|
+
)
|
571
|
+
|
572
|
+
# Constraint validation
|
573
|
+
is_valid, error = port.validate_constraints(value)
|
574
|
+
if not is_valid:
|
575
|
+
errors.append(f"Output port '{name}': {error}")
|
576
|
+
|
577
|
+
return errors
|
578
|
+
|
579
|
+
|
580
|
+
def get_port_registry(node_class: Type) -> PortRegistry:
|
581
|
+
"""Get port registry for a node class.
|
582
|
+
|
583
|
+
Args:
|
584
|
+
node_class: Node class to analyze
|
585
|
+
|
586
|
+
Returns:
|
587
|
+
PortRegistry instance
|
588
|
+
"""
|
589
|
+
if not hasattr(node_class, "_port_registry"):
|
590
|
+
node_class._port_registry = PortRegistry(node_class)
|
591
|
+
return node_class._port_registry
|
592
|
+
|
593
|
+
|
594
|
+
# Convenience type aliases for common port types
|
595
|
+
def StringPort(name: str, **kwargs) -> InputPort[str]:
|
596
|
+
"""Create a string input port."""
|
597
|
+
port = InputPort[str](name, **kwargs)
|
598
|
+
port._type_hint = str
|
599
|
+
return port
|
600
|
+
|
601
|
+
|
602
|
+
def IntPort(name: str, **kwargs) -> InputPort[int]:
|
603
|
+
"""Create an integer input port."""
|
604
|
+
port = InputPort[int](name, **kwargs)
|
605
|
+
port._type_hint = int
|
606
|
+
return port
|
607
|
+
|
608
|
+
|
609
|
+
def FloatPort(name: str, **kwargs) -> InputPort[float]:
|
610
|
+
"""Create a float input port."""
|
611
|
+
port = InputPort[float](name, **kwargs)
|
612
|
+
port._type_hint = float
|
613
|
+
return port
|
614
|
+
|
615
|
+
|
616
|
+
def BoolPort(name: str, **kwargs) -> InputPort[bool]:
|
617
|
+
"""Create a boolean input port."""
|
618
|
+
port = InputPort[bool](name, **kwargs)
|
619
|
+
port._type_hint = bool
|
620
|
+
return port
|
621
|
+
|
622
|
+
|
623
|
+
def ListPort(name: str, **kwargs) -> InputPort[List[Any]]:
|
624
|
+
"""Create a list input port."""
|
625
|
+
port = InputPort[List[Any]](name, **kwargs)
|
626
|
+
port._type_hint = List[Any]
|
627
|
+
return port
|
628
|
+
|
629
|
+
|
630
|
+
def DictPort(name: str, **kwargs) -> InputPort[Dict[str, Any]]:
|
631
|
+
"""Create a dict input port."""
|
632
|
+
port = InputPort[Dict[str, Any]](name, **kwargs)
|
633
|
+
port._type_hint = Dict[str, Any]
|
634
|
+
return port
|
635
|
+
|
636
|
+
|
637
|
+
def StringOutput(name: str, **kwargs) -> OutputPort[str]:
|
638
|
+
"""Create a string output port."""
|
639
|
+
port = OutputPort[str](name, **kwargs)
|
640
|
+
port._type_hint = str
|
641
|
+
return port
|
642
|
+
|
643
|
+
|
644
|
+
def IntOutput(name: str, **kwargs) -> OutputPort[int]:
|
645
|
+
"""Create an integer output port."""
|
646
|
+
port = OutputPort[int](name, **kwargs)
|
647
|
+
port._type_hint = int
|
648
|
+
return port
|
649
|
+
|
650
|
+
|
651
|
+
def FloatOutput(name: str, **kwargs) -> OutputPort[float]:
|
652
|
+
"""Create a float output port."""
|
653
|
+
port = OutputPort[float](name, **kwargs)
|
654
|
+
port._type_hint = float
|
655
|
+
return port
|
656
|
+
|
657
|
+
|
658
|
+
def BoolOutput(name: str, **kwargs) -> OutputPort[bool]:
|
659
|
+
"""Create a boolean output port."""
|
660
|
+
port = OutputPort[bool](name, **kwargs)
|
661
|
+
port._type_hint = bool
|
662
|
+
return port
|
663
|
+
|
664
|
+
|
665
|
+
def ListOutput(name: str, **kwargs) -> OutputPort[List[Any]]:
|
666
|
+
"""Create a list output port."""
|
667
|
+
port = OutputPort[List[Any]](name, **kwargs)
|
668
|
+
port._type_hint = List[Any]
|
669
|
+
return port
|
670
|
+
|
671
|
+
|
672
|
+
def DictOutput(name: str, **kwargs) -> OutputPort[Dict[str, Any]]:
|
673
|
+
"""Create a dict output port."""
|
674
|
+
port = OutputPort[Dict[str, Any]](name, **kwargs)
|
675
|
+
port._type_hint = Dict[str, Any]
|
676
|
+
return port
|