kailash 0.3.2__py3-none-any.whl → 0.4.1__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 (151) hide show
  1. kailash/__init__.py +33 -1
  2. kailash/access_control/__init__.py +129 -0
  3. kailash/access_control/managers.py +461 -0
  4. kailash/access_control/rule_evaluators.py +467 -0
  5. kailash/access_control_abac.py +825 -0
  6. kailash/config/__init__.py +27 -0
  7. kailash/config/database_config.py +359 -0
  8. kailash/database/__init__.py +28 -0
  9. kailash/database/execution_pipeline.py +499 -0
  10. kailash/middleware/__init__.py +306 -0
  11. kailash/middleware/auth/__init__.py +33 -0
  12. kailash/middleware/auth/access_control.py +436 -0
  13. kailash/middleware/auth/auth_manager.py +422 -0
  14. kailash/middleware/auth/jwt_auth.py +477 -0
  15. kailash/middleware/auth/kailash_jwt_auth.py +616 -0
  16. kailash/middleware/communication/__init__.py +37 -0
  17. kailash/middleware/communication/ai_chat.py +989 -0
  18. kailash/middleware/communication/api_gateway.py +802 -0
  19. kailash/middleware/communication/events.py +470 -0
  20. kailash/middleware/communication/realtime.py +710 -0
  21. kailash/middleware/core/__init__.py +21 -0
  22. kailash/middleware/core/agent_ui.py +890 -0
  23. kailash/middleware/core/schema.py +643 -0
  24. kailash/middleware/core/workflows.py +396 -0
  25. kailash/middleware/database/__init__.py +63 -0
  26. kailash/middleware/database/base.py +113 -0
  27. kailash/middleware/database/base_models.py +525 -0
  28. kailash/middleware/database/enums.py +106 -0
  29. kailash/middleware/database/migrations.py +12 -0
  30. kailash/{api/database.py → middleware/database/models.py} +183 -291
  31. kailash/middleware/database/repositories.py +685 -0
  32. kailash/middleware/database/session_manager.py +19 -0
  33. kailash/middleware/mcp/__init__.py +38 -0
  34. kailash/middleware/mcp/client_integration.py +585 -0
  35. kailash/middleware/mcp/enhanced_server.py +576 -0
  36. kailash/nodes/__init__.py +27 -3
  37. kailash/nodes/admin/__init__.py +42 -0
  38. kailash/nodes/admin/audit_log.py +794 -0
  39. kailash/nodes/admin/permission_check.py +864 -0
  40. kailash/nodes/admin/role_management.py +823 -0
  41. kailash/nodes/admin/security_event.py +1523 -0
  42. kailash/nodes/admin/user_management.py +944 -0
  43. kailash/nodes/ai/a2a.py +24 -7
  44. kailash/nodes/ai/ai_providers.py +248 -40
  45. kailash/nodes/ai/embedding_generator.py +11 -11
  46. kailash/nodes/ai/intelligent_agent_orchestrator.py +99 -11
  47. kailash/nodes/ai/llm_agent.py +436 -5
  48. kailash/nodes/ai/self_organizing.py +85 -10
  49. kailash/nodes/ai/vision_utils.py +148 -0
  50. kailash/nodes/alerts/__init__.py +26 -0
  51. kailash/nodes/alerts/base.py +234 -0
  52. kailash/nodes/alerts/discord.py +499 -0
  53. kailash/nodes/api/auth.py +287 -6
  54. kailash/nodes/api/rest.py +151 -0
  55. kailash/nodes/auth/__init__.py +17 -0
  56. kailash/nodes/auth/directory_integration.py +1228 -0
  57. kailash/nodes/auth/enterprise_auth_provider.py +1328 -0
  58. kailash/nodes/auth/mfa.py +2338 -0
  59. kailash/nodes/auth/risk_assessment.py +872 -0
  60. kailash/nodes/auth/session_management.py +1093 -0
  61. kailash/nodes/auth/sso.py +1040 -0
  62. kailash/nodes/base.py +344 -13
  63. kailash/nodes/base_cycle_aware.py +4 -2
  64. kailash/nodes/base_with_acl.py +1 -1
  65. kailash/nodes/code/python.py +283 -10
  66. kailash/nodes/compliance/__init__.py +9 -0
  67. kailash/nodes/compliance/data_retention.py +1888 -0
  68. kailash/nodes/compliance/gdpr.py +2004 -0
  69. kailash/nodes/data/__init__.py +22 -2
  70. kailash/nodes/data/async_connection.py +469 -0
  71. kailash/nodes/data/async_sql.py +757 -0
  72. kailash/nodes/data/async_vector.py +598 -0
  73. kailash/nodes/data/readers.py +767 -0
  74. kailash/nodes/data/retrieval.py +360 -1
  75. kailash/nodes/data/sharepoint_graph.py +397 -21
  76. kailash/nodes/data/sql.py +94 -5
  77. kailash/nodes/data/streaming.py +68 -8
  78. kailash/nodes/data/vector_db.py +54 -4
  79. kailash/nodes/enterprise/__init__.py +13 -0
  80. kailash/nodes/enterprise/batch_processor.py +741 -0
  81. kailash/nodes/enterprise/data_lineage.py +497 -0
  82. kailash/nodes/logic/convergence.py +31 -9
  83. kailash/nodes/logic/operations.py +14 -3
  84. kailash/nodes/mixins/__init__.py +8 -0
  85. kailash/nodes/mixins/event_emitter.py +201 -0
  86. kailash/nodes/mixins/mcp.py +9 -4
  87. kailash/nodes/mixins/security.py +165 -0
  88. kailash/nodes/monitoring/__init__.py +7 -0
  89. kailash/nodes/monitoring/performance_benchmark.py +2497 -0
  90. kailash/nodes/rag/__init__.py +284 -0
  91. kailash/nodes/rag/advanced.py +1615 -0
  92. kailash/nodes/rag/agentic.py +773 -0
  93. kailash/nodes/rag/conversational.py +999 -0
  94. kailash/nodes/rag/evaluation.py +875 -0
  95. kailash/nodes/rag/federated.py +1188 -0
  96. kailash/nodes/rag/graph.py +721 -0
  97. kailash/nodes/rag/multimodal.py +671 -0
  98. kailash/nodes/rag/optimized.py +933 -0
  99. kailash/nodes/rag/privacy.py +1059 -0
  100. kailash/nodes/rag/query_processing.py +1335 -0
  101. kailash/nodes/rag/realtime.py +764 -0
  102. kailash/nodes/rag/registry.py +547 -0
  103. kailash/nodes/rag/router.py +837 -0
  104. kailash/nodes/rag/similarity.py +1854 -0
  105. kailash/nodes/rag/strategies.py +566 -0
  106. kailash/nodes/rag/workflows.py +575 -0
  107. kailash/nodes/security/__init__.py +19 -0
  108. kailash/nodes/security/abac_evaluator.py +1411 -0
  109. kailash/nodes/security/audit_log.py +103 -0
  110. kailash/nodes/security/behavior_analysis.py +1893 -0
  111. kailash/nodes/security/credential_manager.py +401 -0
  112. kailash/nodes/security/rotating_credentials.py +760 -0
  113. kailash/nodes/security/security_event.py +133 -0
  114. kailash/nodes/security/threat_detection.py +1103 -0
  115. kailash/nodes/testing/__init__.py +9 -0
  116. kailash/nodes/testing/credential_testing.py +499 -0
  117. kailash/nodes/transform/__init__.py +10 -2
  118. kailash/nodes/transform/chunkers.py +592 -1
  119. kailash/nodes/transform/processors.py +484 -14
  120. kailash/nodes/validation.py +321 -0
  121. kailash/runtime/access_controlled.py +1 -1
  122. kailash/runtime/async_local.py +41 -7
  123. kailash/runtime/docker.py +1 -1
  124. kailash/runtime/local.py +474 -55
  125. kailash/runtime/parallel.py +1 -1
  126. kailash/runtime/parallel_cyclic.py +1 -1
  127. kailash/runtime/testing.py +210 -2
  128. kailash/security.py +1 -1
  129. kailash/utils/migrations/__init__.py +25 -0
  130. kailash/utils/migrations/generator.py +433 -0
  131. kailash/utils/migrations/models.py +231 -0
  132. kailash/utils/migrations/runner.py +489 -0
  133. kailash/utils/secure_logging.py +342 -0
  134. kailash/workflow/__init__.py +16 -0
  135. kailash/workflow/cyclic_runner.py +3 -4
  136. kailash/workflow/graph.py +70 -2
  137. kailash/workflow/resilience.py +249 -0
  138. kailash/workflow/templates.py +726 -0
  139. {kailash-0.3.2.dist-info → kailash-0.4.1.dist-info}/METADATA +256 -20
  140. kailash-0.4.1.dist-info/RECORD +227 -0
  141. kailash/api/__init__.py +0 -17
  142. kailash/api/__main__.py +0 -6
  143. kailash/api/studio_secure.py +0 -893
  144. kailash/mcp/__main__.py +0 -13
  145. kailash/mcp/server_new.py +0 -336
  146. kailash/mcp/servers/__init__.py +0 -12
  147. kailash-0.3.2.dist-info/RECORD +0 -136
  148. {kailash-0.3.2.dist-info → kailash-0.4.1.dist-info}/WHEEL +0 -0
  149. {kailash-0.3.2.dist-info → kailash-0.4.1.dist-info}/entry_points.txt +0 -0
  150. {kailash-0.3.2.dist-info → kailash-0.4.1.dist-info}/licenses/LICENSE +0 -0
  151. {kailash-0.3.2.dist-info → kailash-0.4.1.dist-info}/top_level.txt +0 -0
