kailash 0.1.1__py3-none-any.whl → 0.1.3__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/api/__init__.py +7 -0
- kailash/api/workflow_api.py +383 -0
- kailash/nodes/__init__.py +2 -1
- kailash/nodes/ai/__init__.py +26 -0
- kailash/nodes/ai/ai_providers.py +1272 -0
- kailash/nodes/ai/embedding_generator.py +853 -0
- kailash/nodes/ai/llm_agent.py +1166 -0
- kailash/nodes/api/auth.py +3 -3
- kailash/nodes/api/graphql.py +2 -2
- kailash/nodes/api/http.py +391 -48
- kailash/nodes/api/rate_limiting.py +2 -2
- kailash/nodes/api/rest.py +465 -57
- kailash/nodes/base.py +71 -12
- kailash/nodes/code/python.py +2 -1
- kailash/nodes/data/__init__.py +7 -0
- kailash/nodes/data/readers.py +28 -26
- kailash/nodes/data/retrieval.py +178 -0
- kailash/nodes/data/sharepoint_graph.py +7 -7
- kailash/nodes/data/sources.py +65 -0
- kailash/nodes/data/sql.py +7 -5
- kailash/nodes/data/vector_db.py +2 -2
- kailash/nodes/data/writers.py +6 -3
- kailash/nodes/logic/__init__.py +2 -1
- kailash/nodes/logic/operations.py +2 -1
- kailash/nodes/logic/workflow.py +439 -0
- kailash/nodes/mcp/__init__.py +11 -0
- kailash/nodes/mcp/client.py +558 -0
- kailash/nodes/mcp/resource.py +682 -0
- kailash/nodes/mcp/server.py +577 -0
- kailash/nodes/transform/__init__.py +16 -1
- kailash/nodes/transform/chunkers.py +78 -0
- kailash/nodes/transform/formatters.py +96 -0
- kailash/nodes/transform/processors.py +5 -3
- kailash/runtime/docker.py +8 -6
- kailash/sdk_exceptions.py +24 -10
- kailash/tracking/metrics_collector.py +2 -1
- kailash/tracking/models.py +0 -20
- kailash/tracking/storage/database.py +4 -4
- kailash/tracking/storage/filesystem.py +0 -1
- kailash/utils/templates.py +6 -6
- kailash/visualization/performance.py +7 -7
- kailash/visualization/reports.py +1 -1
- kailash/workflow/graph.py +4 -4
- kailash/workflow/mock_registry.py +1 -1
- {kailash-0.1.1.dist-info → kailash-0.1.3.dist-info}/METADATA +441 -47
- kailash-0.1.3.dist-info/RECORD +83 -0
- kailash-0.1.1.dist-info/RECORD +0 -69
- {kailash-0.1.1.dist-info → kailash-0.1.3.dist-info}/WHEEL +0 -0
- {kailash-0.1.1.dist-info → kailash-0.1.3.dist-info}/entry_points.txt +0 -0
- {kailash-0.1.1.dist-info → kailash-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.1.1.dist-info → kailash-0.1.3.dist-info}/top_level.txt +0 -0
kailash/nodes/api/auth.py
CHANGED
@@ -21,7 +21,7 @@ from kailash.nodes.base import Node, NodeParameter, register_node
|
|
21
21
|
from kailash.sdk_exceptions import NodeExecutionError, NodeValidationError
|
22
22
|
|
23
23
|
|
24
|
-
@register_node(
|
24
|
+
@register_node()
|
25
25
|
class BasicAuthNode(Node):
|
26
26
|
"""Node for adding Basic Authentication to API requests.
|
27
27
|
|
@@ -116,7 +116,7 @@ class BasicAuthNode(Node):
|
|
116
116
|
return {"headers": headers, "auth_type": "basic"}
|
117
117
|
|
118
118
|
|
119
|
-
@register_node(
|
119
|
+
@register_node()
|
120
120
|
class OAuth2Node(Node):
|
121
121
|
"""Node for handling OAuth 2.0 authentication flows.
|
122
122
|
|
@@ -421,7 +421,7 @@ class OAuth2Node(Node):
|
|
421
421
|
}
|
422
422
|
|
423
423
|
|
424
|
-
@register_node(
|
424
|
+
@register_node()
|
425
425
|
class APIKeyNode(Node):
|
426
426
|
"""Node for API key authentication.
|
427
427
|
|
kailash/nodes/api/graphql.py
CHANGED
@@ -18,7 +18,7 @@ from kailash.nodes.base_async import AsyncNode
|
|
18
18
|
from kailash.sdk_exceptions import NodeExecutionError, NodeValidationError
|
19
19
|
|
20
20
|
|
21
|
-
@register_node(
|
21
|
+
@register_node()
|
22
22
|
class GraphQLClientNode(Node):
|
23
23
|
"""Node for interacting with GraphQL APIs.
|
24
24
|
|
@@ -331,7 +331,7 @@ class GraphQLClientNode(Node):
|
|
331
331
|
}
|
332
332
|
|
333
333
|
|
334
|
-
@register_node(
|
334
|
+
@register_node()
|
335
335
|
class AsyncGraphQLClientNode(AsyncNode):
|
336
336
|
"""Asynchronous node for interacting with GraphQL APIs.
|
337
337
|
|
kailash/nodes/api/http.py
CHANGED
@@ -1,16 +1,12 @@
|
|
1
|
-
"""HTTP client nodes
|
1
|
+
"""Enhanced HTTP client nodes with authentication and advanced features.
|
2
2
|
|
3
|
-
This module provides
|
4
|
-
|
5
|
-
execution modes.
|
6
|
-
|
7
|
-
Key Components:
|
8
|
-
- HTTPRequestNode: Synchronous HTTP client node
|
9
|
-
- AsyncHTTPRequestNode: Asynchronous HTTP client node
|
10
|
-
- Authentication helpers and utilities
|
3
|
+
This module provides an enhanced version of HTTPRequestNode that incorporates
|
4
|
+
the best features from both the original HTTPRequestNode and HTTPClientNode.
|
11
5
|
"""
|
12
6
|
|
13
7
|
import asyncio
|
8
|
+
import base64
|
9
|
+
import time
|
14
10
|
from enum import Enum
|
15
11
|
from typing import Any, Dict, Optional
|
16
12
|
|
@@ -59,31 +55,35 @@ class HTTPResponse(BaseModel):
|
|
59
55
|
url: str
|
60
56
|
|
61
57
|
|
62
|
-
@register_node(
|
58
|
+
@register_node()
|
63
59
|
class HTTPRequestNode(Node):
|
64
|
-
"""
|
60
|
+
"""Enhanced node for making HTTP requests to external APIs.
|
65
61
|
|
66
62
|
This node provides a flexible interface for making HTTP requests with support for:
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
63
|
+
* All common HTTP methods (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS)
|
64
|
+
* Multiple authentication methods (Bearer, Basic, API Key, OAuth2)
|
65
|
+
* JSON, form, and multipart request bodies
|
66
|
+
* Custom headers and query parameters
|
67
|
+
* Response parsing (JSON, text, binary)
|
68
|
+
* Error handling and retries with recovery suggestions
|
69
|
+
* Rate limiting support
|
70
|
+
* Request/response logging
|
72
71
|
|
73
72
|
Design Purpose:
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
73
|
+
* Enable workflow integration with external HTTP APIs
|
74
|
+
* Provide a consistent interface for HTTP operations
|
75
|
+
* Support common authentication patterns
|
76
|
+
* Handle response parsing and error handling
|
77
|
+
* Offer enterprise-grade features like rate limiting
|
78
78
|
|
79
79
|
Upstream Usage:
|
80
|
-
|
81
|
-
|
80
|
+
* Workflow: Creates and configures node for API integration
|
81
|
+
* Specialized API nodes: May extend this node for specific APIs
|
82
82
|
|
83
83
|
Downstream Consumers:
|
84
|
-
|
85
|
-
|
86
|
-
|
84
|
+
* Data processing nodes: Consume API response data
|
85
|
+
* Decision nodes: Route workflow based on API responses
|
86
|
+
* Custom nodes: Process API-specific data formats
|
87
87
|
"""
|
88
88
|
|
89
89
|
def __init__(self, **kwargs):
|
@@ -101,6 +101,13 @@ class HTTPRequestNode(Node):
|
|
101
101
|
verify_ssl (bool, optional): Whether to verify SSL certificates
|
102
102
|
retry_count (int, optional): Number of times to retry failed requests
|
103
103
|
retry_backoff (float, optional): Backoff factor for retries
|
104
|
+
auth_type (str, optional): Authentication type (bearer, basic, api_key, oauth2)
|
105
|
+
auth_token (str, optional): Authentication token/key
|
106
|
+
auth_username (str, optional): Username for basic auth
|
107
|
+
auth_password (str, optional): Password for basic auth
|
108
|
+
api_key_header (str, optional): Header name for API key auth
|
109
|
+
rate_limit_delay (float, optional): Delay between requests for rate limiting
|
110
|
+
log_requests (bool, optional): Whether to log request/response details
|
104
111
|
**kwargs: Additional parameters passed to base Node
|
105
112
|
"""
|
106
113
|
super().__init__(**kwargs)
|
@@ -116,7 +123,7 @@ class HTTPRequestNode(Node):
|
|
116
123
|
"url": NodeParameter(
|
117
124
|
name="url",
|
118
125
|
type=str,
|
119
|
-
required=
|
126
|
+
required=False,
|
120
127
|
description="URL to send the request to",
|
121
128
|
),
|
122
129
|
"method": NodeParameter(
|
@@ -189,6 +196,55 @@ class HTTPRequestNode(Node):
|
|
189
196
|
default=0.5,
|
190
197
|
description="Backoff factor for retries",
|
191
198
|
),
|
199
|
+
"auth_type": NodeParameter(
|
200
|
+
name="auth_type",
|
201
|
+
type=str,
|
202
|
+
required=False,
|
203
|
+
default=None,
|
204
|
+
description="Authentication type: bearer, basic, api_key, oauth2",
|
205
|
+
),
|
206
|
+
"auth_token": NodeParameter(
|
207
|
+
name="auth_token",
|
208
|
+
type=str,
|
209
|
+
required=False,
|
210
|
+
default=None,
|
211
|
+
description="Authentication token/key for bearer, api_key, or oauth2",
|
212
|
+
),
|
213
|
+
"auth_username": NodeParameter(
|
214
|
+
name="auth_username",
|
215
|
+
type=str,
|
216
|
+
required=False,
|
217
|
+
default=None,
|
218
|
+
description="Username for basic authentication",
|
219
|
+
),
|
220
|
+
"auth_password": NodeParameter(
|
221
|
+
name="auth_password",
|
222
|
+
type=str,
|
223
|
+
required=False,
|
224
|
+
default=None,
|
225
|
+
description="Password for basic authentication",
|
226
|
+
),
|
227
|
+
"api_key_header": NodeParameter(
|
228
|
+
name="api_key_header",
|
229
|
+
type=str,
|
230
|
+
required=False,
|
231
|
+
default="X-API-Key",
|
232
|
+
description="Header name for API key authentication",
|
233
|
+
),
|
234
|
+
"rate_limit_delay": NodeParameter(
|
235
|
+
name="rate_limit_delay",
|
236
|
+
type=float,
|
237
|
+
required=False,
|
238
|
+
default=0,
|
239
|
+
description="Delay between requests to respect rate limits (seconds)",
|
240
|
+
),
|
241
|
+
"log_requests": NodeParameter(
|
242
|
+
name="log_requests",
|
243
|
+
type=bool,
|
244
|
+
required=False,
|
245
|
+
default=False,
|
246
|
+
description="Log request and response details for debugging",
|
247
|
+
),
|
192
248
|
}
|
193
249
|
|
194
250
|
def get_output_schema(self) -> Dict[str, NodeParameter]:
|
@@ -218,6 +274,49 @@ class HTTPRequestNode(Node):
|
|
218
274
|
),
|
219
275
|
}
|
220
276
|
|
277
|
+
def _apply_authentication(
|
278
|
+
self,
|
279
|
+
headers: dict,
|
280
|
+
auth_type: Optional[str],
|
281
|
+
auth_token: Optional[str],
|
282
|
+
auth_username: Optional[str],
|
283
|
+
auth_password: Optional[str],
|
284
|
+
api_key_header: str,
|
285
|
+
) -> dict:
|
286
|
+
"""Apply authentication to request headers.
|
287
|
+
|
288
|
+
Args:
|
289
|
+
headers: Existing headers dictionary
|
290
|
+
auth_type: Type of authentication (bearer, basic, api_key, oauth2)
|
291
|
+
auth_token: Token for bearer/api_key/oauth2 authentication
|
292
|
+
auth_username: Username for basic authentication
|
293
|
+
auth_password: Password for basic authentication
|
294
|
+
api_key_header: Header name for API key authentication
|
295
|
+
|
296
|
+
Returns:
|
297
|
+
Updated headers dictionary with authentication
|
298
|
+
"""
|
299
|
+
if not auth_type:
|
300
|
+
return headers
|
301
|
+
|
302
|
+
auth_headers = headers.copy()
|
303
|
+
|
304
|
+
if auth_type.lower() == "bearer" and auth_token:
|
305
|
+
auth_headers["Authorization"] = f"Bearer {auth_token}"
|
306
|
+
|
307
|
+
elif auth_type.lower() == "basic" and auth_username and auth_password:
|
308
|
+
credentials = f"{auth_username}:{auth_password}"
|
309
|
+
encoded = base64.b64encode(credentials.encode()).decode()
|
310
|
+
auth_headers["Authorization"] = f"Basic {encoded}"
|
311
|
+
|
312
|
+
elif auth_type.lower() == "api_key" and auth_token:
|
313
|
+
auth_headers[api_key_header] = auth_token
|
314
|
+
|
315
|
+
elif auth_type.lower() == "oauth2" and auth_token:
|
316
|
+
auth_headers["Authorization"] = f"Bearer {auth_token}"
|
317
|
+
|
318
|
+
return auth_headers
|
319
|
+
|
221
320
|
def run(self, **kwargs) -> Dict[str, Any]:
|
222
321
|
"""Execute an HTTP request.
|
223
322
|
|
@@ -233,6 +332,13 @@ class HTTPRequestNode(Node):
|
|
233
332
|
verify_ssl (bool, optional): Whether to verify SSL certificates
|
234
333
|
retry_count (int, optional): Number of times to retry failed requests
|
235
334
|
retry_backoff (float, optional): Backoff factor for retries
|
335
|
+
auth_type (str, optional): Authentication type
|
336
|
+
auth_token (str, optional): Authentication token
|
337
|
+
auth_username (str, optional): Username for basic auth
|
338
|
+
auth_password (str, optional): Password for basic auth
|
339
|
+
api_key_header (str, optional): Header name for API key
|
340
|
+
rate_limit_delay (float, optional): Rate limit delay
|
341
|
+
log_requests (bool, optional): Log request/response details
|
236
342
|
|
237
343
|
Returns:
|
238
344
|
Dictionary containing:
|
@@ -244,6 +350,8 @@ class HTTPRequestNode(Node):
|
|
244
350
|
NodeExecutionError: If the request fails or returns an error status
|
245
351
|
"""
|
246
352
|
url = kwargs.get("url")
|
353
|
+
if not url:
|
354
|
+
raise NodeValidationError("URL parameter is required")
|
247
355
|
method = kwargs.get("method", "GET").upper()
|
248
356
|
headers = kwargs.get("headers", {})
|
249
357
|
params = kwargs.get("params", {})
|
@@ -254,6 +362,24 @@ class HTTPRequestNode(Node):
|
|
254
362
|
verify_ssl = kwargs.get("verify_ssl", True)
|
255
363
|
retry_count = kwargs.get("retry_count", 0)
|
256
364
|
retry_backoff = kwargs.get("retry_backoff", 0.5)
|
365
|
+
auth_type = kwargs.get("auth_type")
|
366
|
+
auth_token = kwargs.get("auth_token")
|
367
|
+
auth_username = kwargs.get("auth_username")
|
368
|
+
auth_password = kwargs.get("auth_password")
|
369
|
+
api_key_header = kwargs.get("api_key_header", "X-API-Key")
|
370
|
+
rate_limit_delay = kwargs.get("rate_limit_delay", 0)
|
371
|
+
log_requests = kwargs.get("log_requests", False)
|
372
|
+
|
373
|
+
# Apply authentication to headers
|
374
|
+
if auth_type:
|
375
|
+
headers = self._apply_authentication(
|
376
|
+
headers,
|
377
|
+
auth_type,
|
378
|
+
auth_token,
|
379
|
+
auth_username,
|
380
|
+
auth_password,
|
381
|
+
api_key_header,
|
382
|
+
)
|
257
383
|
|
258
384
|
# Validate method
|
259
385
|
try:
|
@@ -273,6 +399,10 @@ class HTTPRequestNode(Node):
|
|
273
399
|
f"Supported formats: {', '.join([f.value for f in ResponseFormat])}"
|
274
400
|
)
|
275
401
|
|
402
|
+
# Apply rate limit delay if configured
|
403
|
+
if rate_limit_delay > 0:
|
404
|
+
time.sleep(rate_limit_delay)
|
405
|
+
|
276
406
|
# Prepare request kwargs
|
277
407
|
request_kwargs = {
|
278
408
|
"url": url,
|
@@ -289,10 +419,15 @@ class HTTPRequestNode(Node):
|
|
289
419
|
request_kwargs["data"] = data
|
290
420
|
|
291
421
|
# Execute request with retries
|
292
|
-
|
422
|
+
if log_requests:
|
423
|
+
self.logger.info(f"Request: {method} {url}")
|
424
|
+
self.logger.info(f"Headers: {headers}")
|
425
|
+
if data or json_data:
|
426
|
+
self.logger.info(f"Body: {json_data or data}")
|
427
|
+
else:
|
428
|
+
self.logger.info(f"Making {method} request to {url}")
|
293
429
|
|
294
430
|
response = None
|
295
|
-
last_error = None
|
296
431
|
|
297
432
|
for attempt in range(retry_count + 1):
|
298
433
|
if attempt > 0:
|
@@ -300,29 +435,42 @@ class HTTPRequestNode(Node):
|
|
300
435
|
self.logger.info(
|
301
436
|
f"Retry attempt {attempt}/{retry_count} after {wait_time:.2f}s"
|
302
437
|
)
|
303
|
-
import time
|
304
|
-
|
305
438
|
time.sleep(wait_time)
|
306
439
|
|
307
440
|
try:
|
308
|
-
import time
|
309
|
-
|
310
441
|
start_time = time.time()
|
311
442
|
response = self.session.request(method=method.value, **request_kwargs)
|
312
443
|
response_time = (time.time() - start_time) * 1000 # Convert to ms
|
313
444
|
|
445
|
+
# Log response if enabled
|
446
|
+
if log_requests:
|
447
|
+
self.logger.info(f"Response: {response.status_code}")
|
448
|
+
self.logger.info(f"Headers: {dict(response.headers)}")
|
449
|
+
self.logger.info(f"Body: {response.text[:500]}...")
|
450
|
+
|
314
451
|
# Success, break the retry loop
|
315
452
|
break
|
316
453
|
|
317
454
|
except requests.RequestException as e:
|
318
|
-
last_error = e
|
319
455
|
self.logger.warning(f"Request failed: {str(e)}")
|
320
456
|
|
321
457
|
# Last attempt, no more retries
|
322
458
|
if attempt == retry_count:
|
323
|
-
|
324
|
-
|
325
|
-
|
459
|
+
# Enhanced error response with recovery suggestions
|
460
|
+
return {
|
461
|
+
"response": None,
|
462
|
+
"status_code": None,
|
463
|
+
"success": False,
|
464
|
+
"error": str(e),
|
465
|
+
"error_type": type(e).__name__,
|
466
|
+
"recovery_suggestions": [
|
467
|
+
"Check network connectivity",
|
468
|
+
"Verify URL is correct and accessible",
|
469
|
+
"Check authentication credentials",
|
470
|
+
"Increase timeout or retry settings",
|
471
|
+
"Check API rate limits",
|
472
|
+
],
|
473
|
+
}
|
326
474
|
|
327
475
|
# Parse response based on format
|
328
476
|
content_type = response.headers.get("Content-Type", "")
|
@@ -363,16 +511,70 @@ class HTTPRequestNode(Node):
|
|
363
511
|
# Return results
|
364
512
|
success = 200 <= response.status_code < 300
|
365
513
|
|
366
|
-
|
514
|
+
# Add recovery suggestions for error responses
|
515
|
+
result = {
|
367
516
|
"response": http_response,
|
368
517
|
"status_code": response.status_code,
|
369
518
|
"success": success,
|
370
519
|
}
|
371
520
|
|
521
|
+
if not success:
|
522
|
+
result["recovery_suggestions"] = self._get_recovery_suggestions(
|
523
|
+
response.status_code
|
524
|
+
)
|
525
|
+
|
526
|
+
return result
|
527
|
+
|
528
|
+
def _get_recovery_suggestions(self, status_code: int) -> list:
|
529
|
+
"""Get recovery suggestions based on status code.
|
372
530
|
|
373
|
-
|
531
|
+
Args:
|
532
|
+
status_code: HTTP status code
|
533
|
+
|
534
|
+
Returns:
|
535
|
+
List of recovery suggestions
|
536
|
+
"""
|
537
|
+
if status_code == 401:
|
538
|
+
return [
|
539
|
+
"Check authentication credentials",
|
540
|
+
"Verify API key or token is valid",
|
541
|
+
"Ensure authentication method matches API requirements",
|
542
|
+
]
|
543
|
+
elif status_code == 403:
|
544
|
+
return [
|
545
|
+
"Verify you have permission to access this resource",
|
546
|
+
"Check API key permissions/scopes",
|
547
|
+
"Ensure IP address is whitelisted if required",
|
548
|
+
]
|
549
|
+
elif status_code == 404:
|
550
|
+
return [
|
551
|
+
"Verify the URL path is correct",
|
552
|
+
"Check if resource ID exists",
|
553
|
+
"Ensure API version in URL is correct",
|
554
|
+
]
|
555
|
+
elif status_code == 429:
|
556
|
+
return [
|
557
|
+
"API rate limit exceeded - wait before retrying",
|
558
|
+
"Implement rate limiting in your requests",
|
559
|
+
"Check rate limit headers for reset time",
|
560
|
+
]
|
561
|
+
elif status_code >= 500:
|
562
|
+
return [
|
563
|
+
"Server error - retry after a delay",
|
564
|
+
"Check API service status page",
|
565
|
+
"Contact API support if issue persists",
|
566
|
+
]
|
567
|
+
else:
|
568
|
+
return [
|
569
|
+
"Check API documentation for this status code",
|
570
|
+
"Verify request format and parameters",
|
571
|
+
"Review response body for error details",
|
572
|
+
]
|
573
|
+
|
574
|
+
|
575
|
+
@register_node()
|
374
576
|
class AsyncHTTPRequestNode(AsyncNode):
|
375
|
-
"""Asynchronous node for making HTTP requests to external APIs.
|
577
|
+
"""Asynchronous enhanced node for making HTTP requests to external APIs.
|
376
578
|
|
377
579
|
This node provides the same functionality as HTTPRequestNode but uses
|
378
580
|
asynchronous I/O for better performance, especially for concurrent requests.
|
@@ -418,6 +620,49 @@ class AsyncHTTPRequestNode(AsyncNode):
|
|
418
620
|
# Same output schema as the synchronous version
|
419
621
|
return HTTPRequestNode().get_output_schema()
|
420
622
|
|
623
|
+
def _apply_authentication(
|
624
|
+
self,
|
625
|
+
headers: dict,
|
626
|
+
auth_type: Optional[str],
|
627
|
+
auth_token: Optional[str],
|
628
|
+
auth_username: Optional[str],
|
629
|
+
auth_password: Optional[str],
|
630
|
+
api_key_header: str,
|
631
|
+
) -> dict:
|
632
|
+
"""Apply authentication to request headers.
|
633
|
+
|
634
|
+
Args:
|
635
|
+
headers: Existing headers dictionary
|
636
|
+
auth_type: Type of authentication (bearer, basic, api_key, oauth2)
|
637
|
+
auth_token: Token for bearer/api_key/oauth2 authentication
|
638
|
+
auth_username: Username for basic authentication
|
639
|
+
auth_password: Password for basic authentication
|
640
|
+
api_key_header: Header name for API key authentication
|
641
|
+
|
642
|
+
Returns:
|
643
|
+
Updated headers dictionary with authentication
|
644
|
+
"""
|
645
|
+
if not auth_type:
|
646
|
+
return headers
|
647
|
+
|
648
|
+
auth_headers = headers.copy()
|
649
|
+
|
650
|
+
if auth_type.lower() == "bearer" and auth_token:
|
651
|
+
auth_headers["Authorization"] = f"Bearer {auth_token}"
|
652
|
+
|
653
|
+
elif auth_type.lower() == "basic" and auth_username and auth_password:
|
654
|
+
credentials = f"{auth_username}:{auth_password}"
|
655
|
+
encoded = base64.b64encode(credentials.encode()).decode()
|
656
|
+
auth_headers["Authorization"] = f"Basic {encoded}"
|
657
|
+
|
658
|
+
elif auth_type.lower() == "api_key" and auth_token:
|
659
|
+
auth_headers[api_key_header] = auth_token
|
660
|
+
|
661
|
+
elif auth_type.lower() == "oauth2" and auth_token:
|
662
|
+
auth_headers["Authorization"] = f"Bearer {auth_token}"
|
663
|
+
|
664
|
+
return auth_headers
|
665
|
+
|
421
666
|
def run(self, **kwargs) -> Dict[str, Any]:
|
422
667
|
"""Synchronous version of the request, for compatibility.
|
423
668
|
|
@@ -450,6 +695,8 @@ class AsyncHTTPRequestNode(AsyncNode):
|
|
450
695
|
NodeExecutionError: If the request fails or returns an error status
|
451
696
|
"""
|
452
697
|
url = kwargs.get("url")
|
698
|
+
if not url:
|
699
|
+
raise NodeValidationError("URL parameter is required")
|
453
700
|
method = kwargs.get("method", "GET").upper()
|
454
701
|
headers = kwargs.get("headers", {})
|
455
702
|
params = kwargs.get("params", {})
|
@@ -460,6 +707,24 @@ class AsyncHTTPRequestNode(AsyncNode):
|
|
460
707
|
verify_ssl = kwargs.get("verify_ssl", True)
|
461
708
|
retry_count = kwargs.get("retry_count", 0)
|
462
709
|
retry_backoff = kwargs.get("retry_backoff", 0.5)
|
710
|
+
auth_type = kwargs.get("auth_type")
|
711
|
+
auth_token = kwargs.get("auth_token")
|
712
|
+
auth_username = kwargs.get("auth_username")
|
713
|
+
auth_password = kwargs.get("auth_password")
|
714
|
+
api_key_header = kwargs.get("api_key_header", "X-API-Key")
|
715
|
+
rate_limit_delay = kwargs.get("rate_limit_delay", 0)
|
716
|
+
log_requests = kwargs.get("log_requests", False)
|
717
|
+
|
718
|
+
# Apply authentication to headers
|
719
|
+
if auth_type:
|
720
|
+
headers = self._apply_authentication(
|
721
|
+
headers,
|
722
|
+
auth_type,
|
723
|
+
auth_token,
|
724
|
+
auth_username,
|
725
|
+
auth_password,
|
726
|
+
api_key_header,
|
727
|
+
)
|
463
728
|
|
464
729
|
# Validate method
|
465
730
|
try:
|
@@ -479,6 +744,10 @@ class AsyncHTTPRequestNode(AsyncNode):
|
|
479
744
|
f"Supported formats: {', '.join([f.value for f in ResponseFormat])}"
|
480
745
|
)
|
481
746
|
|
747
|
+
# Apply rate limit delay if configured
|
748
|
+
if rate_limit_delay > 0:
|
749
|
+
await asyncio.sleep(rate_limit_delay)
|
750
|
+
|
482
751
|
# Create session if needed
|
483
752
|
if self._session is None:
|
484
753
|
self._session = aiohttp.ClientSession()
|
@@ -499,10 +768,15 @@ class AsyncHTTPRequestNode(AsyncNode):
|
|
499
768
|
request_kwargs["data"] = data
|
500
769
|
|
501
770
|
# Execute request with retries
|
502
|
-
|
771
|
+
if log_requests:
|
772
|
+
self.logger.info(f"Request: {method} {url}")
|
773
|
+
self.logger.info(f"Headers: {headers}")
|
774
|
+
if data or json_data:
|
775
|
+
self.logger.info(f"Body: {json_data or data}")
|
776
|
+
else:
|
777
|
+
self.logger.info(f"Making async {method} request to {url}")
|
503
778
|
|
504
779
|
response = None
|
505
|
-
last_error = None
|
506
780
|
|
507
781
|
for attempt in range(retry_count + 1):
|
508
782
|
if attempt > 0:
|
@@ -513,8 +787,6 @@ class AsyncHTTPRequestNode(AsyncNode):
|
|
513
787
|
await asyncio.sleep(wait_time)
|
514
788
|
|
515
789
|
try:
|
516
|
-
import time
|
517
|
-
|
518
790
|
start_time = time.time()
|
519
791
|
|
520
792
|
async with self._session.request(
|
@@ -525,6 +797,13 @@ class AsyncHTTPRequestNode(AsyncNode):
|
|
525
797
|
# Get content type
|
526
798
|
content_type = response.headers.get("Content-Type", "")
|
527
799
|
|
800
|
+
# Log response if enabled
|
801
|
+
if log_requests:
|
802
|
+
self.logger.info(f"Response: {response.status}")
|
803
|
+
self.logger.info(f"Headers: {dict(response.headers)}")
|
804
|
+
text_preview = await response.text()
|
805
|
+
self.logger.info(f"Body: {text_preview[:500]}...")
|
806
|
+
|
528
807
|
# Determine response format
|
529
808
|
actual_format = response_format
|
530
809
|
if actual_format == ResponseFormat.AUTO:
|
@@ -564,27 +843,91 @@ class AsyncHTTPRequestNode(AsyncNode):
|
|
564
843
|
# Return results
|
565
844
|
success = 200 <= response.status < 300
|
566
845
|
|
567
|
-
|
846
|
+
result = {
|
568
847
|
"response": http_response,
|
569
848
|
"status_code": response.status,
|
570
849
|
"success": success,
|
571
850
|
}
|
572
851
|
|
852
|
+
if not success:
|
853
|
+
result["recovery_suggestions"] = self._get_recovery_suggestions(
|
854
|
+
response.status
|
855
|
+
)
|
856
|
+
|
857
|
+
return result
|
858
|
+
|
573
859
|
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
|
574
|
-
last_error = e
|
575
860
|
self.logger.warning(f"Async request failed: {str(e)}")
|
576
861
|
|
577
862
|
# Last attempt, no more retries
|
578
863
|
if attempt == retry_count:
|
579
|
-
|
580
|
-
|
581
|
-
|
864
|
+
# Enhanced error response with recovery suggestions
|
865
|
+
return {
|
866
|
+
"response": None,
|
867
|
+
"status_code": None,
|
868
|
+
"success": False,
|
869
|
+
"error": str(e),
|
870
|
+
"error_type": type(e).__name__,
|
871
|
+
"recovery_suggestions": [
|
872
|
+
"Check network connectivity",
|
873
|
+
"Verify URL is correct and accessible",
|
874
|
+
"Check authentication credentials",
|
875
|
+
"Increase timeout or retry settings",
|
876
|
+
"Check API rate limits",
|
877
|
+
],
|
878
|
+
}
|
582
879
|
|
583
880
|
# Should not reach here, but just in case
|
584
881
|
raise NodeExecutionError(
|
585
882
|
f"Async HTTP request failed after {retry_count + 1} attempts."
|
586
883
|
)
|
587
884
|
|
885
|
+
def _get_recovery_suggestions(self, status_code: int) -> list:
|
886
|
+
"""Get recovery suggestions based on status code.
|
887
|
+
|
888
|
+
Args:
|
889
|
+
status_code: HTTP status code
|
890
|
+
|
891
|
+
Returns:
|
892
|
+
List of recovery suggestions
|
893
|
+
"""
|
894
|
+
if status_code == 401:
|
895
|
+
return [
|
896
|
+
"Check authentication credentials",
|
897
|
+
"Verify API key or token is valid",
|
898
|
+
"Ensure authentication method matches API requirements",
|
899
|
+
]
|
900
|
+
elif status_code == 403:
|
901
|
+
return [
|
902
|
+
"Verify you have permission to access this resource",
|
903
|
+
"Check API key permissions/scopes",
|
904
|
+
"Ensure IP address is whitelisted if required",
|
905
|
+
]
|
906
|
+
elif status_code == 404:
|
907
|
+
return [
|
908
|
+
"Verify the URL path is correct",
|
909
|
+
"Check if resource ID exists",
|
910
|
+
"Ensure API version in URL is correct",
|
911
|
+
]
|
912
|
+
elif status_code == 429:
|
913
|
+
return [
|
914
|
+
"API rate limit exceeded - wait before retrying",
|
915
|
+
"Implement rate limiting in your requests",
|
916
|
+
"Check rate limit headers for reset time",
|
917
|
+
]
|
918
|
+
elif status_code >= 500:
|
919
|
+
return [
|
920
|
+
"Server error - retry after a delay",
|
921
|
+
"Check API service status page",
|
922
|
+
"Contact API support if issue persists",
|
923
|
+
]
|
924
|
+
else:
|
925
|
+
return [
|
926
|
+
"Check API documentation for this status code",
|
927
|
+
"Verify request format and parameters",
|
928
|
+
"Review response body for error details",
|
929
|
+
]
|
930
|
+
|
588
931
|
async def __aenter__(self):
|
589
932
|
"""Context manager support for 'async with' statements."""
|
590
933
|
if self._session is None:
|