kailash 0.1.0__py3-none-any.whl → 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kailash/__init__.py +1 -1
- 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 -44
- kailash/nodes/api/rate_limiting.py +2 -2
- kailash/nodes/api/rest.py +464 -56
- 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 +4 -2
- kailash/nodes/data/writers.py +6 -3
- kailash/nodes/logic/operations.py +2 -1
- 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 +571 -0
- kailash/nodes/transform/__init__.py +16 -1
- kailash/nodes/transform/chunkers.py +78 -0
- kailash/nodes/transform/formatters.py +96 -0
- kailash/runtime/docker.py +6 -6
- kailash/sdk_exceptions.py +24 -10
- kailash/tracking/metrics_collector.py +2 -1
- kailash/utils/templates.py +6 -6
- {kailash-0.1.0.dist-info → kailash-0.1.2.dist-info}/METADATA +349 -49
- {kailash-0.1.0.dist-info → kailash-0.1.2.dist-info}/RECORD +38 -27
- {kailash-0.1.0.dist-info → kailash-0.1.2.dist-info}/WHEEL +0 -0
- {kailash-0.1.0.dist-info → kailash-0.1.2.dist-info}/entry_points.txt +0 -0
- {kailash-0.1.0.dist-info → kailash-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.1.0.dist-info → kailash-0.1.2.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,7 +419,13 @@ 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
431
|
last_error = None
|
@@ -300,17 +436,19 @@ class HTTPRequestNode(Node):
|
|
300
436
|
self.logger.info(
|
301
437
|
f"Retry attempt {attempt}/{retry_count} after {wait_time:.2f}s"
|
302
438
|
)
|
303
|
-
import time
|
304
|
-
|
305
439
|
time.sleep(wait_time)
|
306
440
|
|
307
441
|
try:
|
308
|
-
import time
|
309
|
-
|
310
442
|
start_time = time.time()
|
311
443
|
response = self.session.request(method=method.value, **request_kwargs)
|
312
444
|
response_time = (time.time() - start_time) * 1000 # Convert to ms
|
313
445
|
|
446
|
+
# Log response if enabled
|
447
|
+
if log_requests:
|
448
|
+
self.logger.info(f"Response: {response.status_code}")
|
449
|
+
self.logger.info(f"Headers: {dict(response.headers)}")
|
450
|
+
self.logger.info(f"Body: {response.text[:500]}...")
|
451
|
+
|
314
452
|
# Success, break the retry loop
|
315
453
|
break
|
316
454
|
|
@@ -320,9 +458,21 @@ class HTTPRequestNode(Node):
|
|
320
458
|
|
321
459
|
# Last attempt, no more retries
|
322
460
|
if attempt == retry_count:
|
323
|
-
|
324
|
-
|
325
|
-
|
461
|
+
# Enhanced error response with recovery suggestions
|
462
|
+
return {
|
463
|
+
"response": None,
|
464
|
+
"status_code": None,
|
465
|
+
"success": False,
|
466
|
+
"error": str(e),
|
467
|
+
"error_type": type(e).__name__,
|
468
|
+
"recovery_suggestions": [
|
469
|
+
"Check network connectivity",
|
470
|
+
"Verify URL is correct and accessible",
|
471
|
+
"Check authentication credentials",
|
472
|
+
"Increase timeout or retry settings",
|
473
|
+
"Check API rate limits",
|
474
|
+
],
|
475
|
+
}
|
326
476
|
|
327
477
|
# Parse response based on format
|
328
478
|
content_type = response.headers.get("Content-Type", "")
|
@@ -363,16 +513,70 @@ class HTTPRequestNode(Node):
|
|
363
513
|
# Return results
|
364
514
|
success = 200 <= response.status_code < 300
|
365
515
|
|
366
|
-
|
516
|
+
# Add recovery suggestions for error responses
|
517
|
+
result = {
|
367
518
|
"response": http_response,
|
368
519
|
"status_code": response.status_code,
|
369
520
|
"success": success,
|
370
521
|
}
|
371
522
|
|
523
|
+
if not success:
|
524
|
+
result["recovery_suggestions"] = self._get_recovery_suggestions(
|
525
|
+
response.status_code
|
526
|
+
)
|
527
|
+
|
528
|
+
return result
|
529
|
+
|
530
|
+
def _get_recovery_suggestions(self, status_code: int) -> list:
|
531
|
+
"""Get recovery suggestions based on status code.
|
372
532
|
|
373
|
-
|
533
|
+
Args:
|
534
|
+
status_code: HTTP status code
|
535
|
+
|
536
|
+
Returns:
|
537
|
+
List of recovery suggestions
|
538
|
+
"""
|
539
|
+
if status_code == 401:
|
540
|
+
return [
|
541
|
+
"Check authentication credentials",
|
542
|
+
"Verify API key or token is valid",
|
543
|
+
"Ensure authentication method matches API requirements",
|
544
|
+
]
|
545
|
+
elif status_code == 403:
|
546
|
+
return [
|
547
|
+
"Verify you have permission to access this resource",
|
548
|
+
"Check API key permissions/scopes",
|
549
|
+
"Ensure IP address is whitelisted if required",
|
550
|
+
]
|
551
|
+
elif status_code == 404:
|
552
|
+
return [
|
553
|
+
"Verify the URL path is correct",
|
554
|
+
"Check if resource ID exists",
|
555
|
+
"Ensure API version in URL is correct",
|
556
|
+
]
|
557
|
+
elif status_code == 429:
|
558
|
+
return [
|
559
|
+
"API rate limit exceeded - wait before retrying",
|
560
|
+
"Implement rate limiting in your requests",
|
561
|
+
"Check rate limit headers for reset time",
|
562
|
+
]
|
563
|
+
elif status_code >= 500:
|
564
|
+
return [
|
565
|
+
"Server error - retry after a delay",
|
566
|
+
"Check API service status page",
|
567
|
+
"Contact API support if issue persists",
|
568
|
+
]
|
569
|
+
else:
|
570
|
+
return [
|
571
|
+
"Check API documentation for this status code",
|
572
|
+
"Verify request format and parameters",
|
573
|
+
"Review response body for error details",
|
574
|
+
]
|
575
|
+
|
576
|
+
|
577
|
+
@register_node()
|
374
578
|
class AsyncHTTPRequestNode(AsyncNode):
|
375
|
-
"""Asynchronous node for making HTTP requests to external APIs.
|
579
|
+
"""Asynchronous enhanced node for making HTTP requests to external APIs.
|
376
580
|
|
377
581
|
This node provides the same functionality as HTTPRequestNode but uses
|
378
582
|
asynchronous I/O for better performance, especially for concurrent requests.
|
@@ -418,6 +622,49 @@ class AsyncHTTPRequestNode(AsyncNode):
|
|
418
622
|
# Same output schema as the synchronous version
|
419
623
|
return HTTPRequestNode().get_output_schema()
|
420
624
|
|
625
|
+
def _apply_authentication(
|
626
|
+
self,
|
627
|
+
headers: dict,
|
628
|
+
auth_type: Optional[str],
|
629
|
+
auth_token: Optional[str],
|
630
|
+
auth_username: Optional[str],
|
631
|
+
auth_password: Optional[str],
|
632
|
+
api_key_header: str,
|
633
|
+
) -> dict:
|
634
|
+
"""Apply authentication to request headers.
|
635
|
+
|
636
|
+
Args:
|
637
|
+
headers: Existing headers dictionary
|
638
|
+
auth_type: Type of authentication (bearer, basic, api_key, oauth2)
|
639
|
+
auth_token: Token for bearer/api_key/oauth2 authentication
|
640
|
+
auth_username: Username for basic authentication
|
641
|
+
auth_password: Password for basic authentication
|
642
|
+
api_key_header: Header name for API key authentication
|
643
|
+
|
644
|
+
Returns:
|
645
|
+
Updated headers dictionary with authentication
|
646
|
+
"""
|
647
|
+
if not auth_type:
|
648
|
+
return headers
|
649
|
+
|
650
|
+
auth_headers = headers.copy()
|
651
|
+
|
652
|
+
if auth_type.lower() == "bearer" and auth_token:
|
653
|
+
auth_headers["Authorization"] = f"Bearer {auth_token}"
|
654
|
+
|
655
|
+
elif auth_type.lower() == "basic" and auth_username and auth_password:
|
656
|
+
credentials = f"{auth_username}:{auth_password}"
|
657
|
+
encoded = base64.b64encode(credentials.encode()).decode()
|
658
|
+
auth_headers["Authorization"] = f"Basic {encoded}"
|
659
|
+
|
660
|
+
elif auth_type.lower() == "api_key" and auth_token:
|
661
|
+
auth_headers[api_key_header] = auth_token
|
662
|
+
|
663
|
+
elif auth_type.lower() == "oauth2" and auth_token:
|
664
|
+
auth_headers["Authorization"] = f"Bearer {auth_token}"
|
665
|
+
|
666
|
+
return auth_headers
|
667
|
+
|
421
668
|
def run(self, **kwargs) -> Dict[str, Any]:
|
422
669
|
"""Synchronous version of the request, for compatibility.
|
423
670
|
|
@@ -450,6 +697,8 @@ class AsyncHTTPRequestNode(AsyncNode):
|
|
450
697
|
NodeExecutionError: If the request fails or returns an error status
|
451
698
|
"""
|
452
699
|
url = kwargs.get("url")
|
700
|
+
if not url:
|
701
|
+
raise NodeValidationError("URL parameter is required")
|
453
702
|
method = kwargs.get("method", "GET").upper()
|
454
703
|
headers = kwargs.get("headers", {})
|
455
704
|
params = kwargs.get("params", {})
|
@@ -460,6 +709,24 @@ class AsyncHTTPRequestNode(AsyncNode):
|
|
460
709
|
verify_ssl = kwargs.get("verify_ssl", True)
|
461
710
|
retry_count = kwargs.get("retry_count", 0)
|
462
711
|
retry_backoff = kwargs.get("retry_backoff", 0.5)
|
712
|
+
auth_type = kwargs.get("auth_type")
|
713
|
+
auth_token = kwargs.get("auth_token")
|
714
|
+
auth_username = kwargs.get("auth_username")
|
715
|
+
auth_password = kwargs.get("auth_password")
|
716
|
+
api_key_header = kwargs.get("api_key_header", "X-API-Key")
|
717
|
+
rate_limit_delay = kwargs.get("rate_limit_delay", 0)
|
718
|
+
log_requests = kwargs.get("log_requests", False)
|
719
|
+
|
720
|
+
# Apply authentication to headers
|
721
|
+
if auth_type:
|
722
|
+
headers = self._apply_authentication(
|
723
|
+
headers,
|
724
|
+
auth_type,
|
725
|
+
auth_token,
|
726
|
+
auth_username,
|
727
|
+
auth_password,
|
728
|
+
api_key_header,
|
729
|
+
)
|
463
730
|
|
464
731
|
# Validate method
|
465
732
|
try:
|
@@ -479,6 +746,10 @@ class AsyncHTTPRequestNode(AsyncNode):
|
|
479
746
|
f"Supported formats: {', '.join([f.value for f in ResponseFormat])}"
|
480
747
|
)
|
481
748
|
|
749
|
+
# Apply rate limit delay if configured
|
750
|
+
if rate_limit_delay > 0:
|
751
|
+
await asyncio.sleep(rate_limit_delay)
|
752
|
+
|
482
753
|
# Create session if needed
|
483
754
|
if self._session is None:
|
484
755
|
self._session = aiohttp.ClientSession()
|
@@ -499,7 +770,13 @@ class AsyncHTTPRequestNode(AsyncNode):
|
|
499
770
|
request_kwargs["data"] = data
|
500
771
|
|
501
772
|
# Execute request with retries
|
502
|
-
|
773
|
+
if log_requests:
|
774
|
+
self.logger.info(f"Request: {method} {url}")
|
775
|
+
self.logger.info(f"Headers: {headers}")
|
776
|
+
if data or json_data:
|
777
|
+
self.logger.info(f"Body: {json_data or data}")
|
778
|
+
else:
|
779
|
+
self.logger.info(f"Making async {method} request to {url}")
|
503
780
|
|
504
781
|
response = None
|
505
782
|
last_error = None
|
@@ -513,8 +790,6 @@ class AsyncHTTPRequestNode(AsyncNode):
|
|
513
790
|
await asyncio.sleep(wait_time)
|
514
791
|
|
515
792
|
try:
|
516
|
-
import time
|
517
|
-
|
518
793
|
start_time = time.time()
|
519
794
|
|
520
795
|
async with self._session.request(
|
@@ -525,6 +800,13 @@ class AsyncHTTPRequestNode(AsyncNode):
|
|
525
800
|
# Get content type
|
526
801
|
content_type = response.headers.get("Content-Type", "")
|
527
802
|
|
803
|
+
# Log response if enabled
|
804
|
+
if log_requests:
|
805
|
+
self.logger.info(f"Response: {response.status}")
|
806
|
+
self.logger.info(f"Headers: {dict(response.headers)}")
|
807
|
+
text_preview = await response.text()
|
808
|
+
self.logger.info(f"Body: {text_preview[:500]}...")
|
809
|
+
|
528
810
|
# Determine response format
|
529
811
|
actual_format = response_format
|
530
812
|
if actual_format == ResponseFormat.AUTO:
|
@@ -564,27 +846,92 @@ class AsyncHTTPRequestNode(AsyncNode):
|
|
564
846
|
# Return results
|
565
847
|
success = 200 <= response.status < 300
|
566
848
|
|
567
|
-
|
849
|
+
result = {
|
568
850
|
"response": http_response,
|
569
851
|
"status_code": response.status,
|
570
852
|
"success": success,
|
571
853
|
}
|
572
854
|
|
855
|
+
if not success:
|
856
|
+
result["recovery_suggestions"] = self._get_recovery_suggestions(
|
857
|
+
response.status
|
858
|
+
)
|
859
|
+
|
860
|
+
return result
|
861
|
+
|
573
862
|
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
|
574
863
|
last_error = e
|
575
864
|
self.logger.warning(f"Async request failed: {str(e)}")
|
576
865
|
|
577
866
|
# Last attempt, no more retries
|
578
867
|
if attempt == retry_count:
|
579
|
-
|
580
|
-
|
581
|
-
|
868
|
+
# Enhanced error response with recovery suggestions
|
869
|
+
return {
|
870
|
+
"response": None,
|
871
|
+
"status_code": None,
|
872
|
+
"success": False,
|
873
|
+
"error": str(e),
|
874
|
+
"error_type": type(e).__name__,
|
875
|
+
"recovery_suggestions": [
|
876
|
+
"Check network connectivity",
|
877
|
+
"Verify URL is correct and accessible",
|
878
|
+
"Check authentication credentials",
|
879
|
+
"Increase timeout or retry settings",
|
880
|
+
"Check API rate limits",
|
881
|
+
],
|
882
|
+
}
|
582
883
|
|
583
884
|
# Should not reach here, but just in case
|
584
885
|
raise NodeExecutionError(
|
585
886
|
f"Async HTTP request failed after {retry_count + 1} attempts."
|
586
887
|
)
|
587
888
|
|
889
|
+
def _get_recovery_suggestions(self, status_code: int) -> list:
|
890
|
+
"""Get recovery suggestions based on status code.
|
891
|
+
|
892
|
+
Args:
|
893
|
+
status_code: HTTP status code
|
894
|
+
|
895
|
+
Returns:
|
896
|
+
List of recovery suggestions
|
897
|
+
"""
|
898
|
+
if status_code == 401:
|
899
|
+
return [
|
900
|
+
"Check authentication credentials",
|
901
|
+
"Verify API key or token is valid",
|
902
|
+
"Ensure authentication method matches API requirements",
|
903
|
+
]
|
904
|
+
elif status_code == 403:
|
905
|
+
return [
|
906
|
+
"Verify you have permission to access this resource",
|
907
|
+
"Check API key permissions/scopes",
|
908
|
+
"Ensure IP address is whitelisted if required",
|
909
|
+
]
|
910
|
+
elif status_code == 404:
|
911
|
+
return [
|
912
|
+
"Verify the URL path is correct",
|
913
|
+
"Check if resource ID exists",
|
914
|
+
"Ensure API version in URL is correct",
|
915
|
+
]
|
916
|
+
elif status_code == 429:
|
917
|
+
return [
|
918
|
+
"API rate limit exceeded - wait before retrying",
|
919
|
+
"Implement rate limiting in your requests",
|
920
|
+
"Check rate limit headers for reset time",
|
921
|
+
]
|
922
|
+
elif status_code >= 500:
|
923
|
+
return [
|
924
|
+
"Server error - retry after a delay",
|
925
|
+
"Check API service status page",
|
926
|
+
"Contact API support if issue persists",
|
927
|
+
]
|
928
|
+
else:
|
929
|
+
return [
|
930
|
+
"Check API documentation for this status code",
|
931
|
+
"Verify request format and parameters",
|
932
|
+
"Review response body for error details",
|
933
|
+
]
|
934
|
+
|
588
935
|
async def __aenter__(self):
|
589
936
|
"""Context manager support for 'async with' statements."""
|
590
937
|
if self._session is None:
|
@@ -259,7 +259,7 @@ def create_rate_limiter(config: RateLimitConfig) -> RateLimiter:
|
|
259
259
|
raise ValueError(f"Unsupported rate limiting strategy: {config.strategy}")
|
260
260
|
|
261
261
|
|
262
|
-
@register_node(
|
262
|
+
@register_node()
|
263
263
|
class RateLimitedAPINode(Node):
|
264
264
|
"""Wrapper node that adds rate limiting to any API node.
|
265
265
|
|
@@ -427,7 +427,7 @@ class RateLimitedAPINode(Node):
|
|
427
427
|
)
|
428
428
|
|
429
429
|
|
430
|
-
@register_node(
|
430
|
+
@register_node()
|
431
431
|
class AsyncRateLimitedAPINode(AsyncNode):
|
432
432
|
"""Asynchronous wrapper node that adds rate limiting to any async API node.
|
433
433
|
|