kailash/nodes/api/auth.py CHANGED
@@ -155,12 +155,17 @@ class OAuth2Node(Node):
155
155
  username (str, optional): Username for password grant
156
156
  password (str, optional): Password for password grant
157
157
  refresh_token (str, optional): Refresh token for refresh flow
158
+ refresh_buffer_seconds (int, optional): Seconds before expiry to trigger refresh (default: 300)
159
+ validate_token_response (bool, optional): Whether to validate token response (default: True)
158
160
  **kwargs: Additional parameters passed to base Node
159
161
  """
160
162
  super().__init__(**kwargs)
161
163
  self.http_node = HTTPRequestNode(**kwargs)
162
164
  self.token_data = None # Will store token information
163
165
  self.token_expires_at = 0 # Timestamp when token expires
166
+ self.refresh_buffer_seconds = kwargs.get("refresh_buffer_seconds", 300)
167
+ self.validate_token_response = kwargs.get("validate_token_response", True)
168
+ self._last_token_request_duration = None
164
169
 
165
170
  def get_parameters(self) -> dict[str, NodeParameter]:
166
171
  """Define the parameters this node accepts.
@@ -234,6 +239,27 @@ class OAuth2Node(Node):
234
239
  default=True,
235
240
  description="Whether to automatically refresh expired tokens",
