kailash 0.1.0__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 +31 -0
- kailash/__main__.py +11 -0
- kailash/cli/__init__.py +5 -0
- kailash/cli/commands.py +563 -0
- kailash/manifest.py +778 -0
- kailash/nodes/__init__.py +23 -0
- kailash/nodes/ai/__init__.py +26 -0
- kailash/nodes/ai/agents.py +417 -0
- kailash/nodes/ai/models.py +488 -0
- kailash/nodes/api/__init__.py +52 -0
- kailash/nodes/api/auth.py +567 -0
- kailash/nodes/api/graphql.py +480 -0
- kailash/nodes/api/http.py +598 -0
- kailash/nodes/api/rate_limiting.py +572 -0
- kailash/nodes/api/rest.py +665 -0
- kailash/nodes/base.py +1032 -0
- kailash/nodes/base_async.py +128 -0
- kailash/nodes/code/__init__.py +32 -0
- kailash/nodes/code/python.py +1021 -0
- kailash/nodes/data/__init__.py +125 -0
- kailash/nodes/data/readers.py +496 -0
- kailash/nodes/data/sharepoint_graph.py +623 -0
- kailash/nodes/data/sql.py +380 -0
- kailash/nodes/data/streaming.py +1168 -0
- kailash/nodes/data/vector_db.py +964 -0
- kailash/nodes/data/writers.py +529 -0
- kailash/nodes/logic/__init__.py +6 -0
- kailash/nodes/logic/async_operations.py +702 -0
- kailash/nodes/logic/operations.py +551 -0
- kailash/nodes/transform/__init__.py +5 -0
- kailash/nodes/transform/processors.py +379 -0
- kailash/runtime/__init__.py +6 -0
- kailash/runtime/async_local.py +356 -0
- kailash/runtime/docker.py +697 -0
- kailash/runtime/local.py +434 -0
- kailash/runtime/parallel.py +557 -0
- kailash/runtime/runner.py +110 -0
- kailash/runtime/testing.py +347 -0
- kailash/sdk_exceptions.py +307 -0
- kailash/tracking/__init__.py +7 -0
- kailash/tracking/manager.py +885 -0
- kailash/tracking/metrics_collector.py +342 -0
- kailash/tracking/models.py +535 -0
- kailash/tracking/storage/__init__.py +0 -0
- kailash/tracking/storage/base.py +113 -0
- kailash/tracking/storage/database.py +619 -0
- kailash/tracking/storage/filesystem.py +543 -0
- kailash/utils/__init__.py +0 -0
- kailash/utils/export.py +924 -0
- kailash/utils/templates.py +680 -0
- kailash/visualization/__init__.py +62 -0
- kailash/visualization/api.py +732 -0
- kailash/visualization/dashboard.py +951 -0
- kailash/visualization/performance.py +808 -0
- kailash/visualization/reports.py +1471 -0
- kailash/workflow/__init__.py +15 -0
- kailash/workflow/builder.py +245 -0
- kailash/workflow/graph.py +827 -0
- kailash/workflow/mermaid_visualizer.py +628 -0
- kailash/workflow/mock_registry.py +63 -0
- kailash/workflow/runner.py +302 -0
- kailash/workflow/state.py +238 -0
- kailash/workflow/visualization.py +588 -0
- kailash-0.1.0.dist-info/METADATA +710 -0
- kailash-0.1.0.dist-info/RECORD +69 -0
- kailash-0.1.0.dist-info/WHEEL +5 -0
- kailash-0.1.0.dist-info/entry_points.txt +2 -0
- kailash-0.1.0.dist-info/licenses/LICENSE +21 -0
- kailash-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,598 @@
|
|
1
|
+
"""HTTP client nodes for making requests to external APIs.
|
2
|
+
|
3
|
+
This module provides nodes for making HTTP requests to external services.
|
4
|
+
Both synchronous and asynchronous versions are provided to support different workflow
|
5
|
+
execution modes.
|
6
|
+
|
7
|
+
Key Components:
|
8
|
+
- HTTPRequestNode: Synchronous HTTP client node
|
9
|
+
- AsyncHTTPRequestNode: Asynchronous HTTP client node
|
10
|
+
- Authentication helpers and utilities
|
11
|
+
"""
|
12
|
+
|
13
|
+
import asyncio
|
14
|
+
from enum import Enum
|
15
|
+
from typing import Any, Dict, Optional
|
16
|
+
|
17
|
+
import aiohttp
|
18
|
+
import requests
|
19
|
+
from pydantic import BaseModel
|
20
|
+
|
21
|
+
from kailash.nodes.base import Node, NodeParameter, register_node
|
22
|
+
from kailash.nodes.base_async import AsyncNode
|
23
|
+
from kailash.sdk_exceptions import NodeExecutionError, NodeValidationError
|
24
|
+
|
25
|
+
|
26
|
+
class HTTPMethod(str, Enum):
|
27
|
+
"""HTTP methods supported by the HTTPRequestNode."""
|
28
|
+
|
29
|
+
GET = "GET"
|
30
|
+
POST = "POST"
|
31
|
+
PUT = "PUT"
|
32
|
+
PATCH = "PATCH"
|
33
|
+
DELETE = "DELETE"
|
34
|
+
HEAD = "HEAD"
|
35
|
+
OPTIONS = "OPTIONS"
|
36
|
+
|
37
|
+
|
38
|
+
class ResponseFormat(str, Enum):
|
39
|
+
"""Response formats supported by the HTTPRequestNode."""
|
40
|
+
|
41
|
+
JSON = "json"
|
42
|
+
TEXT = "text"
|
43
|
+
BINARY = "binary"
|
44
|
+
AUTO = "auto" # Determine based on Content-Type header
|
45
|
+
|
46
|
+
|
47
|
+
class HTTPResponse(BaseModel):
|
48
|
+
"""Model for HTTP response data.
|
49
|
+
|
50
|
+
This model provides a consistent structure for HTTP responses
|
51
|
+
returned by the HTTPRequestNode.
|
52
|
+
"""
|
53
|
+
|
54
|
+
status_code: int
|
55
|
+
headers: Dict[str, str]
|
56
|
+
content_type: Optional[str] = None
|
57
|
+
content: Any # Can be dict, str, bytes depending on response format
|
58
|
+
response_time_ms: float
|
59
|
+
url: str
|
60
|
+
|
61
|
+
|
62
|
+
@register_node(alias="HTTPRequest")
|
63
|
+
class HTTPRequestNode(Node):
|
64
|
+
"""Node for making HTTP requests to external APIs.
|
65
|
+
|
66
|
+
This node provides a flexible interface for making HTTP requests with support for:
|
67
|
+
- All common HTTP methods (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS)
|
68
|
+
- JSON, form, and multipart request bodies
|
69
|
+
- Custom headers and query parameters
|
70
|
+
- Response parsing (JSON, text, binary)
|
71
|
+
- Basic error handling and retries
|
72
|
+
|
73
|
+
Design Purpose:
|
74
|
+
- Enable workflow integration with external HTTP APIs
|
75
|
+
- Provide a consistent interface for HTTP operations
|
76
|
+
- Support common authentication patterns
|
77
|
+
- Handle response parsing and error handling
|
78
|
+
|
79
|
+
Upstream Usage:
|
80
|
+
- Workflow: Creates and configures node for API integration
|
81
|
+
- Specialized API nodes: May extend this node for specific APIs
|
82
|
+
|
83
|
+
Downstream Consumers:
|
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
|
+
"""
|
88
|
+
|
89
|
+
def __init__(self, **kwargs):
|
90
|
+
"""Initialize the HTTP request node.
|
91
|
+
|
92
|
+
Args:
|
93
|
+
url (str): The URL to send the request to
|
94
|
+
method (str): HTTP method to use (GET, POST, PUT, etc.)
|
95
|
+
headers (dict, optional): HTTP headers to include in the request
|
96
|
+
params (dict, optional): Query parameters to include in the URL
|
97
|
+
data (dict/str, optional): Request body data (for POST, PUT, etc.)
|
98
|
+
json_data (dict, optional): JSON data to send (automatically sets Content-Type)
|
99
|
+
response_format (str, optional): Format to parse response as (json, text, binary, auto)
|
100
|
+
timeout (int, optional): Request timeout in seconds
|
101
|
+
verify_ssl (bool, optional): Whether to verify SSL certificates
|
102
|
+
retry_count (int, optional): Number of times to retry failed requests
|
103
|
+
retry_backoff (float, optional): Backoff factor for retries
|
104
|
+
**kwargs: Additional parameters passed to base Node
|
105
|
+
"""
|
106
|
+
super().__init__(**kwargs)
|
107
|
+
self.session = requests.Session()
|
108
|
+
|
109
|
+
def get_parameters(self) -> Dict[str, NodeParameter]:
|
110
|
+
"""Define the parameters this node accepts.
|
111
|
+
|
112
|
+
Returns:
|
113
|
+
Dictionary of parameter definitions
|
114
|
+
"""
|
115
|
+
return {
|
116
|
+
"url": NodeParameter(
|
117
|
+
name="url",
|
118
|
+
type=str,
|
119
|
+
required=True,
|
120
|
+
description="URL to send the request to",
|
121
|
+
),
|
122
|
+
"method": NodeParameter(
|
123
|
+
name="method",
|
124
|
+
type=str,
|
125
|
+
required=True,
|
126
|
+
default="GET",
|
127
|
+
description="HTTP method (GET, POST, PUT, PATCH, DELETE)",
|
128
|
+
),
|
129
|
+
"headers": NodeParameter(
|
130
|
+
name="headers",
|
131
|
+
type=dict,
|
132
|
+
required=False,
|
133
|
+
default={},
|
134
|
+
description="HTTP headers to include in the request",
|
135
|
+
),
|
136
|
+
"params": NodeParameter(
|
137
|
+
name="params",
|
138
|
+
type=dict,
|
139
|
+
required=False,
|
140
|
+
default={},
|
141
|
+
description="Query parameters to include in the URL",
|
142
|
+
),
|
143
|
+
"data": NodeParameter(
|
144
|
+
name="data",
|
145
|
+
type=Any,
|
146
|
+
required=False,
|
147
|
+
default=None,
|
148
|
+
description="Request body data (for POST, PUT, etc.)",
|
149
|
+
),
|
150
|
+
"json_data": NodeParameter(
|
151
|
+
name="json_data",
|
152
|
+
type=dict,
|
153
|
+
required=False,
|
154
|
+
default=None,
|
155
|
+
description="JSON data to send (automatically sets Content-Type)",
|
156
|
+
),
|
157
|
+
"response_format": NodeParameter(
|
158
|
+
name="response_format",
|
159
|
+
type=str,
|
160
|
+
required=False,
|
161
|
+
default="auto",
|
162
|
+
description="Format to parse response as (json, text, binary, auto)",
|
163
|
+
),
|
164
|
+
"timeout": NodeParameter(
|
165
|
+
name="timeout",
|
166
|
+
type=int,
|
167
|
+
required=False,
|
168
|
+
default=30,
|
169
|
+
description="Request timeout in seconds",
|
170
|
+
),
|
171
|
+
"verify_ssl": NodeParameter(
|
172
|
+
name="verify_ssl",
|
173
|
+
type=bool,
|
174
|
+
required=False,
|
175
|
+
default=True,
|
176
|
+
description="Whether to verify SSL certificates",
|
177
|
+
),
|
178
|
+
"retry_count": NodeParameter(
|
179
|
+
name="retry_count",
|
180
|
+
type=int,
|
181
|
+
required=False,
|
182
|
+
default=0,
|
183
|
+
description="Number of times to retry failed requests",
|
184
|
+
),
|
185
|
+
"retry_backoff": NodeParameter(
|
186
|
+
name="retry_backoff",
|
187
|
+
type=float,
|
188
|
+
required=False,
|
189
|
+
default=0.5,
|
190
|
+
description="Backoff factor for retries",
|
191
|
+
),
|
192
|
+
}
|
193
|
+
|
194
|
+
def get_output_schema(self) -> Dict[str, NodeParameter]:
|
195
|
+
"""Define the output schema for this node.
|
196
|
+
|
197
|
+
Returns:
|
198
|
+
Dictionary of output parameter definitions
|
199
|
+
"""
|
200
|
+
return {
|
201
|
+
"response": NodeParameter(
|
202
|
+
name="response",
|
203
|
+
type=dict,
|
204
|
+
required=True,
|
205
|
+
description="HTTP response data including status, headers, and content",
|
206
|
+
),
|
207
|
+
"status_code": NodeParameter(
|
208
|
+
name="status_code",
|
209
|
+
type=int,
|
210
|
+
required=True,
|
211
|
+
description="HTTP status code",
|
212
|
+
),
|
213
|
+
"success": NodeParameter(
|
214
|
+
name="success",
|
215
|
+
type=bool,
|
216
|
+
required=True,
|
217
|
+
description="Whether the request was successful (status code 200-299)",
|
218
|
+
),
|
219
|
+
}
|
220
|
+
|
221
|
+
def run(self, **kwargs) -> Dict[str, Any]:
|
222
|
+
"""Execute an HTTP request.
|
223
|
+
|
224
|
+
Args:
|
225
|
+
url (str): The URL to send the request to
|
226
|
+
method (str): HTTP method to use
|
227
|
+
headers (dict, optional): HTTP headers
|
228
|
+
params (dict, optional): Query parameters
|
229
|
+
data (dict/str, optional): Request body data
|
230
|
+
json_data (dict, optional): JSON data to send
|
231
|
+
response_format (str, optional): Format to parse response as
|
232
|
+
timeout (int, optional): Request timeout in seconds
|
233
|
+
verify_ssl (bool, optional): Whether to verify SSL certificates
|
234
|
+
retry_count (int, optional): Number of times to retry failed requests
|
235
|
+
retry_backoff (float, optional): Backoff factor for retries
|
236
|
+
|
237
|
+
Returns:
|
238
|
+
Dictionary containing:
|
239
|
+
response: HTTPResponse object
|
240
|
+
status_code: HTTP status code
|
241
|
+
success: Boolean indicating request success
|
242
|
+
|
243
|
+
Raises:
|
244
|
+
NodeExecutionError: If the request fails or returns an error status
|
245
|
+
"""
|
246
|
+
url = kwargs.get("url")
|
247
|
+
method = kwargs.get("method", "GET").upper()
|
248
|
+
headers = kwargs.get("headers", {})
|
249
|
+
params = kwargs.get("params", {})
|
250
|
+
data = kwargs.get("data")
|
251
|
+
json_data = kwargs.get("json_data")
|
252
|
+
response_format = kwargs.get("response_format", "auto")
|
253
|
+
timeout = kwargs.get("timeout", 30)
|
254
|
+
verify_ssl = kwargs.get("verify_ssl", True)
|
255
|
+
retry_count = kwargs.get("retry_count", 0)
|
256
|
+
retry_backoff = kwargs.get("retry_backoff", 0.5)
|
257
|
+
|
258
|
+
# Validate method
|
259
|
+
try:
|
260
|
+
method = HTTPMethod(method)
|
261
|
+
except ValueError:
|
262
|
+
raise NodeValidationError(
|
263
|
+
f"Invalid HTTP method: {method}. "
|
264
|
+
f"Supported methods: {', '.join([m.value for m in HTTPMethod])}"
|
265
|
+
)
|
266
|
+
|
267
|
+
# Validate response format
|
268
|
+
try:
|
269
|
+
response_format = ResponseFormat(response_format)
|
270
|
+
except ValueError:
|
271
|
+
raise NodeValidationError(
|
272
|
+
f"Invalid response format: {response_format}. "
|
273
|
+
f"Supported formats: {', '.join([f.value for f in ResponseFormat])}"
|
274
|
+
)
|
275
|
+
|
276
|
+
# Prepare request kwargs
|
277
|
+
request_kwargs = {
|
278
|
+
"url": url,
|
279
|
+
"headers": headers,
|
280
|
+
"params": params,
|
281
|
+
"timeout": timeout,
|
282
|
+
"verify": verify_ssl,
|
283
|
+
}
|
284
|
+
|
285
|
+
# Add data or json based on what was provided
|
286
|
+
if json_data is not None:
|
287
|
+
request_kwargs["json"] = json_data
|
288
|
+
elif data is not None:
|
289
|
+
request_kwargs["data"] = data
|
290
|
+
|
291
|
+
# Execute request with retries
|
292
|
+
self.logger.info(f"Making {method} request to {url}")
|
293
|
+
|
294
|
+
response = None
|
295
|
+
last_error = None
|
296
|
+
|
297
|
+
for attempt in range(retry_count + 1):
|
298
|
+
if attempt > 0:
|
299
|
+
wait_time = retry_backoff * (2 ** (attempt - 1))
|
300
|
+
self.logger.info(
|
301
|
+
f"Retry attempt {attempt}/{retry_count} after {wait_time:.2f}s"
|
302
|
+
)
|
303
|
+
import time
|
304
|
+
|
305
|
+
time.sleep(wait_time)
|
306
|
+
|
307
|
+
try:
|
308
|
+
import time
|
309
|
+
|
310
|
+
start_time = time.time()
|
311
|
+
response = self.session.request(method=method.value, **request_kwargs)
|
312
|
+
response_time = (time.time() - start_time) * 1000 # Convert to ms
|
313
|
+
|
314
|
+
# Success, break the retry loop
|
315
|
+
break
|
316
|
+
|
317
|
+
except requests.RequestException as e:
|
318
|
+
last_error = e
|
319
|
+
self.logger.warning(f"Request failed: {str(e)}")
|
320
|
+
|
321
|
+
# Last attempt, no more retries
|
322
|
+
if attempt == retry_count:
|
323
|
+
raise NodeExecutionError(
|
324
|
+
f"HTTP request failed after {retry_count + 1} attempts: {str(e)}"
|
325
|
+
) from e
|
326
|
+
|
327
|
+
# Parse response based on format
|
328
|
+
content_type = response.headers.get("Content-Type", "")
|
329
|
+
|
330
|
+
if response_format == ResponseFormat.AUTO:
|
331
|
+
if "application/json" in content_type:
|
332
|
+
response_format = ResponseFormat.JSON
|
333
|
+
elif "text/" in content_type:
|
334
|
+
response_format = ResponseFormat.TEXT
|
335
|
+
else:
|
336
|
+
response_format = ResponseFormat.BINARY
|
337
|
+
|
338
|
+
try:
|
339
|
+
if response_format == ResponseFormat.JSON:
|
340
|
+
content = response.json()
|
341
|
+
elif response_format == ResponseFormat.TEXT:
|
342
|
+
content = response.text
|
343
|
+
elif response_format == ResponseFormat.BINARY:
|
344
|
+
content = response.content
|
345
|
+
else:
|
346
|
+
content = response.text # Fallback to text
|
347
|
+
except Exception as e:
|
348
|
+
self.logger.warning(
|
349
|
+
f"Failed to parse response as {response_format}: {str(e)}"
|
350
|
+
)
|
351
|
+
content = response.text # Fallback to text
|
352
|
+
|
353
|
+
# Create response object
|
354
|
+
http_response = HTTPResponse(
|
355
|
+
status_code=response.status_code,
|
356
|
+
headers=dict(response.headers),
|
357
|
+
content_type=content_type,
|
358
|
+
content=content,
|
359
|
+
response_time_ms=response_time,
|
360
|
+
url=response.url,
|
361
|
+
).model_dump()
|
362
|
+
|
363
|
+
# Return results
|
364
|
+
success = 200 <= response.status_code < 300
|
365
|
+
|
366
|
+
return {
|
367
|
+
"response": http_response,
|
368
|
+
"status_code": response.status_code,
|
369
|
+
"success": success,
|
370
|
+
}
|
371
|
+
|
372
|
+
|
373
|
+
@register_node(alias="AsyncHTTPRequest")
|
374
|
+
class AsyncHTTPRequestNode(AsyncNode):
|
375
|
+
"""Asynchronous node for making HTTP requests to external APIs.
|
376
|
+
|
377
|
+
This node provides the same functionality as HTTPRequestNode but uses
|
378
|
+
asynchronous I/O for better performance, especially for concurrent requests.
|
379
|
+
|
380
|
+
Design Purpose:
|
381
|
+
- Enable efficient, non-blocking HTTP operations in workflows
|
382
|
+
- Provide the same interface as HTTPRequestNode but with async execution
|
383
|
+
- Support high-throughput API integrations with minimal overhead
|
384
|
+
|
385
|
+
Upstream Usage:
|
386
|
+
- AsyncLocalRuntime: Executes workflow with async support
|
387
|
+
- Specialized async API nodes: May extend this node
|
388
|
+
|
389
|
+
Downstream Consumers:
|
390
|
+
- Data processing nodes: Consume API response data
|
391
|
+
- Decision nodes: Route workflow based on API responses
|
392
|
+
"""
|
393
|
+
|
394
|
+
def __init__(self, **kwargs):
|
395
|
+
"""Initialize the async HTTP request node.
|
396
|
+
|
397
|
+
Args:
|
398
|
+
Same as HTTPRequestNode
|
399
|
+
"""
|
400
|
+
super().__init__(**kwargs)
|
401
|
+
self._session = None # Will be created when needed
|
402
|
+
|
403
|
+
def get_parameters(self) -> Dict[str, NodeParameter]:
|
404
|
+
"""Define the parameters this node accepts.
|
405
|
+
|
406
|
+
Returns:
|
407
|
+
Dictionary of parameter definitions
|
408
|
+
"""
|
409
|
+
# Same parameters as the synchronous version
|
410
|
+
return HTTPRequestNode().get_parameters()
|
411
|
+
|
412
|
+
def get_output_schema(self) -> Dict[str, NodeParameter]:
|
413
|
+
"""Define the output schema for this node.
|
414
|
+
|
415
|
+
Returns:
|
416
|
+
Dictionary of output parameter definitions
|
417
|
+
"""
|
418
|
+
# Same output schema as the synchronous version
|
419
|
+
return HTTPRequestNode().get_output_schema()
|
420
|
+
|
421
|
+
def run(self, **kwargs) -> Dict[str, Any]:
|
422
|
+
"""Synchronous version of the request, for compatibility.
|
423
|
+
|
424
|
+
This is implemented for compatibility but users should use the
|
425
|
+
async_run method for better performance.
|
426
|
+
|
427
|
+
Args:
|
428
|
+
Same as HTTPRequestNode.run()
|
429
|
+
|
430
|
+
Returns:
|
431
|
+
Same as HTTPRequestNode.run()
|
432
|
+
|
433
|
+
Raises:
|
434
|
+
NodeExecutionError: If the request fails or returns an error status
|
435
|
+
"""
|
436
|
+
# For compatibility, create a requests.Session() and use it
|
437
|
+
http_node = HTTPRequestNode(**self.config)
|
438
|
+
return http_node.run(**kwargs)
|
439
|
+
|
440
|
+
async def async_run(self, **kwargs) -> Dict[str, Any]:
|
441
|
+
"""Execute an HTTP request asynchronously.
|
442
|
+
|
443
|
+
Args:
|
444
|
+
Same as HTTPRequestNode.run()
|
445
|
+
|
446
|
+
Returns:
|
447
|
+
Same as HTTPRequestNode.run()
|
448
|
+
|
449
|
+
Raises:
|
450
|
+
NodeExecutionError: If the request fails or returns an error status
|
451
|
+
"""
|
452
|
+
url = kwargs.get("url")
|
453
|
+
method = kwargs.get("method", "GET").upper()
|
454
|
+
headers = kwargs.get("headers", {})
|
455
|
+
params = kwargs.get("params", {})
|
456
|
+
data = kwargs.get("data")
|
457
|
+
json_data = kwargs.get("json_data")
|
458
|
+
response_format = kwargs.get("response_format", "auto")
|
459
|
+
timeout = kwargs.get("timeout", 30)
|
460
|
+
verify_ssl = kwargs.get("verify_ssl", True)
|
461
|
+
retry_count = kwargs.get("retry_count", 0)
|
462
|
+
retry_backoff = kwargs.get("retry_backoff", 0.5)
|
463
|
+
|
464
|
+
# Validate method
|
465
|
+
try:
|
466
|
+
method = HTTPMethod(method)
|
467
|
+
except ValueError:
|
468
|
+
raise NodeValidationError(
|
469
|
+
f"Invalid HTTP method: {method}. "
|
470
|
+
f"Supported methods: {', '.join([m.value for m in HTTPMethod])}"
|
471
|
+
)
|
472
|
+
|
473
|
+
# Validate response format
|
474
|
+
try:
|
475
|
+
response_format = ResponseFormat(response_format)
|
476
|
+
except ValueError:
|
477
|
+
raise NodeValidationError(
|
478
|
+
f"Invalid response format: {response_format}. "
|
479
|
+
f"Supported formats: {', '.join([f.value for f in ResponseFormat])}"
|
480
|
+
)
|
481
|
+
|
482
|
+
# Create session if needed
|
483
|
+
if self._session is None:
|
484
|
+
self._session = aiohttp.ClientSession()
|
485
|
+
|
486
|
+
# Prepare request kwargs
|
487
|
+
request_kwargs = {
|
488
|
+
"url": url,
|
489
|
+
"headers": headers,
|
490
|
+
"params": params,
|
491
|
+
"timeout": aiohttp.ClientTimeout(total=timeout),
|
492
|
+
"ssl": verify_ssl,
|
493
|
+
}
|
494
|
+
|
495
|
+
# Add data or json based on what was provided
|
496
|
+
if json_data is not None:
|
497
|
+
request_kwargs["json"] = json_data
|
498
|
+
elif data is not None:
|
499
|
+
request_kwargs["data"] = data
|
500
|
+
|
501
|
+
# Execute request with retries
|
502
|
+
self.logger.info(f"Making async {method} request to {url}")
|
503
|
+
|
504
|
+
response = None
|
505
|
+
last_error = None
|
506
|
+
|
507
|
+
for attempt in range(retry_count + 1):
|
508
|
+
if attempt > 0:
|
509
|
+
wait_time = retry_backoff * (2 ** (attempt - 1))
|
510
|
+
self.logger.info(
|
511
|
+
f"Retry attempt {attempt}/{retry_count} after {wait_time:.2f}s"
|
512
|
+
)
|
513
|
+
await asyncio.sleep(wait_time)
|
514
|
+
|
515
|
+
try:
|
516
|
+
import time
|
517
|
+
|
518
|
+
start_time = time.time()
|
519
|
+
|
520
|
+
async with self._session.request(
|
521
|
+
method=method.value, **request_kwargs
|
522
|
+
) as response:
|
523
|
+
response_time = (time.time() - start_time) * 1000 # Convert to ms
|
524
|
+
|
525
|
+
# Get content type
|
526
|
+
content_type = response.headers.get("Content-Type", "")
|
527
|
+
|
528
|
+
# Determine response format
|
529
|
+
actual_format = response_format
|
530
|
+
if actual_format == ResponseFormat.AUTO:
|
531
|
+
if "application/json" in content_type:
|
532
|
+
actual_format = ResponseFormat.JSON
|
533
|
+
elif "text/" in content_type:
|
534
|
+
actual_format = ResponseFormat.TEXT
|
535
|
+
else:
|
536
|
+
actual_format = ResponseFormat.BINARY
|
537
|
+
|
538
|
+
# Parse response
|
539
|
+
try:
|
540
|
+
if actual_format == ResponseFormat.JSON:
|
541
|
+
content = await response.json()
|
542
|
+
elif actual_format == ResponseFormat.TEXT:
|
543
|
+
content = await response.text()
|
544
|
+
elif actual_format == ResponseFormat.BINARY:
|
545
|
+
content = await response.read()
|
546
|
+
else:
|
547
|
+
content = await response.text() # Fallback to text
|
548
|
+
except Exception as e:
|
549
|
+
self.logger.warning(
|
550
|
+
f"Failed to parse response as {actual_format}: {str(e)}"
|
551
|
+
)
|
552
|
+
content = await response.text() # Fallback to text
|
553
|
+
|
554
|
+
# Create response object
|
555
|
+
http_response = HTTPResponse(
|
556
|
+
status_code=response.status,
|
557
|
+
headers=dict(response.headers),
|
558
|
+
content_type=content_type,
|
559
|
+
content=content,
|
560
|
+
response_time_ms=response_time,
|
561
|
+
url=str(response.url),
|
562
|
+
).model_dump()
|
563
|
+
|
564
|
+
# Return results
|
565
|
+
success = 200 <= response.status < 300
|
566
|
+
|
567
|
+
return {
|
568
|
+
"response": http_response,
|
569
|
+
"status_code": response.status,
|
570
|
+
"success": success,
|
571
|
+
}
|
572
|
+
|
573
|
+
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
|
574
|
+
last_error = e
|
575
|
+
self.logger.warning(f"Async request failed: {str(e)}")
|
576
|
+
|
577
|
+
# Last attempt, no more retries
|
578
|
+
if attempt == retry_count:
|
579
|
+
raise NodeExecutionError(
|
580
|
+
f"Async HTTP request failed after {retry_count + 1} attempts: {str(e)}"
|
581
|
+
) from e
|
582
|
+
|
583
|
+
# Should not reach here, but just in case
|
584
|
+
raise NodeExecutionError(
|
585
|
+
f"Async HTTP request failed after {retry_count + 1} attempts."
|
586
|
+
)
|
587
|
+
|
588
|
+
async def __aenter__(self):
|
589
|
+
"""Context manager support for 'async with' statements."""
|
590
|
+
if self._session is None:
|
591
|
+
self._session = aiohttp.ClientSession()
|
592
|
+
return self
|
593
|
+
|
594
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
595
|
+
"""Clean up session when exiting context."""
|
596
|
+
if self._session is not None:
|
597
|
+
await self._session.close()
|
598
|
+
self._session = None
|