kailash 0.8.4__py3-none-any.whl → 0.8.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. kailash/__init__.py +5 -11
  2. kailash/channels/__init__.py +2 -1
  3. kailash/channels/mcp_channel.py +23 -4
  4. kailash/cli/__init__.py +11 -1
  5. kailash/cli/validate_imports.py +202 -0
  6. kailash/cli/validation_audit.py +570 -0
  7. kailash/core/actors/supervisor.py +1 -1
  8. kailash/core/resilience/bulkhead.py +15 -5
  9. kailash/core/resilience/circuit_breaker.py +74 -1
  10. kailash/core/resilience/health_monitor.py +433 -33
  11. kailash/edge/compliance.py +33 -0
  12. kailash/edge/consistency.py +609 -0
  13. kailash/edge/coordination/__init__.py +30 -0
  14. kailash/edge/coordination/global_ordering.py +355 -0
  15. kailash/edge/coordination/leader_election.py +217 -0
  16. kailash/edge/coordination/partition_detector.py +296 -0
  17. kailash/edge/coordination/raft.py +485 -0
  18. kailash/edge/discovery.py +63 -1
  19. kailash/edge/migration/__init__.py +19 -0
  20. kailash/edge/migration/edge_migration_service.py +384 -0
  21. kailash/edge/migration/edge_migrator.py +832 -0
  22. kailash/edge/monitoring/__init__.py +21 -0
  23. kailash/edge/monitoring/edge_monitor.py +736 -0
  24. kailash/edge/prediction/__init__.py +10 -0
  25. kailash/edge/prediction/predictive_warmer.py +591 -0
  26. kailash/edge/resource/__init__.py +102 -0
  27. kailash/edge/resource/cloud_integration.py +796 -0
  28. kailash/edge/resource/cost_optimizer.py +949 -0
  29. kailash/edge/resource/docker_integration.py +919 -0
  30. kailash/edge/resource/kubernetes_integration.py +893 -0
  31. kailash/edge/resource/platform_integration.py +913 -0
  32. kailash/edge/resource/predictive_scaler.py +959 -0
  33. kailash/edge/resource/resource_analyzer.py +824 -0
  34. kailash/edge/resource/resource_pools.py +610 -0
  35. kailash/integrations/dataflow_edge.py +261 -0
  36. kailash/mcp_server/registry_integration.py +1 -1
  37. kailash/mcp_server/server.py +351 -8
  38. kailash/mcp_server/transports.py +305 -0
  39. kailash/middleware/gateway/event_store.py +1 -0
  40. kailash/monitoring/__init__.py +18 -0
  41. kailash/monitoring/alerts.py +646 -0
  42. kailash/monitoring/metrics.py +677 -0
  43. kailash/nodes/__init__.py +2 -0
  44. kailash/nodes/ai/semantic_memory.py +2 -2
  45. kailash/nodes/base.py +622 -1
  46. kailash/nodes/code/python.py +44 -3
  47. kailash/nodes/data/async_sql.py +42 -20
  48. kailash/nodes/edge/__init__.py +36 -0
  49. kailash/nodes/edge/base.py +240 -0
  50. kailash/nodes/edge/cloud_node.py +710 -0
  51. kailash/nodes/edge/coordination.py +239 -0
  52. kailash/nodes/edge/docker_node.py +825 -0
  53. kailash/nodes/edge/edge_data.py +582 -0
  54. kailash/nodes/edge/edge_migration_node.py +396 -0
  55. kailash/nodes/edge/edge_monitoring_node.py +421 -0
  56. kailash/nodes/edge/edge_state.py +673 -0
  57. kailash/nodes/edge/edge_warming_node.py +393 -0
  58. kailash/nodes/edge/kubernetes_node.py +652 -0
  59. kailash/nodes/edge/platform_node.py +766 -0
  60. kailash/nodes/edge/resource_analyzer_node.py +378 -0
  61. kailash/nodes/edge/resource_optimizer_node.py +501 -0
  62. kailash/nodes/edge/resource_scaler_node.py +397 -0
  63. kailash/nodes/governance.py +410 -0
  64. kailash/nodes/ports.py +676 -0
  65. kailash/nodes/rag/registry.py +1 -1
  66. kailash/nodes/transaction/distributed_transaction_manager.py +48 -1
  67. kailash/nodes/transaction/saga_state_storage.py +2 -1
  68. kailash/nodes/validation.py +8 -8
  69. kailash/runtime/local.py +374 -1
  70. kailash/runtime/validation/__init__.py +12 -0
  71. kailash/runtime/validation/connection_context.py +119 -0
  72. kailash/runtime/validation/enhanced_error_formatter.py +202 -0
  73. kailash/runtime/validation/error_categorizer.py +164 -0
  74. kailash/runtime/validation/import_validator.py +446 -0
  75. kailash/runtime/validation/metrics.py +380 -0
  76. kailash/runtime/validation/performance.py +615 -0
  77. kailash/runtime/validation/suggestion_engine.py +212 -0
  78. kailash/testing/fixtures.py +2 -2
  79. kailash/utils/data_paths.py +74 -0
  80. kailash/workflow/builder.py +413 -8
  81. kailash/workflow/contracts.py +418 -0
  82. kailash/workflow/edge_infrastructure.py +369 -0
  83. kailash/workflow/mermaid_visualizer.py +3 -1
  84. kailash/workflow/migration.py +3 -3
  85. kailash/workflow/templates.py +6 -6
  86. kailash/workflow/type_inference.py +669 -0
  87. kailash/workflow/validation.py +134 -3
  88. {kailash-0.8.4.dist-info → kailash-0.8.6.dist-info}/METADATA +52 -34
  89. {kailash-0.8.4.dist-info → kailash-0.8.6.dist-info}/RECORD +93 -42
  90. kailash/nexus/__init__.py +0 -21
  91. kailash/nexus/cli/__init__.py +0 -5
  92. kailash/nexus/cli/__main__.py +0 -6
  93. kailash/nexus/cli/main.py +0 -176
  94. kailash/nexus/factory.py +0 -413
  95. kailash/nexus/gateway.py +0 -545
  96. {kailash-0.8.4.dist-info → kailash-0.8.6.dist-info}/WHEEL +0 -0
  97. {kailash-0.8.4.dist-info → kailash-0.8.6.dist-info}/entry_points.txt +0 -0
  98. {kailash-0.8.4.dist-info → kailash-0.8.6.dist-info}/licenses/LICENSE +0 -0
  99. {kailash-0.8.4.dist-info → kailash-0.8.6.dist-info}/top_level.txt +0 -0