236
241
  ),
242
+ "refresh_buffer_seconds": NodeParameter(
243
+ name="refresh_buffer_seconds",
244
+ type=int,
245
+ required=False,
246
+ default=300,
247
+ description="Seconds before token expiry to trigger automatic refresh",
248
+ ),
249
+ "validate_token_response": NodeParameter(
250
+ name="validate_token_response",
251
+ type=bool,
252
+ required=False,
253
+ default=True,
254
+ description="Whether to validate token response structure",
255
+ ),
256
+ "include_token_metadata": NodeParameter(
257
+ name="include_token_metadata",
258
+ type=bool,
259
+ required=False,
260
+ default=False,
261
+ description="Include additional token metadata in response",
262
+ ),
237
263
  }
238
264
 
239
265
  def get_output_schema(self) -> dict[str, NodeParameter]:
@@ -267,8 +293,132 @@ class OAuth2Node(Node):
267
293
  required=False,
268
294
  description="Seconds until token expiration",
269
295
  ),
296
+ "token_type": NodeParameter(
297
+ name="token_type",
298
+ type=str,
299
+ required=False,
300
+ description="Token type from response (usually 'Bearer')",
301
+ ),
302
+ "scope": NodeParameter(
303
+ name="scope",
304
+ type=str,
305
+ required=False,
306
+ description="Actual granted scopes from response",
307
+ ),
308
+ "refresh_token_present": NodeParameter(
309
+ name="refresh_token_present",
310
+ type=bool,
311
+ required=False,
312
+ description="Whether a refresh token is available",
313
+ ),
314
+ "token_expires_at": NodeParameter(
315
+ name="token_expires_at",
316
+ type=str,
317
+ required=False,
318
+ description="ISO format timestamp of token expiration",
319
+ ),
320
+ "raw_response": NodeParameter(
321
+ name="raw_response",
322
+ type=dict,
323
+ required=False,
324
+ description="Full token response for debugging (if include_raw_response is True)",
325
+ ),
326
+ "token": NodeParameter(
327
+ name="token",
328
+ type=dict,
329
+ required=False,
330
+ description="Structured token information with all components (if include_token_metadata is True)",
331
+ ),
332
+ "metadata": NodeParameter(
333
+ name="metadata",
334
+ type=dict,
335
+ required=False,
336
+ description="Additional token metadata and health information (if include_token_metadata is True)",
337
+ ),
338
+ }
339
+
340
+ def _validate_token_response(self, token_data: dict) -> None:
341
+ """Validate the token response structure.
342
+
343
+ Args:
344
+ token_data: Token response from OAuth server
345
+
346
+ Raises:
347
+ NodeExecutionError: If token response is invalid
348
+ """
349
+ required_fields = ["access_token"]
350
+ missing_fields = [field for field in required_fields if field not in token_data]
351
+
352
+ if missing_fields:
353
+ raise NodeExecutionError(
354
+ f"Invalid token response - missing required fields: {missing_fields}. "
355
+ f"Response contained: {list(token_data.keys())}. "
356
+ "Please verify your OAuth configuration and credentials."
357
+ )
358
+
359
+ # Validate token format
360
+ access_token = token_data.get("access_token", "")
361
+ if not access_token or not isinstance(access_token, str):
362
+ raise NodeExecutionError(
363
+ "Invalid access token format. Token must be a non-empty string. "
364
+ "Please check your OAuth server configuration."
365
+ )
366
+
367
+ def _calculate_token_health(self, expires_in: int) -> dict[str, Any]:
368
+ """Calculate token health metrics.
369
+
370
+ Args:
371
+ expires_in: Seconds until token expiration
372
+
373
+ Returns:
374
+ Dictionary with health metrics
375
+ """
376
+ health = {
377
+ "status": "healthy",
378
+ "expires_in_seconds": expires_in,
379
+ "expires_in_minutes": round(expires_in / 60, 1),
380
+ "expires_in_human": self._format_duration(expires_in),
381
+ "should_refresh": expires_in <= self.refresh_buffer_seconds,
382
+ "health_percentage": min(
383
+ 100, (expires_in / 3600) * 100
384
+ ), # Assume 1hr tokens
270
385
  }
