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.
- kailash/__init__.py +33 -1
- kailash/access_control/__init__.py +129 -0
- kailash/access_control/managers.py +461 -0
- kailash/access_control/rule_evaluators.py +467 -0
- kailash/access_control_abac.py +825 -0
- kailash/config/__init__.py +27 -0
- kailash/config/database_config.py +359 -0
- kailash/database/__init__.py +28 -0
- kailash/database/execution_pipeline.py +499 -0
- kailash/middleware/__init__.py +306 -0
- kailash/middleware/auth/__init__.py +33 -0
- kailash/middleware/auth/access_control.py +436 -0
- kailash/middleware/auth/auth_manager.py +422 -0
- kailash/middleware/auth/jwt_auth.py +477 -0
- kailash/middleware/auth/kailash_jwt_auth.py +616 -0
- kailash/middleware/communication/__init__.py +37 -0
- kailash/middleware/communication/ai_chat.py +989 -0
- kailash/middleware/communication/api_gateway.py +802 -0
- kailash/middleware/communication/events.py +470 -0
- kailash/middleware/communication/realtime.py +710 -0
- kailash/middleware/core/__init__.py +21 -0
- kailash/middleware/core/agent_ui.py +890 -0
- kailash/middleware/core/schema.py +643 -0
- kailash/middleware/core/workflows.py +396 -0
- kailash/middleware/database/__init__.py +63 -0
- kailash/middleware/database/base.py +113 -0
- kailash/middleware/database/base_models.py +525 -0
- kailash/middleware/database/enums.py +106 -0
- kailash/middleware/database/migrations.py +12 -0
- kailash/{api/database.py → middleware/database/models.py} +183 -291
- kailash/middleware/database/repositories.py +685 -0
- kailash/middleware/database/session_manager.py +19 -0
- kailash/middleware/mcp/__init__.py +38 -0
- kailash/middleware/mcp/client_integration.py +585 -0
- kailash/middleware/mcp/enhanced_server.py +576 -0
- kailash/nodes/__init__.py +27 -3
- kailash/nodes/admin/__init__.py +42 -0
- kailash/nodes/admin/audit_log.py +794 -0
- kailash/nodes/admin/permission_check.py +864 -0
- kailash/nodes/admin/role_management.py +823 -0
- kailash/nodes/admin/security_event.py +1523 -0
- kailash/nodes/admin/user_management.py +944 -0
- kailash/nodes/ai/a2a.py +24 -7
- kailash/nodes/ai/ai_providers.py +248 -40
- kailash/nodes/ai/embedding_generator.py +11 -11
- kailash/nodes/ai/intelligent_agent_orchestrator.py +99 -11
- kailash/nodes/ai/llm_agent.py +436 -5
- kailash/nodes/ai/self_organizing.py +85 -10
- kailash/nodes/ai/vision_utils.py +148 -0
- kailash/nodes/alerts/__init__.py +26 -0
- kailash/nodes/alerts/base.py +234 -0
- kailash/nodes/alerts/discord.py +499 -0
- kailash/nodes/api/auth.py +287 -6
- kailash/nodes/api/rest.py +151 -0
- kailash/nodes/auth/__init__.py +17 -0
- kailash/nodes/auth/directory_integration.py +1228 -0
- kailash/nodes/auth/enterprise_auth_provider.py +1328 -0
- kailash/nodes/auth/mfa.py +2338 -0
- kailash/nodes/auth/risk_assessment.py +872 -0
- kailash/nodes/auth/session_management.py +1093 -0
- kailash/nodes/auth/sso.py +1040 -0
- kailash/nodes/base.py +344 -13
- kailash/nodes/base_cycle_aware.py +4 -2
- kailash/nodes/base_with_acl.py +1 -1
- kailash/nodes/code/python.py +283 -10
- kailash/nodes/compliance/__init__.py +9 -0
- kailash/nodes/compliance/data_retention.py +1888 -0
- kailash/nodes/compliance/gdpr.py +2004 -0
- kailash/nodes/data/__init__.py +22 -2
- kailash/nodes/data/async_connection.py +469 -0
- kailash/nodes/data/async_sql.py +757 -0
- kailash/nodes/data/async_vector.py +598 -0
- kailash/nodes/data/readers.py +767 -0
- kailash/nodes/data/retrieval.py +360 -1
- kailash/nodes/data/sharepoint_graph.py +397 -21
- kailash/nodes/data/sql.py +94 -5
- kailash/nodes/data/streaming.py +68 -8
- kailash/nodes/data/vector_db.py +54 -4
- kailash/nodes/enterprise/__init__.py +13 -0
- kailash/nodes/enterprise/batch_processor.py +741 -0
- kailash/nodes/enterprise/data_lineage.py +497 -0
- kailash/nodes/logic/convergence.py +31 -9
- kailash/nodes/logic/operations.py +14 -3
- kailash/nodes/mixins/__init__.py +8 -0
- kailash/nodes/mixins/event_emitter.py +201 -0
- kailash/nodes/mixins/mcp.py +9 -4
- kailash/nodes/mixins/security.py +165 -0
- kailash/nodes/monitoring/__init__.py +7 -0
- kailash/nodes/monitoring/performance_benchmark.py +2497 -0
- kailash/nodes/rag/__init__.py +284 -0
- kailash/nodes/rag/advanced.py +1615 -0
- kailash/nodes/rag/agentic.py +773 -0
- kailash/nodes/rag/conversational.py +999 -0
- kailash/nodes/rag/evaluation.py +875 -0
- kailash/nodes/rag/federated.py +1188 -0
- kailash/nodes/rag/graph.py +721 -0
- kailash/nodes/rag/multimodal.py +671 -0
- kailash/nodes/rag/optimized.py +933 -0
- kailash/nodes/rag/privacy.py +1059 -0
- kailash/nodes/rag/query_processing.py +1335 -0
- kailash/nodes/rag/realtime.py +764 -0
- kailash/nodes/rag/registry.py +547 -0
- kailash/nodes/rag/router.py +837 -0
- kailash/nodes/rag/similarity.py +1854 -0
- kailash/nodes/rag/strategies.py +566 -0
- kailash/nodes/rag/workflows.py +575 -0
- kailash/nodes/security/__init__.py +19 -0
- kailash/nodes/security/abac_evaluator.py +1411 -0
- kailash/nodes/security/audit_log.py +103 -0
- kailash/nodes/security/behavior_analysis.py +1893 -0
- kailash/nodes/security/credential_manager.py +401 -0
- kailash/nodes/security/rotating_credentials.py +760 -0
- kailash/nodes/security/security_event.py +133 -0
- kailash/nodes/security/threat_detection.py +1103 -0
- kailash/nodes/testing/__init__.py +9 -0
- kailash/nodes/testing/credential_testing.py +499 -0
- kailash/nodes/transform/__init__.py +10 -2
- kailash/nodes/transform/chunkers.py +592 -1
- kailash/nodes/transform/processors.py +484 -14
- kailash/nodes/validation.py +321 -0
- kailash/runtime/access_controlled.py +1 -1
- kailash/runtime/async_local.py +41 -7
- kailash/runtime/docker.py +1 -1
- kailash/runtime/local.py +474 -55
- kailash/runtime/parallel.py +1 -1
- kailash/runtime/parallel_cyclic.py +1 -1
- kailash/runtime/testing.py +210 -2
- kailash/security.py +1 -1
- kailash/utils/migrations/__init__.py +25 -0
- kailash/utils/migrations/generator.py +433 -0
- kailash/utils/migrations/models.py +231 -0
- kailash/utils/migrations/runner.py +489 -0
- kailash/utils/secure_logging.py +342 -0
- kailash/workflow/__init__.py +16 -0
- kailash/workflow/cyclic_runner.py +3 -4
- kailash/workflow/graph.py +70 -2
- kailash/workflow/resilience.py +249 -0
- kailash/workflow/templates.py +726 -0
- {kailash-0.3.2.dist-info → kailash-0.4.1.dist-info}/METADATA +256 -20
- kailash-0.4.1.dist-info/RECORD +227 -0
- kailash/api/__init__.py +0 -17
- kailash/api/__main__.py +0 -6
- kailash/api/studio_secure.py +0 -893
- kailash/mcp/__main__.py +0 -13
- kailash/mcp/server_new.py +0 -336
- kailash/mcp/servers/__init__.py +0 -12
- kailash-0.3.2.dist-info/RECORD +0 -136
- {kailash-0.3.2.dist-info → kailash-0.4.1.dist-info}/WHEEL +0 -0
- {kailash-0.3.2.dist-info → kailash-0.4.1.dist-info}/entry_points.txt +0 -0
- {kailash-0.3.2.dist-info → kailash-0.4.1.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
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 (
|
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
|
-
|
613
|
+
access_token = self.token_data.get("access_token", "")
|
614
|
+
token_type = self.token_data.get("token_type", "Bearer")
|
415
615
|
|
416
|
-
|
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
|
+
]
|