kailash/nodes/base.py CHANGED
@@ -31,6 +31,7 @@ from typing import Any
31
31
 
32
32
  from pydantic import BaseModel, Field, ValidationError
33
33
 
34
+ from kailash.nodes.ports import InputPort, OutputPort, get_port_registry
34
35
  from kailash.sdk_exceptions import (
35
36
  NodeConfigurationError,
36
37
  NodeExecutionError,
@@ -254,6 +255,54 @@ class Node(ABC):
254
255
  f"Failed to initialize node '{self.id}': {e}"
255
256
  ) from e
256
257
 
258
+ def get_workflow_context(self, key: str, default: Any = None) -> Any:
259
+ """Get a value from the workflow context.
260
+
261
+ This method allows nodes to retrieve shared state from the workflow
262
+ execution context. The workflow context is managed by the runtime
263
+ and provides a way for nodes to share data within a single workflow
264
+ execution.
265
+
266
+ Args:
267
+ key: The key to retrieve from the workflow context
268
+ default: Default value to return if key is not found
269
+
270
+ Returns:
271
+ The value from the workflow context, or default if not found
272
+
273
+ Example:
274
+ >>> # In a transaction node
275
+ >>> connection = self.get_workflow_context('transaction_connection')
276
+ >>> if connection:
277
+ >>> # Use the shared connection
278
+ >>> result = await connection.execute(query)
279
+ """
280
+ if not hasattr(self, "_workflow_context"):
281
+ self._workflow_context = {}
282
+ return self._workflow_context.get(key, default)
283
+
284
+ def set_workflow_context(self, key: str, value: Any) -> None:
285
+ """Set a value in the workflow context.
286
+
287
+ This method allows nodes to store shared state in the workflow
288
+ execution context. Other nodes in the same workflow execution
289
+ can retrieve this data using get_workflow_context().
290
+
291
+ Args:
292
+ key: The key to store the value under
293
+ value: The value to store in the workflow context
294
+
295
+ Example:
296
+ >>> # In a transaction scope node
297
+ >>> connection = await self.get_connection()
298
+ >>> transaction = await connection.begin()
299
+ >>> self.set_workflow_context('transaction_connection', connection)
300
+ >>> self.set_workflow_context('active_transaction', transaction)
301
+ """
302
+ if not hasattr(self, "_workflow_context"):
303
+ self._workflow_context = {}
304
+ self._workflow_context[key] = value
305
+
257
306
  @abstractmethod