271
386
 
387
+ # Determine health status
388
+ if expires_in <= 0:
389
+ health["status"] = "expired"
390
+ elif expires_in <= 60:
391
+ health["status"] = "critical"
392
+ elif expires_in <= self.refresh_buffer_seconds:
393
+ health["status"] = "needs_refresh"
394
+ elif expires_in <= 600: # 10 minutes
395
+ health["status"] = "warning"
396
+
397
+ return health
398
+
399
+ def _format_duration(self, seconds: int) -> str:
400
+ """Format duration in human-readable format.
401
+
402
+ Args:
403
+ seconds: Duration in seconds
404
+
405
+ Returns:
406
+ Human-readable duration string
407
+ """
408
+ if seconds <= 0:
409
+ return "expired"
410
+ elif seconds < 60:
411
+ return f"{seconds} seconds"
412
+ elif seconds < 3600:
413
+ minutes = seconds // 60
414
+ return f"{minutes} minute{'s' if minutes > 1 else ''}"
415
+ else:
416
+ hours = seconds // 3600
417
+ minutes = (seconds % 3600) // 60
418
+ if minutes > 0:
419
+ return f"{hours} hour{'s' if hours > 1 else ''} {minutes} minute{'s' if minutes > 1 else ''}"
420
+ return f"{hours} hour{'s' if hours > 1 else ''}"
421
+
272
422
  def _get_token(self, **kwargs) -> dict[str, Any]:
