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,665 @@
|
|
1
|
+
"""REST API client nodes for the Kailash SDK.
|
2
|
+
|
3
|
+
This module provides specialized nodes for interacting with REST APIs in both
|
4
|
+
synchronous and asynchronous modes. These nodes build on the base HTTP nodes
|
5
|
+
to provide a more convenient interface for working with REST APIs.
|
6
|
+
|
7
|
+
Key Components:
|
8
|
+
- RESTClientNode: Synchronous REST API client
|
9
|
+
- AsyncRESTClientNode: Asynchronous REST API client
|
10
|
+
- Resource path builders and response handlers
|
11
|
+
"""
|
12
|
+
|
13
|
+
from typing import Any, Dict, List, Optional
|
14
|
+
|
15
|
+
from kailash.nodes.api.http import AsyncHTTPRequestNode, HTTPRequestNode
|
16
|
+
from kailash.nodes.base import Node, NodeParameter, register_node
|
17
|
+
from kailash.nodes.base_async import AsyncNode
|
18
|
+
from kailash.sdk_exceptions import NodeExecutionError, NodeValidationError
|
19
|
+
|
20
|
+
|
21
|
+
@register_node(alias="RESTClient")
|
22
|
+
class RESTClientNode(Node):
|
23
|
+
"""Node for interacting with REST APIs.
|
24
|
+
|
25
|
+
This node provides a higher-level interface for interacting with REST APIs,
|
26
|
+
with built-in support for:
|
27
|
+
- Resource-based operations (e.g., GET /users/{id})
|
28
|
+
- Common REST patterns (list, get, create, update, delete)
|
29
|
+
- Pagination handling
|
30
|
+
- Response schema validation
|
31
|
+
- Error response handling
|
32
|
+
|
33
|
+
Design Purpose:
|
34
|
+
- Simplify REST API integration in workflows
|
35
|
+
- Provide consistent interfaces for common REST operations
|
36
|
+
- Support standard REST conventions and patterns
|
37
|
+
- Handle common REST-specific error cases
|
38
|
+
|
39
|
+
Upstream Usage:
|
40
|
+
- Workflow: Creates and configures for specific REST APIs
|
41
|
+
- API integration workflows: Uses for external service integration
|
42
|
+
|
43
|
+
Downstream Consumers:
|
44
|
+
- Data processing nodes: Consume API response data
|
45
|
+
- Custom nodes: Process API-specific data formats
|
46
|
+
"""
|
47
|
+
|
48
|
+
def __init__(self, **kwargs):
|
49
|
+
"""Initialize the REST client node.
|
50
|
+
|
51
|
+
Args:
|
52
|
+
base_url (str): Base URL for the REST API
|
53
|
+
headers (dict, optional): Default headers for all requests
|
54
|
+
auth (dict, optional): Authentication configuration
|
55
|
+
version (str, optional): API version to use
|
56
|
+
timeout (int, optional): Default request timeout in seconds
|
57
|
+
verify_ssl (bool, optional): Whether to verify SSL certificates
|
58
|
+
retry_count (int, optional): Number of times to retry failed requests
|
59
|
+
retry_backoff (float, optional): Backoff factor for retries
|
60
|
+
**kwargs: Additional parameters passed to base Node
|
61
|
+
"""
|
62
|
+
super().__init__(**kwargs)
|
63
|
+
self.http_node = HTTPRequestNode(**kwargs)
|
64
|
+
|
65
|
+
def get_parameters(self) -> Dict[str, NodeParameter]:
|
66
|
+
"""Define the parameters this node accepts.
|
67
|
+
|
68
|
+
Returns:
|
69
|
+
Dictionary of parameter definitions
|
70
|
+
"""
|
71
|
+
return {
|
72
|
+
"base_url": NodeParameter(
|
73
|
+
name="base_url",
|
74
|
+
type=str,
|
75
|
+
required=True,
|
76
|
+
description="Base URL for the REST API (e.g., https://api.example.com)",
|
77
|
+
),
|
78
|
+
"resource": NodeParameter(
|
79
|
+
name="resource",
|
80
|
+
type=str,
|
81
|
+
required=True,
|
82
|
+
description="API resource path (e.g., 'users' or 'products/{id}')",
|
83
|
+
),
|
84
|
+
"method": NodeParameter(
|
85
|
+
name="method",
|
86
|
+
type=str,
|
87
|
+
required=False,
|
88
|
+
default="GET",
|
89
|
+
description="HTTP method (GET, POST, PUT, PATCH, DELETE)",
|
90
|
+
),
|
91
|
+
"path_params": NodeParameter(
|
92
|
+
name="path_params",
|
93
|
+
type=dict,
|
94
|
+
required=False,
|
95
|
+
default={},
|
96
|
+
description="Parameters to substitute in resource path (e.g., {'id': 123})",
|
97
|
+
),
|
98
|
+
"query_params": NodeParameter(
|
99
|
+
name="query_params",
|
100
|
+
type=dict,
|
101
|
+
required=False,
|
102
|
+
default={},
|
103
|
+
description="Query parameters to include in the URL",
|
104
|
+
),
|
105
|
+
"headers": NodeParameter(
|
106
|
+
name="headers",
|
107
|
+
type=dict,
|
108
|
+
required=False,
|
109
|
+
default={},
|
110
|
+
description="HTTP headers to include in the request",
|
111
|
+
),
|
112
|
+
"data": NodeParameter(
|
113
|
+
name="data",
|
114
|
+
type=Any,
|
115
|
+
required=False,
|
116
|
+
default=None,
|
117
|
+
description="Request body data (for POST, PUT, etc.)",
|
118
|
+
),
|
119
|
+
"version": NodeParameter(
|
120
|
+
name="version",
|
121
|
+
type=str,
|
122
|
+
required=False,
|
123
|
+
default=None,
|
124
|
+
description="API version to use (e.g., 'v1')",
|
125
|
+
),
|
126
|
+
"timeout": NodeParameter(
|
127
|
+
name="timeout",
|
128
|
+
type=int,
|
129
|
+
required=False,
|
130
|
+
default=30,
|
131
|
+
description="Request timeout in seconds",
|
132
|
+
),
|
133
|
+
"verify_ssl": NodeParameter(
|
134
|
+
name="verify_ssl",
|
135
|
+
type=bool,
|
136
|
+
required=False,
|
137
|
+
default=True,
|
138
|
+
description="Whether to verify SSL certificates",
|
139
|
+
),
|
140
|
+
"paginate": NodeParameter(
|
141
|
+
name="paginate",
|
142
|
+
type=bool,
|
143
|
+
required=False,
|
144
|
+
default=False,
|
145
|
+
description="Whether to handle pagination automatically (for GET requests)",
|
146
|
+
),
|
147
|
+
"pagination_params": NodeParameter(
|
148
|
+
name="pagination_params",
|
149
|
+
type=dict,
|
150
|
+
required=False,
|
151
|
+
default=None,
|
152
|
+
description="Pagination configuration parameters",
|
153
|
+
),
|
154
|
+
"retry_count": NodeParameter(
|
155
|
+
name="retry_count",
|
156
|
+
type=int,
|
157
|
+
required=False,
|
158
|
+
default=0,
|
159
|
+
description="Number of times to retry failed requests",
|
160
|
+
),
|
161
|
+
"retry_backoff": NodeParameter(
|
162
|
+
name="retry_backoff",
|
163
|
+
type=float,
|
164
|
+
required=False,
|
165
|
+
default=0.5,
|
166
|
+
description="Backoff factor for retries",
|
167
|
+
),
|
168
|
+
}
|
169
|
+
|
170
|
+
def get_output_schema(self) -> Dict[str, NodeParameter]:
|
171
|
+
"""Define the output schema for this node.
|
172
|
+
|
173
|
+
Returns:
|
174
|
+
Dictionary of output parameter definitions
|
175
|
+
"""
|
176
|
+
return {
|
177
|
+
"data": NodeParameter(
|
178
|
+
name="data",
|
179
|
+
type=Any,
|
180
|
+
required=True,
|
181
|
+
description="Parsed response data from the API",
|
182
|
+
),
|
183
|
+
"status_code": NodeParameter(
|
184
|
+
name="status_code",
|
185
|
+
type=int,
|
186
|
+
required=True,
|
187
|
+
description="HTTP status code",
|
188
|
+
),
|
189
|
+
"success": NodeParameter(
|
190
|
+
name="success",
|
191
|
+
type=bool,
|
192
|
+
required=True,
|
193
|
+
description="Whether the request was successful (status code 200-299)",
|
194
|
+
),
|
195
|
+
"metadata": NodeParameter(
|
196
|
+
name="metadata",
|
197
|
+
type=dict,
|
198
|
+
required=True,
|
199
|
+
description="Additional metadata about the request and response",
|
200
|
+
),
|
201
|
+
}
|
202
|
+
|
203
|
+
def _build_url(
|
204
|
+
self,
|
205
|
+
base_url: str,
|
206
|
+
resource: str,
|
207
|
+
path_params: Dict[str, Any],
|
208
|
+
version: Optional[str] = None,
|
209
|
+
) -> str:
|
210
|
+
"""Build the full URL for a REST API request.
|
211
|
+
|
212
|
+
Args:
|
213
|
+
base_url: Base API URL
|
214
|
+
resource: Resource path pattern
|
215
|
+
path_params: Parameters to substitute in the path
|
216
|
+
version: API version to include in the URL
|
217
|
+
|
218
|
+
Returns:
|
219
|
+
Complete URL with path parameters substituted
|
220
|
+
|
221
|
+
Raises:
|
222
|
+
NodeValidationError: If a required path parameter is missing
|
223
|
+
"""
|
224
|
+
# Remove trailing slash from base URL if present
|
225
|
+
base_url = base_url.rstrip("/")
|
226
|
+
|
227
|
+
# Add version to URL if specified
|
228
|
+
if version:
|
229
|
+
base_url = f"{base_url}/{version}"
|
230
|
+
|
231
|
+
# Substitute path parameters
|
232
|
+
try:
|
233
|
+
# Extract required path parameters from the resource pattern
|
234
|
+
required_params = [
|
235
|
+
param.strip("{}")
|
236
|
+
for param in resource.split("/")
|
237
|
+
if param.startswith("{") and param.endswith("}")
|
238
|
+
]
|
239
|
+
|
240
|
+
# Check if all required parameters are provided
|
241
|
+
for param in required_params:
|
242
|
+
if param not in path_params:
|
243
|
+
raise NodeValidationError(
|
244
|
+
f"Missing required path parameter '{param}' for resource '{resource}'"
|
245
|
+
)
|
246
|
+
|
247
|
+
# Substitute parameters in the resource path
|
248
|
+
resource_path = resource
|
249
|
+
for param, value in path_params.items():
|
250
|
+
placeholder = f"{{{param}}}"
|
251
|
+
if placeholder in resource_path:
|
252
|
+
resource_path = resource_path.replace(placeholder, str(value))
|
253
|
+
|
254
|
+
# Ensure path starts without a slash
|
255
|
+
resource_path = resource_path.lstrip("/")
|
256
|
+
|
257
|
+
# Build complete URL
|
258
|
+
return f"{base_url}/{resource_path}"
|
259
|
+
|
260
|
+
except Exception as e:
|
261
|
+
if not isinstance(e, NodeValidationError):
|
262
|
+
raise NodeValidationError(f"Failed to build URL: {str(e)}") from e
|
263
|
+
raise
|
264
|
+
|
265
|
+
def _handle_pagination(
|
266
|
+
self,
|
267
|
+
initial_response: Dict[str, Any],
|
268
|
+
query_params: Dict[str, Any],
|
269
|
+
pagination_params: Dict[str, Any],
|
270
|
+
) -> List[Any]:
|
271
|
+
"""Handle pagination for REST API responses.
|
272
|
+
|
273
|
+
This method supports common pagination patterns:
|
274
|
+
- Page-based: ?page=1&per_page=100
|
275
|
+
- Offset-based: ?offset=0&limit=100
|
276
|
+
- Cursor-based: ?cursor=abc123
|
277
|
+
|
278
|
+
Args:
|
279
|
+
initial_response: Response from the first API call
|
280
|
+
query_params: Original query parameters
|
281
|
+
pagination_params: Configuration for pagination handling
|
282
|
+
|
283
|
+
Returns:
|
284
|
+
Combined list of items from all pages
|
285
|
+
|
286
|
+
Raises:
|
287
|
+
NodeExecutionError: If pagination fails
|
288
|
+
"""
|
289
|
+
if not pagination_params:
|
290
|
+
# Default pagination configuration
|
291
|
+
pagination_params = {
|
292
|
+
"type": "page", # page, offset, or cursor
|
293
|
+
"page_param": "page", # query parameter for page number
|
294
|
+
"limit_param": "per_page", # query parameter for items per page
|
295
|
+
"items_path": "data", # path to items in response
|
296
|
+
"total_path": "meta.total", # path to total count in response
|
297
|
+
"next_page_path": "meta.next_page", # path to next page in response
|
298
|
+
"max_pages": 10, # maximum number of pages to fetch
|
299
|
+
}
|
300
|
+
|
301
|
+
pagination_type = pagination_params.get("type", "page")
|
302
|
+
items_path = pagination_params.get("items_path", "data")
|
303
|
+
max_pages = pagination_params.get("max_pages", 10)
|
304
|
+
|
305
|
+
# Extract items from initial response
|
306
|
+
all_items = self._get_nested_value(initial_response, items_path, [])
|
307
|
+
if not isinstance(all_items, list):
|
308
|
+
raise NodeExecutionError(
|
309
|
+
f"Pagination items path '{items_path}' did not return a list in response"
|
310
|
+
)
|
311
|
+
|
312
|
+
# Return immediately if no additional pages
|
313
|
+
if pagination_type == "page":
|
314
|
+
current_page = int(
|
315
|
+
query_params.get(pagination_params.get("page_param", "page"), 1)
|
316
|
+
)
|
317
|
+
total_path = pagination_params.get("total_path")
|
318
|
+
per_page = int(
|
319
|
+
query_params.get(pagination_params.get("limit_param", "per_page"), 20)
|
320
|
+
)
|
321
|
+
|
322
|
+
# If we have total info, check if more pages exist
|
323
|
+
if total_path:
|
324
|
+
total_items = self._get_nested_value(initial_response, total_path, 0)
|
325
|
+
if not total_items or current_page * per_page >= total_items:
|
326
|
+
return all_items
|
327
|
+
|
328
|
+
elif pagination_type == "cursor":
|
329
|
+
next_cursor_path = pagination_params.get("next_page_path", "meta.next")
|
330
|
+
next_cursor = self._get_nested_value(initial_response, next_cursor_path)
|
331
|
+
if not next_cursor:
|
332
|
+
return all_items
|
333
|
+
|
334
|
+
# TODO: Implement actual pagination fetching for different types
|
335
|
+
# This would involve making additional HTTP requests to fetch subsequent pages
|
336
|
+
# and combining the results, but for brevity we're omitting the implementation
|
337
|
+
|
338
|
+
self.logger.warning("Pagination is not fully implemented in this example")
|
339
|
+
return all_items
|
340
|
+
|
341
|
+
def _get_nested_value(
|
342
|
+
self, obj: Dict[str, Any], path: str, default: Any = None
|
343
|
+
) -> Any:
|
344
|
+
"""Get a nested value from a dictionary using a dot-separated path.
|
345
|
+
|
346
|
+
Args:
|
347
|
+
obj: Dictionary to extract value from
|
348
|
+
path: Dot-separated path to the value (e.g., "meta.pagination.next")
|
349
|
+
default: Value to return if path doesn't exist
|
350
|
+
|
351
|
+
Returns:
|
352
|
+
Value at the specified path or default if not found
|
353
|
+
"""
|
354
|
+
if not path:
|
355
|
+
return obj
|
356
|
+
|
357
|
+
parts = path.split(".")
|
358
|
+
current = obj
|
359
|
+
|
360
|
+
for part in parts:
|
361
|
+
if isinstance(current, dict) and part in current:
|
362
|
+
current = current[part]
|
363
|
+
else:
|
364
|
+
return default
|
365
|
+
|
366
|
+
return current
|
367
|
+
|
368
|
+
def run(self, **kwargs) -> Dict[str, Any]:
|
369
|
+
"""Execute a REST API request.
|
370
|
+
|
371
|
+
Args:
|
372
|
+
base_url (str): Base URL for the REST API
|
373
|
+
resource (str): API resource path template
|
374
|
+
method (str, optional): HTTP method to use
|
375
|
+
path_params (dict, optional): Path parameters to substitute
|
376
|
+
query_params (dict, optional): Query parameters
|
377
|
+
headers (dict, optional): HTTP headers
|
378
|
+
data (dict/str, optional): Request body data
|
379
|
+
version (str, optional): API version
|
380
|
+
timeout (int, optional): Request timeout in seconds
|
381
|
+
verify_ssl (bool, optional): Whether to verify SSL certificates
|
382
|
+
paginate (bool, optional): Whether to handle pagination
|
383
|
+
pagination_params (dict, optional): Pagination configuration
|
384
|
+
retry_count (int, optional): Number of times to retry failed requests
|
385
|
+
retry_backoff (float, optional): Backoff factor for retries
|
386
|
+
|
387
|
+
Returns:
|
388
|
+
Dictionary containing:
|
389
|
+
data: Parsed response data
|
390
|
+
status_code: HTTP status code
|
391
|
+
success: Boolean indicating request success
|
392
|
+
metadata: Additional request/response metadata
|
393
|
+
|
394
|
+
Raises:
|
395
|
+
NodeValidationError: If required parameters are missing or invalid
|
396
|
+
NodeExecutionError: If the request fails or returns an error status
|
397
|
+
"""
|
398
|
+
base_url = kwargs.get("base_url")
|
399
|
+
resource = kwargs.get("resource")
|
400
|
+
method = kwargs.get("method", "GET").upper()
|
401
|
+
path_params = kwargs.get("path_params", {})
|
402
|
+
query_params = kwargs.get("query_params", {})
|
403
|
+
headers = kwargs.get("headers", {})
|
404
|
+
data = kwargs.get("data")
|
405
|
+
version = kwargs.get("version")
|
406
|
+
timeout = kwargs.get("timeout", 30)
|
407
|
+
verify_ssl = kwargs.get("verify_ssl", True)
|
408
|
+
paginate = kwargs.get("paginate", False)
|
409
|
+
pagination_params = kwargs.get("pagination_params")
|
410
|
+
retry_count = kwargs.get("retry_count", 0)
|
411
|
+
retry_backoff = kwargs.get("retry_backoff", 0.5)
|
412
|
+
|
413
|
+
# Build full URL with path parameters
|
414
|
+
url = self._build_url(base_url, resource, path_params, version)
|
415
|
+
|
416
|
+
# Set default Content-Type header for requests with body
|
417
|
+
if (
|
418
|
+
method in ("POST", "PUT", "PATCH")
|
419
|
+
and data
|
420
|
+
and "Content-Type" not in headers
|
421
|
+
):
|
422
|
+
headers["Content-Type"] = "application/json"
|
423
|
+
|
424
|
+
# Accept JSON responses by default
|
425
|
+
if "Accept" not in headers:
|
426
|
+
headers["Accept"] = "application/json"
|
427
|
+
|
428
|
+
# Build HTTP request parameters
|
429
|
+
http_params = {
|
430
|
+
"url": url,
|
431
|
+
"method": method,
|
432
|
+
"headers": headers,
|
433
|
+
"params": query_params,
|
434
|
+
"json_data": data if isinstance(data, dict) else None,
|
435
|
+
"data": data if not isinstance(data, dict) else None,
|
436
|
+
"response_format": "json",
|
437
|
+
"timeout": timeout,
|
438
|
+
"verify_ssl": verify_ssl,
|
439
|
+
"retry_count": retry_count,
|
440
|
+
"retry_backoff": retry_backoff,
|
441
|
+
}
|
442
|
+
|
443
|
+
# Execute the HTTP request
|
444
|
+
self.logger.info(f"Making REST {method} request to {url}")
|
445
|
+
result = self.http_node.run(**http_params)
|
446
|
+
|
447
|
+
# Extract response data
|
448
|
+
response = result["response"]
|
449
|
+
status_code = result["status_code"]
|
450
|
+
success = result["success"]
|
451
|
+
|
452
|
+
# Handle potential error responses
|
453
|
+
if not success:
|
454
|
+
error_message = "Unknown error"
|
455
|
+
if isinstance(response["content"], dict):
|
456
|
+
# Try to extract error message from common formats
|
457
|
+
error_message = (
|
458
|
+
response["content"].get("error", {}).get("message")
|
459
|
+
or response["content"].get("message")
|
460
|
+
or response["content"].get("error")
|
461
|
+
or f"API returned error status: {status_code}"
|
462
|
+
)
|
463
|
+
|
464
|
+
self.logger.error(f"REST API error: {error_message}")
|
465
|
+
|
466
|
+
# Note: We don't raise an exception here, as the caller might want
|
467
|
+
# to handle error responses normally. Instead, we set success=False
|
468
|
+
# and include error details in the response.
|
469
|
+
|
470
|
+
# Handle pagination if requested
|
471
|
+
data = response["content"]
|
472
|
+
if paginate and method == "GET" and success:
|
473
|
+
try:
|
474
|
+
data = self._handle_pagination(data, query_params, pagination_params)
|
475
|
+
except Exception as e:
|
476
|
+
self.logger.warning(f"Pagination handling failed: {str(e)}")
|
477
|
+
|
478
|
+
# Return processed results
|
479
|
+
metadata = {
|
480
|
+
"url": url,
|
481
|
+
"method": method,
|
482
|
+
"response_time_ms": response["response_time_ms"],
|
483
|
+
"headers": response["headers"],
|
484
|
+
}
|
485
|
+
|
486
|
+
return {
|
487
|
+
"data": data,
|
488
|
+
"status_code": status_code,
|
489
|
+
"success": success,
|
490
|
+
"metadata": metadata,
|
491
|
+
}
|
492
|
+
|
493
|
+
|
494
|
+
@register_node(alias="AsyncRESTClient")
|
495
|
+
class AsyncRESTClientNode(AsyncNode):
|
496
|
+
"""Asynchronous node for interacting with REST APIs.
|
497
|
+
|
498
|
+
This node provides the same functionality as RESTClientNode but uses
|
499
|
+
asynchronous I/O for better performance, especially for concurrent requests.
|
500
|
+
|
501
|
+
Design Purpose:
|
502
|
+
- Enable efficient, non-blocking REST API operations in workflows
|
503
|
+
- Provide the same interface as RESTClientNode but with async execution
|
504
|
+
- Support high-throughput API integrations with minimal overhead
|
505
|
+
|
506
|
+
Upstream Usage:
|
507
|
+
- AsyncLocalRuntime: Executes workflow with async support
|
508
|
+
- Specialized async API nodes: May extend this node
|
509
|
+
|
510
|
+
Downstream Consumers:
|
511
|
+
- Data processing nodes: Consume API response data
|
512
|
+
- Decision nodes: Route workflow based on API responses
|
513
|
+
"""
|
514
|
+
|
515
|
+
def __init__(self, **kwargs):
|
516
|
+
"""Initialize the async REST client node.
|
517
|
+
|
518
|
+
Args:
|
519
|
+
Same as RESTClientNode
|
520
|
+
"""
|
521
|
+
super().__init__(**kwargs)
|
522
|
+
self.http_node = AsyncHTTPRequestNode(**kwargs)
|
523
|
+
self.rest_node = RESTClientNode(**kwargs)
|
524
|
+
|
525
|
+
def get_parameters(self) -> Dict[str, NodeParameter]:
|
526
|
+
"""Define the parameters this node accepts.
|
527
|
+
|
528
|
+
Returns:
|
529
|
+
Dictionary of parameter definitions
|
530
|
+
"""
|
531
|
+
# Same parameters as the synchronous version
|
532
|
+
return self.rest_node.get_parameters()
|
533
|
+
|
534
|
+
def get_output_schema(self) -> Dict[str, NodeParameter]:
|
535
|
+
"""Define the output schema for this node.
|
536
|
+
|
537
|
+
Returns:
|
538
|
+
Dictionary of output parameter definitions
|
539
|
+
"""
|
540
|
+
# Same output schema as the synchronous version
|
541
|
+
return self.rest_node.get_output_schema()
|
542
|
+
|
543
|
+
def run(self, **kwargs) -> Dict[str, Any]:
|
544
|
+
"""Synchronous version of the REST request, for compatibility.
|
545
|
+
|
546
|
+
This is implemented for compatibility but users should use the
|
547
|
+
async_run method for better performance.
|
548
|
+
|
549
|
+
Args:
|
550
|
+
Same as RESTClientNode.run()
|
551
|
+
|
552
|
+
Returns:
|
553
|
+
Same as RESTClientNode.run()
|
554
|
+
|
555
|
+
Raises:
|
556
|
+
NodeExecutionError: If the request fails or returns an error status
|
557
|
+
"""
|
558
|
+
# Forward to the synchronous REST node
|
559
|
+
return self.rest_node.run(**kwargs)
|
560
|
+
|
561
|
+
async def async_run(self, **kwargs) -> Dict[str, Any]:
|
562
|
+
"""Execute a REST API request asynchronously.
|
563
|
+
|
564
|
+
Args:
|
565
|
+
Same as RESTClientNode.run()
|
566
|
+
|
567
|
+
Returns:
|
568
|
+
Same as RESTClientNode.run()
|
569
|
+
|
570
|
+
Raises:
|
571
|
+
NodeValidationError: If required parameters are missing or invalid
|
572
|
+
NodeExecutionError: If the request fails or returns an error status
|
573
|
+
"""
|
574
|
+
base_url = kwargs.get("base_url")
|
575
|
+
resource = kwargs.get("resource")
|
576
|
+
method = kwargs.get("method", "GET").upper()
|
577
|
+
path_params = kwargs.get("path_params", {})
|
578
|
+
query_params = kwargs.get("query_params", {})
|
579
|
+
headers = kwargs.get("headers", {})
|
580
|
+
data = kwargs.get("data")
|
581
|
+
version = kwargs.get("version")
|
582
|
+
timeout = kwargs.get("timeout", 30)
|
583
|
+
verify_ssl = kwargs.get("verify_ssl", True)
|
584
|
+
paginate = kwargs.get("paginate", False)
|
585
|
+
pagination_params = kwargs.get("pagination_params")
|
586
|
+
retry_count = kwargs.get("retry_count", 0)
|
587
|
+
retry_backoff = kwargs.get("retry_backoff", 0.5)
|
588
|
+
|
589
|
+
# Build full URL with path parameters (reuse from synchronous version)
|
590
|
+
url = self.rest_node._build_url(base_url, resource, path_params, version)
|
591
|
+
|
592
|
+
# Set default Content-Type header for requests with body
|
593
|
+
if (
|
594
|
+
method in ("POST", "PUT", "PATCH")
|
595
|
+
and data
|
596
|
+
and "Content-Type" not in headers
|
597
|
+
):
|
598
|
+
headers["Content-Type"] = "application/json"
|
599
|
+
|
600
|
+
# Accept JSON responses by default
|
601
|
+
if "Accept" not in headers:
|
602
|
+
headers["Accept"] = "application/json"
|
603
|
+
|
604
|
+
# Build HTTP request parameters
|
605
|
+
http_params = {
|
606
|
+
"url": url,
|
607
|
+
"method": method,
|
608
|
+
"headers": headers,
|
609
|
+
"params": query_params,
|
610
|
+
"json_data": data if isinstance(data, dict) else None,
|
611
|
+
"data": data if not isinstance(data, dict) else None,
|
612
|
+
"response_format": "json",
|
613
|
+
"timeout": timeout,
|
614
|
+
"verify_ssl": verify_ssl,
|
615
|
+
"retry_count": retry_count,
|
616
|
+
"retry_backoff": retry_backoff,
|
617
|
+
}
|
618
|
+
|
619
|
+
# Execute the HTTP request asynchronously
|
620
|
+
self.logger.info(f"Making async REST {method} request to {url}")
|
621
|
+
result = await self.http_node.async_run(**http_params)
|
622
|
+
|
623
|
+
# Extract response data
|
624
|
+
response = result["response"]
|
625
|
+
status_code = result["status_code"]
|
626
|
+
success = result["success"]
|
627
|
+
|
628
|
+
# Handle potential error responses
|
629
|
+
if not success:
|
630
|
+
error_message = "Unknown error"
|
631
|
+
if isinstance(response["content"], dict):
|
632
|
+
# Try to extract error message from common formats
|
633
|
+
error_message = (
|
634
|
+
response["content"].get("error", {}).get("message")
|
635
|
+
or response["content"].get("message")
|
636
|
+
or response["content"].get("error")
|
637
|
+
or f"API returned error status: {status_code}"
|
638
|
+
)
|
639
|
+
|
640
|
+
self.logger.error(f"REST API error: {error_message}")
|
641
|
+
|
642
|
+
# Handle pagination if requested (simplified for now)
|
643
|
+
data = response["content"]
|
644
|
+
if paginate and method == "GET" and success:
|
645
|
+
try:
|
646
|
+
data = self.rest_node._handle_pagination(
|
647
|
+
data, query_params, pagination_params
|
648
|
+
)
|
649
|
+
except Exception as e:
|
650
|
+
self.logger.warning(f"Pagination handling failed: {str(e)}")
|
651
|
+
|
652
|
+
# Return processed results
|
653
|
+
metadata = {
|
654
|
+
"url": url,
|
655
|
+
"method": method,
|
656
|
+
"response_time_ms": response["response_time_ms"],
|
657
|
+
"headers": response["headers"],
|
658
|
+
}
|
659
|
+
|
660
|
+
return {
|
661
|
+
"data": data,
|
662
|
+
"status_code": status_code,
|
663
|
+
"success": success,
|
664
|
+
"metadata": metadata,
|
665
|
+
}
|