258
307
  def get_parameters(self) -> dict[str, NodeParameter]:
259
308
  """Define the parameters this node accepts.
@@ -466,9 +515,23 @@ class Node(ABC):
466
515
  # Skip type checking for Any type
467
516
  if param_def.type is Any:
468
517
  continue
518
+ # Skip validation for template expressions like ${variable_name}
519
+ if isinstance(value, str) and self._is_template_expression(value):
520
+ continue
469
521
  if not isinstance(value, param_def.type):
470
522
  try:
471
- self.config[param_name] = param_def.type(value)
523
+ # Special handling for datetime conversion from ISO strings
524
+ if param_def.type.__name__ == "datetime" and isinstance(
525
+ value, str
526
+ ):
527
+ from datetime import datetime
528
+
529
+ # Try to parse ISO format string
530
+ self.config[param_name] = datetime.fromisoformat(
531
+ value.replace("Z", "+00:00")
532
+ )
533
+ else:
534
+ self.config[param_name] = param_def.type(value)
472
535
  except (ValueError, TypeError) as e:
473
536
  raise NodeConfigurationError(
474
537
  f"Configuration parameter '{param_name}' must be of type "
@@ -476,6 +539,20 @@ class Node(ABC):
476
539
  f"Conversion failed: {e}"
477
540
  ) from e
478
541
 
542
+ def _is_template_expression(self, value: str) -> bool:
543
+ """Check if a string value is a template expression like ${variable_name}.
544
+
545
+ Args:
546
+ value: String value to check
547
+
548
+ Returns:
549
+ True if the value is a template expression, False otherwise
550
+ """
551
+ import re
552
+
553
+ # Match template expressions like ${variable_name} or ${node.output}
554
+ return bool(re.match(r"^\$\{[^}]+\}$", value))
555
+
479
556
  def _get_cached_parameters(self) -> dict[str, NodeParameter]:
480
557
  """Get cached parameter definitions.
481
558
 
@@ -1224,6 +1301,550 @@ class Node(ABC):
1224
1301
  ) from e
1225
1302
 
1226
1303
 