273
423
  """Get an OAuth token using the configured grant type.
274
424
 
@@ -338,12 +488,17 @@ class OAuth2Node(Node):
338
488
  if "client_secret" in data:
339
489
  del data["client_secret"]
340
490
 
491
+ start_time = time.time()
492
+
341
493
  try:
342
494
  response = requests.post(token_url, data=data, headers=headers, timeout=30)
343
495
 
344
496
  response.raise_for_status()
345
497
  token_data = response.json()
346
498
 
499
+ # Record request duration
500
+ self._last_token_request_duration = int((time.time() - start_time) * 1000)
501
+
347
502
  # Check for required fields in response
348
503
  if "access_token" not in token_data:
349
504
  raise NodeExecutionError(
@@ -353,7 +508,34 @@ class OAuth2Node(Node):
353
508
  return token_data
354
509
 
355
510
  except requests.RequestException as e:
356
- raise NodeExecutionError(f"Failed to acquire OAuth token: {str(e)}") from e
511
+ # Enhance error message with suggestions
512
+ error_msg = f"Failed to acquire OAuth token: {str(e)}"
513
+
514
+ suggestions = []
515
+ if "401" in str(e) or "unauthorized" in str(e).lower():
516
+ suggestions.append(
517
+ "Verify your client_id and client_secret are correct"
518
+ )
519
+ suggestions.append(
520
+ "Check if your credentials have the required permissions"
521
+ )
522
+ elif "400" in str(e) or "bad request" in str(e).lower():
523
+ suggestions.append(
524
+ "Verify the grant_type is supported by your OAuth server"
525
+ )
526
+ suggestions.append("Check if all required parameters are provided")
527
+ elif "connection" in str(e).lower() or "timeout" in str(e).lower():
528
+ suggestions.append("Verify the token_url is correct and accessible")
529
+ suggestions.append(
530
+ "Check your network connection and firewall settings"
531
+ )
532
+
533
+ if suggestions:
534
+ error_msg += "\n\nSuggestions:\n" + "\n".join(
535
+ f"- {s}" for s in suggestions
536
+ )
537
+
538
+ raise NodeExecutionError(error_msg) from e
357
539
  except ValueError as e:
358
540
  raise NodeExecutionError(
359
541
  f"Failed to parse OAuth token response: {str(e)}"
@@ -383,22 +565,39 @@ class OAuth2Node(Node):
383
565
  token_data: Complete token response
384
566
  auth_type: Authentication type ('oauth2')
385
567
  expires_in: Seconds until token expiration
568
+ token_type: Token type from response (usually 'Bearer')
569
+ scope: Actual granted scopes from response
570
+ refresh_token_present: Whether a refresh token is available
571
+ token_expires_at: ISO format timestamp of token expiration
572
+ raw_response: Full token response for debugging (if include_raw is True)
386
573
  """
387
574
  force_refresh = kwargs.get("force_refresh", False)
388
575
  auto_refresh = kwargs.get("auto_refresh", True)
