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.
Files changed (69) hide show
  1. kailash/__init__.py +31 -0
  2. kailash/__main__.py +11 -0
  3. kailash/cli/__init__.py +5 -0
  4. kailash/cli/commands.py +563 -0
  5. kailash/manifest.py +778 -0
  6. kailash/nodes/__init__.py +23 -0
  7. kailash/nodes/ai/__init__.py +26 -0
  8. kailash/nodes/ai/agents.py +417 -0
  9. kailash/nodes/ai/models.py +488 -0
  10. kailash/nodes/api/__init__.py +52 -0
  11. kailash/nodes/api/auth.py +567 -0
  12. kailash/nodes/api/graphql.py +480 -0
  13. kailash/nodes/api/http.py +598 -0
  14. kailash/nodes/api/rate_limiting.py +572 -0
  15. kailash/nodes/api/rest.py +665 -0
  16. kailash/nodes/base.py +1032 -0
  17. kailash/nodes/base_async.py +128 -0
  18. kailash/nodes/code/__init__.py +32 -0
  19. kailash/nodes/code/python.py +1021 -0
  20. kailash/nodes/data/__init__.py +125 -0
  21. kailash/nodes/data/readers.py +496 -0
  22. kailash/nodes/data/sharepoint_graph.py +623 -0
  23. kailash/nodes/data/sql.py +380 -0
  24. kailash/nodes/data/streaming.py +1168 -0
  25. kailash/nodes/data/vector_db.py +964 -0
  26. kailash/nodes/data/writers.py +529 -0
  27. kailash/nodes/logic/__init__.py +6 -0
  28. kailash/nodes/logic/async_operations.py +702 -0
  29. kailash/nodes/logic/operations.py +551 -0
  30. kailash/nodes/transform/__init__.py +5 -0
  31. kailash/nodes/transform/processors.py +379 -0
  32. kailash/runtime/__init__.py +6 -0
  33. kailash/runtime/async_local.py +356 -0
  34. kailash/runtime/docker.py +697 -0
  35. kailash/runtime/local.py +434 -0
  36. kailash/runtime/parallel.py +557 -0
  37. kailash/runtime/runner.py +110 -0
  38. kailash/runtime/testing.py +347 -0
  39. kailash/sdk_exceptions.py +307 -0
  40. kailash/tracking/__init__.py +7 -0
  41. kailash/tracking/manager.py +885 -0
  42. kailash/tracking/metrics_collector.py +342 -0
  43. kailash/tracking/models.py +535 -0
  44. kailash/tracking/storage/__init__.py +0 -0
  45. kailash/tracking/storage/base.py +113 -0
  46. kailash/tracking/storage/database.py +619 -0
  47. kailash/tracking/storage/filesystem.py +543 -0
  48. kailash/utils/__init__.py +0 -0
  49. kailash/utils/export.py +924 -0
  50. kailash/utils/templates.py +680 -0
  51. kailash/visualization/__init__.py +62 -0
  52. kailash/visualization/api.py +732 -0
  53. kailash/visualization/dashboard.py +951 -0
  54. kailash/visualization/performance.py +808 -0
  55. kailash/visualization/reports.py +1471 -0
  56. kailash/workflow/__init__.py +15 -0
  57. kailash/workflow/builder.py +245 -0
  58. kailash/workflow/graph.py +827 -0
  59. kailash/workflow/mermaid_visualizer.py +628 -0
  60. kailash/workflow/mock_registry.py +63 -0
  61. kailash/workflow/runner.py +302 -0
  62. kailash/workflow/state.py +238 -0
  63. kailash/workflow/visualization.py +588 -0
  64. kailash-0.1.0.dist-info/METADATA +710 -0
  65. kailash-0.1.0.dist-info/RECORD +69 -0
  66. kailash-0.1.0.dist-info/WHEEL +5 -0
  67. kailash-0.1.0.dist-info/entry_points.txt +2 -0
  68. kailash-0.1.0.dist-info/licenses/LICENSE +21 -0
  69. 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
+ }