1304
+ class TypedNode(Node):
1305
+ """Enhanced node base class with type-safe port system.
1306
+
1307
+ This class extends the base Node with a declarative port system that provides:
1308
+
1309
+ 1. Type-safe input/output declarations using descriptors
1310
+ 2. Automatic parameter schema generation from ports
1311
+ 3. IDE support with full autocomplete and type checking
1312
+ 4. Runtime type validation and constraint enforcement
1313
+ 5. Backward compatibility with existing Node patterns
1314
+
1315
+ Design Goals:
1316
+ - Better developer experience with IDE support
1317
+ - Compile-time type checking for safer workflows
1318
+ - Declarative port definitions reduce boilerplate
1319
+ - Runtime safety through automatic validation
1320
+ - Seamless migration from existing Node classes
1321
+
1322
+ Usage Pattern:
1323
+ class MyTypedNode(TypedNode):
1324
+ # Input ports with type safety
1325
+ text_input = InputPort[str]("text_input", description="Text to process")
1326
+ count = InputPort[int]("count", default=1, description="Number of iterations")
1327
+
1328
+ # Output ports
1329
+ result = OutputPort[str]("result", description="Processed text")
1330
+ metadata = OutputPort[Dict[str, Any]]("metadata", description="Processing info")
1331
+
1332
+ def run(self, **kwargs) -> Dict[str, Any]:
1333
+ # Type-safe access to inputs
1334
+ text = self.text_input.get()
1335
+ count = self.count.get()
1336
+
1337
+ # Process data
1338
+ processed = text * count
1339
+
1340
+ # Set outputs (with type validation)
1341
+ self.result.set(processed)
1342
+ self.metadata.set({"length": len(processed), "iterations": count})
1343
+
1344
+ # Return traditional dict format
1345
+ return {
1346
+ self.result.name: processed,
1347
+ self.metadata.name: {"length": len(processed), "iterations": count}
1348
+ }
1349
+
1350
+ Migration Benefits:
1351
+ - Existing Node.run() signature unchanged
1352
+ - get_parameters() automatically generated from ports
1353
+ - execute() handles port-to-parameter conversion
1354
+ - Full backward compatibility maintained
1355
+
1356
+ Advanced Features:
1357
+ - Port constraints (min/max length, value ranges, patterns)
1358
+ - Complex type support (Union, Optional, List[T], Dict[K,V])
1359
+ - Port metadata for documentation and UI generation
1360
+ - Connection compatibility checking
1361
+ """
1362
+
1363
+ def __init__(self, **kwargs):
1364
+ """Initialize typed node with port system integration.
1365
+
1366
+ Performs the same initialization as Node, plus:
1367
+ 1. Scan class for port definitions
1368
+ 2. Set up port registry for validation
1369
+ 3. Initialize port instances for this node
1370
+
1371
+ Args:
1372
+ **kwargs: Node configuration including port defaults
1373
+ """
1374
+ # Set up port registry BEFORE calling super().__init__()
1375
+ # because base class will call get_parameters() during validation
1376
+ self._port_registry = get_port_registry(self.__class__)
1377
+
1378
+ # Initialize base node
1379
+ super().__init__(**kwargs)
1380
+
1381
+ # Set default values for input ports from config
1382
+ for port_name, port in self._port_registry.input_ports.items():
1383
+ if hasattr(self, port_name):
1384
+ bound_port = getattr(self, port_name)
1385
+ # Set default from config if available
1386
+ if port_name in self.config and hasattr(bound_port, "set"):
1387
+ try:
1388
+ bound_port.set(self.config[port_name])
1389
+ except (TypeError, ValueError):
1390
+ # If type validation fails, let normal validation handle it
1391
+ pass
1392
+
1393
+ def get_parameters(self) -> dict[str, NodeParameter]:
1394
+ """Generate parameter schema from port definitions.
1395
+
1396
+ Automatically creates NodeParameter definitions from InputPort declarations,
1397
+ providing seamless integration with existing Node validation systems.
1398
+
1399
+ Returns:
1400
+ Dictionary mapping parameter names to NodeParameter instances
1401
+ generated from port definitions
1402
+ """
1403
+ parameters = {}
1404
+
1405
+ for port_name, port in self._port_registry.input_ports.items():
1406
+ # Convert port metadata to NodeParameter
1407
+ param_type = port.type_hint if port.type_hint else Any
1408
+
1409
+ # Handle generic types - NodeParameter expects plain types
1410
+ if hasattr(param_type, "__origin__"):
1411
+ # For generic types like List[str], Dict[str, Any], use the origin type
1412
+ from typing import Union, get_origin
1413
+
1414
+ origin = get_origin(param_type)
1415
+ if origin is Union:
1416
+ # For Union types (including Optional), use object as a safe fallback
1417
+ param_type = object
1418
+ else:
1419
+ param_type = origin or param_type
1420
+
1421
+ parameters[port_name] = NodeParameter(
1422
+ name=port_name,
1423
+ type=param_type,
1424
+ required=port.metadata.required,
1425
+ default=port.metadata.default,
1426
+ description=port.metadata.description,
1427
+ )
1428
+
1429
+ return parameters
1430
+
1431
+ def get_output_schema(self) -> dict[str, NodeParameter]:
1432
+ """Generate output schema from port definitions.
1433
+
1434
+ Creates output parameter definitions from OutputPort declarations,
1435
+ enabling output validation and documentation generation.
1436
+
1437
+ Returns:
1438
+ Dictionary mapping output names to NodeParameter instances
1439
+ """
1440
+ outputs = {}
1441
+
1442
+ for port_name, port in self._port_registry.output_ports.items():
1443
+ param_type = port.type_hint if port.type_hint else Any
1444
+
1445
+ # Handle generic types - NodeParameter expects plain types
1446
+ if hasattr(param_type, "__origin__"):
1447
+ # For generic types like List[str], Dict[str, Any], use the origin type
1448
+ from typing import Union, get_origin
1449
+
1450
+ origin = get_origin(param_type)
1451
+ if origin is Union:
1452
+ # For Union types (including Optional), use object as a safe fallback
1453
+ param_type = object
1454
+ else:
1455
+ param_type = origin or param_type
1456
+
1457
+ outputs[port_name] = NodeParameter(
1458
+ name=port_name,
1459
+ type=param_type,
1460
+ required=False, # Output ports are generally not "required"
1461
+ default=None,
1462
+ description=port.metadata.description,
1463
+ )
1464
+
1465
+ return outputs
1466
+
1467
+ def validate_inputs(self, **kwargs) -> dict[str, Any]:
1468
+ """Enhanced input validation using port system.
1469
+
1470
+ Performs validation in two phases:
1471
+ 1. Standard Node validation for backward compatibility
1472
+ 2. Port-specific validation for enhanced type checking
1473
+
1474
+ This dual approach ensures:
1475
+ - Existing validation logic continues to work
1476
+ - Enhanced type safety from port definitions
1477
+ - Constraint validation (min/max, patterns, etc.)
1478
+ - Better error messages with port context
1479
+
1480
+ Args:
1481
+ **kwargs: Runtime inputs to validate
1482
+
1483
+ Returns:
1484
+ Validated inputs with type conversions applied
1485
+
1486
+ Raises:
1487
+ NodeValidationError: If validation fails with enhanced error context
1488
+ """
1489
+ # First, run standard Node validation
1490
+ validated = super().validate_inputs(**kwargs)
1491
+
1492
+ # Then, perform port-specific validation
1493
+ port_errors = self._port_registry.validate_input_types(validated)
1494
+ if port_errors:
1495
+ error_details = "; ".join(port_errors)
1496
+ raise NodeValidationError(
1497
+ f"Port validation failed for node '{self.id}': {error_details}"
1498
+ )
1499
+
1500
+ # Set validated values in bound ports for type-safe access
1501
+ # This allows port.get() to work during run() execution
1502
+ for port_name, port in self._port_registry.input_ports.items():
1503
+ if port_name in validated:
1504
+ bound_port = getattr(self, port_name, None)
1505
+ if bound_port and hasattr(bound_port, "set"):
1506
+ try:
1507
+ bound_port.set(validated[port_name])
1508
+ except (TypeError, ValueError):
1509
+ # Port validation should have caught this, but be safe
1510
+ pass
1511
+ elif hasattr(self, port_name):
1512
+ # If bound port doesn't have set method, set the value directly
1513
+ port_instance = getattr(self, port_name)
1514
+ if hasattr(port_instance, "_value"):
1515
+ port_instance._value = validated[port_name]
1516
+
1517
+ return validated
1518
+
1519
+ def validate_outputs(self, outputs: dict[str, Any]) -> dict[str, Any]:
1520
+ """Enhanced output validation using port system.
1521
+
1522
+ Validates outputs using both standard Node validation and port definitions:
1523
+ 1. Standard JSON serializability checks
1524
+ 2. Port type validation with enhanced error messages
1525
+ 3. Constraint validation for output values
1526
+
1527
+ Args:
1528
+ outputs: Output dictionary from run() method
1529
+
1530
+ Returns:
1531
+ Validated outputs
1532
+
1533
+ Raises:
1534
+ NodeValidationError: If validation fails
1535
+ """
1536
+ # First, run standard Node validation
1537
+ validated = super().validate_outputs(outputs)
1538
+
1539
+ # Then, perform port-specific validation
1540
+ port_errors = self._port_registry.validate_output_types(validated)
1541
+ if port_errors:
1542
+ error_details = "; ".join(port_errors)
1543
+ raise NodeValidationError(
1544
+ f"Output port validation failed for node '{self.id}': {error_details}"
1545
+ )
1546
+
1547
+ return validated
1548
+
1549
+ def get_port_schema(self) -> dict[str, Any]:
1550
+ """Get complete port schema for documentation and tooling.
1551
+
1552
+ Returns the full port schema including type information,
1553
+ constraints, examples, and metadata. Used by:
1554
+ - Documentation generators
1555
+ - UI form builders
1556
+ - Workflow validation tools
1557
+ - Type inference systems
1558
+
1559
+ Returns:
1560
+ Complete port schema with input and output definitions
1561
+ """
1562
+ return self._port_registry.get_port_schema()
1563
+
1564
+ def to_dict(self) -> dict[str, Any]:
1565
+ """Enhanced serialization including port information.
1566
+
1567
+ Extends base Node serialization with port schema information
1568
+ for complete node documentation and reconstruction.
1569
+
1570
+ Returns:
1571
+ Node dictionary with port schema included
1572
+ """
1573
+ base_dict = super().to_dict()
1574
+ base_dict["port_schema"] = self.get_port_schema()
1575
+ return base_dict
1576
+
1577
+
1578
+ class AsyncTypedNode(TypedNode):
1579
+ """Async version of TypedNode with full async support.
1580
+
1581
+ This class combines the type-safe port system from TypedNode with
1582
+ the async execution capabilities of AsyncNode, providing:
1583
+
1584
+ 1. Type-safe input/output ports with async execution
1585
+ 2. Async-first execution with execute_async() and async_run()
1586
+ 3. All port validation and type checking in async context
1587
+ 4. Full backward compatibility with TypedNode patterns
1588
+ 5. Optimal performance for I/O-bound async operations
1589
+
1590
+ Design Goals:
1591
+ - Async-first execution for modern Kailash workflows
1592
+ - Type safety with full IDE support in async context
1593
+ - Seamless port access during async execution
1594
+ - Compatible with AsyncLocalRuntime and async workflows
1595
+
1596
+ Usage Pattern:
1597
+ class MyAsyncTypedNode(AsyncTypedNode):
1598
+ # Same port declarations as TypedNode
1599
+ text_input = InputPort[str]("text_input", description="Text to process")
1600
+ count = InputPort[int]("count", default=1, description="Number of iterations")
1601
+
1602
+ # Output ports
1603
+ result = OutputPort[str]("result", description="Processed text")
1604
+ metadata = OutputPort[Dict[str, Any]]("metadata", description="Processing info")
1605
+
1606
+ async def async_run(self, **kwargs) -> Dict[str, Any]:
1607
+ # Type-safe async access to inputs
1608
+ text = self.text_input.get()
1609
+ count = self.count.get()
1610
+
1611
+ # Async processing (e.g., API calls, DB queries)
1612
+ processed = await self.process_async(text, count)
1613
+
1614
+ # Set outputs (with type validation)
1615
+ self.result.set(processed)
1616
+ self.metadata.set({"length": len(processed), "iterations": count})
1617
+
1618
+ # Return traditional dict format
1619
+ return {
1620
+ self.result.name: processed,
1621
+ self.metadata.name: {"length": len(processed), "iterations": count}
1622
+ }
1623
+
1624
+ async def process_async(self, text: str, count: int) -> str:
1625
+ # Example async processing
1626
+ await asyncio.sleep(0.1) # Simulate I/O
1627
+ return text * count
1628
+
1629
+ Migration from TypedNode:
1630
+ - Change inheritance from TypedNode to AsyncTypedNode
1631
+ - Change run() method to async def async_run()
1632
+ - Add await to any async operations
1633
+ - Use execute_async() for execution instead of execute()
1634
+ """
1635
+
1636
+ def run(self, **kwargs) -> dict[str, Any]:
1637
+ """Override run() to require async_run() implementation.
1638
+
1639
+ AsyncTypedNode requires async_run() implementation for proper async execution.
1640
+ This method should not be called directly - use execute_async() instead.
1641
+
1642
+ Raises:
1643
+ NotImplementedError: Always, as async typed nodes must use async_run()
1644
+ """
1645
+ raise NotImplementedError(
1646
+ f"AsyncTypedNode '{self.__class__.__name__}' should implement async_run() method, not run()"
1647
+ )
1648
+
1649
+ async def async_run(self, **kwargs) -> dict[str, Any]:
1650
+ """Execute the async node's logic with type-safe port access.
1651
+
1652
+ This is the core method that implements the node's async data processing
1653
+ logic. It receives validated inputs and must return a dictionary of outputs.
1654
+
1655
+ Design requirements:
1656
+ - Must be async and stateless - no side effects between runs
1657
+ - All inputs are provided as keyword arguments
1658
+ - Must return a dictionary (JSON-serializable)
1659
+ - Can use self.port.get() for type-safe input access
1660
+ - Can use self.port.set() for type-safe output setting
1661
+ - Should handle errors gracefully with async context
1662
+ - Can use self.config for configuration values
1663
+ - Should use self.logger for status reporting
1664
+ - Can perform async I/O operations (API calls, DB queries, etc.)
1665
+
1666
+ Example:
1667
+ async def async_run(self, **kwargs):
1668
+ # Type-safe port access
1669
+ text = self.text_input.get()
1670
+ count = self.count.get()
1671
+
1672
+ # Async processing
1673
+ result = await self.process_text_async(text, count)
1674
+
1675
+ # Set outputs and return
1676
+ self.result.set(result)
1677
+ return {"result": result}
1678
+
1679
+ Args:
1680
+ **kwargs: Validated input parameters matching get_parameters()
1681
+
1682
+ Returns:
1683
+ Dictionary of outputs that will be validated and passed
1684
+ to downstream nodes
1685
+
1686
+ Raises:
1687
+ NodeExecutionError: If execution fails (will be caught and
1688
+ re-raised by execute_async())
1689
+
1690
+ Called by:
1691
+ - execute_async(): Wraps with validation and error handling
1692
+ - AsyncLocalRuntime: During async workflow execution
1693
+ - Async test runners: During async unit testing
1694
+ """
1695
+ raise NotImplementedError(
1696
+ f"AsyncTypedNode '{self.__class__.__name__}' must implement async_run() method"
1697
+ )
1698
+
1699
+ async def execute_async(self, **runtime_inputs) -> dict[str, Any]:
1700
+ """Execute the async node with validation and error handling.
1701
+
1702
+ This is the main entry point for async node execution that orchestrates
1703
+ the complete async execution lifecycle:
1704
+
1705
+ 1. Input validation (validate_inputs)
1706
+ 2. Async execution (async_run)
1707
+ 3. Output validation (validate_outputs)
1708
+ 4. Error handling and logging
1709
+ 5. Performance metrics
1710
+
1711
+ Async execution flow:
1712
+ 1. Logs execution start
1713
+ 2. Validates inputs against parameter schema (including port validation)
1714
+ 3. Sets validated values in ports for type-safe access
1715
+ 4. Calls async_run() with validated inputs
1716
+ 5. Validates outputs are JSON-serializable (including port validation)
1717
+ 6. Logs execution time
1718
+ 7. Returns validated outputs
1719
+
1720
+ Args:
1721
+ **runtime_inputs: Runtime inputs for async node execution
1722
+
1723
+ Returns:
1724
+ Dictionary of validated outputs from async_run()
1725
+
1726
+ Raises:
1727
+ NodeExecutionError: If async execution fails in async_run()
1728
+ NodeValidationError: If input/output validation fails
1729
+ """
1730
+ from datetime import UTC, datetime
1731
+
1732
+ start_time = datetime.now(UTC)
1733
+ try:
1734
+ self.logger.info(f"Executing async node {self.id}")
1735
+
1736
+ # Merge runtime inputs with config (runtime inputs take precedence)
1737
+ merged_inputs = {**self.config, **runtime_inputs}
1738
+
1739
+ # Handle nested config case (same as base Node)
1740
+ if "config" in merged_inputs and isinstance(merged_inputs["config"], dict):
1741
+ nested_config = merged_inputs["config"]
1742
+ for key, value in nested_config.items():
1743
+ if key not in runtime_inputs:
1744
+ merged_inputs[key] = value
1745
+
1746
+ # Validate inputs (includes port validation and setting port values)
1747
+ validated_inputs = self.validate_inputs(**merged_inputs)
1748
+ self.logger.debug(
1749
+ f"Validated inputs for async node {self.id}: {validated_inputs}"
1750
+ )
1751
+
1752
+ # Execute async node logic
1753
+ outputs = await self.async_run(**validated_inputs)
1754
+
1755
+ # Validate outputs (includes port validation)
1756
+ validated_outputs = self.validate_outputs(outputs)
1757
+
1758
+ execution_time = (datetime.now(UTC) - start_time).total_seconds()
1759
+ self.logger.info(
1760
+ f"Async node {self.id} executed successfully in {execution_time:.3f}s"
1761
+ )
1762
+ return validated_outputs
1763
+
1764
+ except NodeValidationError:
1765
+ # Re-raise validation errors as-is
1766
+ raise
1767
+ except NodeExecutionError:
1768
+ # Re-raise execution errors as-is
1769
+ raise
1770
+ except Exception as e:
1771
+ # Wrap any other exception in NodeExecutionError
1772
+ self.logger.error(
1773
+ f"Async node {self.id} execution failed: {e}", exc_info=True
1774
+ )
1775
+ raise NodeExecutionError(
1776
+ f"Async node '{self.id}' execution failed: {type(e).__name__}: {e}"
1777
+ ) from e
1778
+
1779
+ def execute(self, **runtime_inputs) -> dict[str, Any]:
1780
+ """Execute the async node synchronously by running async code.
1781
+
1782
+ This method provides backward compatibility by running the async execution
1783
+ in a synchronous context. It handles event loop management automatically.
1784
+
1785
+ For optimal performance in async workflows, use execute_async() directly.
1786
+
1787
+ Args:
1788
+ **runtime_inputs: Runtime inputs for node execution
1789
+
1790
+ Returns:
1791
+ Dictionary of validated outputs
1792
+ """
1793
+ import asyncio
1794
+ import concurrent.futures
1795
+ import sys
1796
+ import threading
1797
+
1798
+ # Handle event loop scenarios (same as AsyncNode)
1799
+ if sys.platform == "win32":
1800
+ asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
1801
+
1802
+ current_thread = threading.current_thread()
1803
+ is_main_thread = isinstance(current_thread, threading._MainThread)
1804
+
1805
+ try:
1806
+ # Try to get current event loop
1807
+ loop = asyncio.get_running_loop()
1808
+ # Event loop is running - need to run in separate thread
1809
+ return self._execute_in_thread(**runtime_inputs)
1810
+ except RuntimeError:
1811
+ # No event loop running
1812
+ if is_main_thread:
1813
+ # Main thread without loop - safe to use asyncio.run()
1814
+ return asyncio.run(self.execute_async(**runtime_inputs))
1815
+ else:
1816
+ # Non-main thread without loop - create new loop
1817
+ return self._execute_in_new_loop(**runtime_inputs)
1818
+
1819
+ def _execute_in_thread(self, **runtime_inputs) -> dict[str, Any]:
1820
+ """Execute async code in a separate thread with its own event loop."""
1821
+ import asyncio
1822
+ import concurrent.futures
1823
+
1824
+ def run_in_thread():
1825
+ loop = asyncio.new_event_loop()
1826
+ asyncio.set_event_loop(loop)
1827
+ try:
1828
+ return loop.run_until_complete(self.execute_async(**runtime_inputs))
1829
+ finally:
1830
+ loop.close()
1831
+
1832
+ with concurrent.futures.ThreadPoolExecutor() as executor:
1833
+ future = executor.submit(run_in_thread)
1834
+ return future.result()
1835
+
1836
+ def _execute_in_new_loop(self, **runtime_inputs) -> dict[str, Any]:
1837
+ """Execute async code in a new event loop."""
1838
+ import asyncio
1839
+
1840
+ new_loop = asyncio.new_event_loop()
1841
+ asyncio.set_event_loop(new_loop)
1842
+ try:
1843
+ return new_loop.run_until_complete(self.execute_async(**runtime_inputs))
1844
+ finally:
1845
+ new_loop.close()
1846
+
1847
+
1227
1848
  # Node Registry
1228
1849
  class NodeRegistry:
1229
1850
  """Registry for discovering and managing available nodes.