576
+ include_raw = kwargs.get("include_raw_response", False)
577
+ include_metadata = kwargs.get("include_token_metadata", False)
389
578
 
390
579
  current_time = time.time()
391
580
 
392
581
  # Check if we need to refresh the token
393
- if (
582
+ # Consider the refresh buffer when determining if refresh is needed
583
+ needs_refresh = (
394
584
  not self.token_data
395
585
  or force_refresh
396
- or (auto_refresh and current_time >= self.token_expires_at)
397
- ):
586
+ or (
587
+ auto_refresh
588
+ and current_time
589
+ >= (self.token_expires_at - self.refresh_buffer_seconds)
590
+ )
591
+ )
398
592
 
593
+ if needs_refresh:
399
594
  # Get new token
400
595
  self.token_data = self._get_token(**kwargs)
401
596
 
597
+ # Validate token response if requested
598
+ if self.validate_token_response:
599
+ self._validate_token_response(self.token_data)
600
+
402
601
  # Calculate expiration time
403
602
  expires_in = self.token_data.get("expires_in", 3600) # Default 1 hour
404
603
  self.token_expires_at = current_time + expires_in
@@ -411,15 +610,97 @@ class OAuth2Node(Node):
411
610
  current_expires_in = max(0, int(self.token_expires_at - current_time))
412
611
 
413
612
  # Create headers
414
- headers = {"Authorization": f"Bearer {self.token_data['access_token']}"}
613
+ access_token = self.token_data.get("access_token", "")
614
+ token_type = self.token_data.get("token_type", "Bearer")
415
615
 
416
- return {
616
+ # Format authorization header based on token type
617
+ if token_type.lower() == "bearer":
618
+ headers = {"Authorization": f"Bearer {access_token}"}
619
+ else:
620
+ headers = {"Authorization": f"{token_type} {access_token}"}
621
+
622
+ # Calculate expiration timestamp
623
+ from datetime import datetime, timezone
624
+
625
+ token_expires_at_dt = datetime.fromtimestamp(
626
+ self.token_expires_at, tz=timezone.utc
627
+ )
628
+ token_expires_at_iso = token_expires_at_dt.isoformat()
629
+
630
+ # Build response
631
+ result = {
417
632
  "headers": headers,
418
633
  "token_data": self.token_data,
419
634
  "auth_type": "oauth2",
420
635
  "expires_in": current_expires_in,
636
+ "token_type": token_type,
637
+ "scope": self.token_data.get("scope", ""),
638
+ "refresh_token_present": bool(self.token_data.get("refresh_token")),
639
+ "token_expires_at": token_expires_at_iso,
421
640
  }
422
641
 
642
+ # Include raw response if requested
643
+ if include_raw:
644
+ result["raw_response"] = self.token_data.copy()
645
+
646
+ # Include structured token and metadata if requested
647
+ if include_metadata:
648
+ # Build structured token object
649
+ issued_at_dt = datetime.fromtimestamp(
650
+ self.token_expires_at - self.token_data.get("expires_in", 3600),
651
+ tz=timezone.utc,
652
+ )
653
+
654
+ token = {
655
+ "access_token": self.token_data.get("access_token", ""),
656
+ "token_type": token_type,
657
+ "expires_in": current_expires_in,
658
+ "expires_at": token_expires_at_iso,
659
+ "issued_at": issued_at_dt.isoformat(),
660
+ "scope": self.token_data.get("scope", ""),
661
+ "is_valid": current_expires_in > 0,
662
+ "has_refresh_token": bool(self.token_data.get("refresh_token")),
663
+ "headers": headers, # Include ready-to-use headers
664
+ }
665
+
666
+ # Add refresh token hint if present (but not the actual value for security)
667
+ refresh_token = self.token_data.get("refresh_token")
668
+ if refresh_token:
669
+ token["refresh_token_hint"] = (
670
+ f"...{refresh_token[-4:]}" if len(refresh_token) > 4 else "****"
671
+ )
672
+
673
+ result["token"] = token
674
+
675
+ # Add health and metadata
676
+ health = self._calculate_token_health(current_expires_in)
677
+
678
+ metadata = {
679
+ "health": health,
680
+ "grant_type": kwargs.get("grant_type", "client_credentials"),
681
+ "token_endpoint": kwargs.get("token_url", ""),
682
+ "scopes_requested": (
683
+ kwargs.get("scope", "").split() if kwargs.get("scope") else []
684
+ ),
685
+ "scopes_granted": (
686
+ self.token_data.get("scope", "").split()
687
+ if self.token_data.get("scope")
688
+ else []
689
+ ),
690
+ "token_size_bytes": len(self.token_data.get("access_token", "")),
691
+ "response_fields": (
692
+ list(self.token_data.keys()) if self.token_data else []
693
+ ),
694
+ }
695
+
696
+ # Add timing information
697
+ if self._last_token_request_duration is not None:
698
+ metadata["last_request_duration_ms"] = self._last_token_request_duration
699
+
700
+ result["metadata"] = metadata
701
+
702
+ return result
703
+
423
704
 
424
705
  @register_node()
425
706
  class APIKeyNode(Node):
kailash/nodes/api/rest.py CHANGED
@@ -954,6 +954,157 @@ class RESTClientNode(Node):
954
954
 
955
955
  return links if links else None
956
956
 
957
+ async def async_run(self, **kwargs) -> dict[str, Any]:
958
+ """Execute a REST API request asynchronously.
959
+
960
+ This method provides true async implementation for REST API calls,
961
+ offering 3-5x performance improvement for I/O-heavy workflows.
962
+
963
+ Args:
964
+ Same as run() method
965
+
966
+ Returns:
967
+ Same as run() method
968
+
969
+ Raises:
970
+ NodeValidationError: If required parameters are missing or invalid
971
+ NodeExecutionError: If the request fails or returns an error status
972
+ """
973
+ # Use AsyncHTTPRequestNode for true async performance
974
+ if not hasattr(self, "_async_http_node"):
975
+ # Create async HTTP node instance lazily
976
+ from kailash.nodes.api.http import AsyncHTTPRequestNode
977
+
978
+ self._async_http_node = AsyncHTTPRequestNode()
979
+
980
+ # Extract REST-specific parameters
981
+ base_url = kwargs.get("base_url")
982
+ resource = kwargs.get("resource", "")
983
+ method = kwargs.get("method", "GET").upper()
984
+ path_params = kwargs.get("path_params", {})
985
+ query_params = kwargs.get("query_params", {})
986
+ headers = kwargs.get("headers", {})
987
+
988
+ # Build full URL using same logic as sync version
989
+ full_url = self._build_url(
990
+ base_url, resource, path_params, kwargs.get("version")
991
+ )
992
+
993
+ # Set default headers (same as sync version)
994
+ if (
995
+ method in ("POST", "PUT", "PATCH")
996
+ and kwargs.get("data")
997
+ and "Content-Type" not in headers
998
+ ):
999
+ headers["Content-Type"] = "application/json"
1000
+ if "Accept" not in headers:
1001
+ headers["Accept"] = "application/json"
1002
+
1003
+ # Execute async HTTP request
1004
+ http_result = await self._async_http_node.async_run(
1005
+ url=full_url,
1006
+ method=method,
1007
+ headers=headers,
1008
+ params=query_params,
1009
+ json_data=(
1010
+ kwargs.get("data") if isinstance(kwargs.get("data"), dict) else None
1011
+ ),
1012
+ data=(
1013
+ kwargs.get("data") if not isinstance(kwargs.get("data"), dict) else None
1014
+ ),
1015
+ response_format="json",
1016
+ timeout=kwargs.get("timeout", 30),
1017
+ verify_ssl=kwargs.get("verify_ssl", True),
1018
+ )
1019
+
1020
+ # Process response (simplified version for async)
1021
+ result = {
1022
+ "data": http_result.get("content"),
1023
+ "status_code": http_result.get("status_code"),
1024
+ "success": http_result.get("success", False),
1025
+ "metadata": {
1026
+ "url": full_url,
1027
+ "method": method,
1028
+ "headers": http_result.get("headers", {}),
1029
+ },
1030
+ }
1031
+
1032
+ # Handle pagination if requested (async version)
1033
+ if kwargs.get("paginate", False) and result.get("success", False):
1034
+ result = await self._handle_async_pagination(result, kwargs)
1035
+
1036
+ return result
1037
+
1038
+ async def _handle_async_pagination(
1039
+ self, initial_result: dict, kwargs: dict
1040
+ ) -> dict:
1041
+ """Handle pagination asynchronously for better performance.
1042
+
1043
+ Args:
1044
+ initial_result: The first page response
1045
+ kwargs: Original request parameters
1046
+
1047
+ Returns:
1048
+ Combined results from all pages
1049
+ """
1050
+ all_data = initial_result.get("data", [])
1051
+ pagination_config = kwargs.get("pagination_params", {})
1052
+ max_pages = pagination_config.get("max_pages", 10)
1053
+ page_count = 1
1054
+
1055
+ current_result = initial_result
1056
+
1057
+ while page_count < max_pages:
1058
+ # Check for next page link in metadata
1059
+ metadata = current_result.get("metadata", {})
1060
+ pagination = metadata.get("pagination", {})
1061
+ links = metadata.get("links", {})
1062
+
1063
+ next_url = links.get("next") or pagination.get("next_url")
1064
+ if not next_url:
1065
+ break
1066
+
1067
+ try:
1068
+ # Make async request for next page
1069
+ http_result = await self._async_http_node.async_run(
1070
+ url=next_url,
1071
+ method="GET",
1072
+ headers=kwargs.get("headers", {}),
1073
+ timeout=kwargs.get("timeout", 30),
1074
+ verify_ssl=kwargs.get("verify_ssl", True),
1075
+ )
1076
+
1077
+ current_result = {
1078
+ "data": http_result.get("content"),
1079
+ "status_code": http_result.get("status_code"),
1080
+ "success": http_result.get("success", False),
1081
+ "metadata": {
1082
+ "url": next_url,
1083
+ "method": "GET",
1084
+ "headers": http_result.get("headers", {}),
1085
+ },
1086
+ }
1087
+
1088
+ if current_result.get("success", False):
1089
+ page_data = current_result.get("data", [])
1090
+ if isinstance(page_data, list):
1091
+ all_data.extend(page_data)
1092
+ page_count += 1
1093
+ else:
1094
+ break
1095
+
1096
+ except Exception:
1097
+ # Stop pagination on error
1098
+ break
1099
+
1100
+ # Update result with combined data
1101
+ result = initial_result.copy()
1102
+ result["data"] = all_data
1103
+ if "metadata" in result:
1104
+ result["metadata"]["total_pages_fetched"] = page_count
1105
+
1106
+ return result
1107
+
957
1108
 
958
1109
  @register_node()
959
1110
  class AsyncRESTClientNode(AsyncNode):
@@ -0,0 +1,17 @@
1
+ """Authentication and authorization nodes for the Kailash SDK."""
2
+
3
+ from .directory_integration import DirectoryIntegrationNode
4
+ from .enterprise_auth_provider import EnterpriseAuthProviderNode
5
+ from .mfa import MultiFactorAuthNode
6
+ from .risk_assessment import RiskAssessmentNode
7
+ from .session_management import SessionManagementNode
8
+ from .sso import SSOAuthenticationNode
9
+
10
+ __all__ = [
11
+ "MultiFactorAuthNode",
12
+ "SessionManagementNode",
13
+ "SSOAuthenticationNode",
14
+ "DirectoryIntegrationNode",
15
+ "EnterpriseAuthProviderNode",
16
+ "RiskAssessmentNode",
17
+ ]