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/rest.py
CHANGED
@@ -5,9 +5,9 @@ synchronous and asynchronous modes. These nodes build on the base HTTP nodes
|
|
5
5
|
to provide a more convenient interface for working with REST APIs.
|
6
6
|
|
7
7
|
Key Components:
|
8
|
-
|
9
|
-
|
10
|
-
|
8
|
+
* RESTClientNode: Synchronous REST API client
|
9
|
+
* AsyncRESTClientNode: Asynchronous REST API client
|
10
|
+
* Resource path builders and response handlers
|
11
11
|
"""
|
12
12
|
|
13
13
|
from typing import Any, Dict, List, Optional
|
@@ -18,31 +18,31 @@ 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 RESTClientNode(Node):
|
23
23
|
"""Node for interacting with REST APIs.
|
24
24
|
|
25
25
|
This node provides a higher-level interface for interacting with REST APIs,
|
26
26
|
with built-in support for:
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
32
|
|
33
33
|
Design Purpose:
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
38
|
|
39
39
|
Upstream Usage:
|
40
|
-
|
41
|
-
|
40
|
+
* Workflow: Creates and configures for specific REST APIs
|
41
|
+
* API integration workflows: Uses for external service integration
|
42
42
|
|
43
43
|
Downstream Consumers:
|
44
|
-
|
45
|
-
|
44
|
+
* Data processing nodes: Consume API response data
|
45
|
+
* Custom nodes: Process API-specific data formats
|
46
46
|
"""
|
47
47
|
|
48
48
|
def __init__(self, **kwargs):
|
@@ -60,7 +60,7 @@ class RESTClientNode(Node):
|
|
60
60
|
**kwargs: Additional parameters passed to base Node
|
61
61
|
"""
|
62
62
|
super().__init__(**kwargs)
|
63
|
-
self.http_node = HTTPRequestNode(
|
63
|
+
self.http_node = HTTPRequestNode(url="")
|
64
64
|
|
65
65
|
def get_parameters(self) -> Dict[str, NodeParameter]:
|
66
66
|
"""Define the parameters this node accepts.
|
@@ -72,13 +72,13 @@ class RESTClientNode(Node):
|
|
72
72
|
"base_url": NodeParameter(
|
73
73
|
name="base_url",
|
74
74
|
type=str,
|
75
|
-
required=
|
75
|
+
required=False,
|
76
76
|
description="Base URL for the REST API (e.g., https://api.example.com)",
|
77
77
|
),
|
78
78
|
"resource": NodeParameter(
|
79
79
|
name="resource",
|
80
80
|
type=str,
|
81
|
-
required=
|
81
|
+
required=False,
|
82
82
|
description="API resource path (e.g., 'users' or 'products/{id}')",
|
83
83
|
),
|
84
84
|
"method": NodeParameter(
|
@@ -165,6 +165,41 @@ class RESTClientNode(Node):
|
|
165
165
|
default=0.5,
|
166
166
|
description="Backoff factor for retries",
|
167
167
|
),
|
168
|
+
"auth_type": NodeParameter(
|
169
|
+
name="auth_type",
|
170
|
+
type=str,
|
171
|
+
required=False,
|
172
|
+
default=None,
|
173
|
+
description="Authentication type: bearer, basic, api_key, oauth2",
|
174
|
+
),
|
175
|
+
"auth_token": NodeParameter(
|
176
|
+
name="auth_token",
|
177
|
+
type=str,
|
178
|
+
required=False,
|
179
|
+
default=None,
|
180
|
+
description="Authentication token/key for bearer, api_key, or oauth2",
|
181
|
+
),
|
182
|
+
"auth_username": NodeParameter(
|
183
|
+
name="auth_username",
|
184
|
+
type=str,
|
185
|
+
required=False,
|
186
|
+
default=None,
|
187
|
+
description="Username for basic authentication",
|
188
|
+
),
|
189
|
+
"auth_password": NodeParameter(
|
190
|
+
name="auth_password",
|
191
|
+
type=str,
|
192
|
+
required=False,
|
193
|
+
default=None,
|
194
|
+
description="Password for basic authentication",
|
195
|
+
),
|
196
|
+
"api_key_header": NodeParameter(
|
197
|
+
name="api_key_header",
|
198
|
+
type=str,
|
199
|
+
required=False,
|
200
|
+
default="X-API-Key",
|
201
|
+
description="Header name for API key authentication",
|
202
|
+
),
|
168
203
|
}
|
169
204
|
|
170
205
|
def get_output_schema(self) -> Dict[str, NodeParameter]:
|
@@ -271,9 +306,9 @@ class RESTClientNode(Node):
|
|
271
306
|
"""Handle pagination for REST API responses.
|
272
307
|
|
273
308
|
This method supports common pagination patterns:
|
274
|
-
|
275
|
-
|
276
|
-
|
309
|
+
* Page-based: ?page=1&per_page=100
|
310
|
+
* Offset-based: ?offset=0&limit=100
|
311
|
+
* Cursor-based: ?cursor=abc123
|
277
312
|
|
278
313
|
Args:
|
279
314
|
initial_response: Response from the first API call
|
@@ -383,6 +418,11 @@ class RESTClientNode(Node):
|
|
383
418
|
pagination_params (dict, optional): Pagination configuration
|
384
419
|
retry_count (int, optional): Number of times to retry failed requests
|
385
420
|
retry_backoff (float, optional): Backoff factor for retries
|
421
|
+
auth_type (str, optional): Authentication type (bearer, basic, api_key, oauth2)
|
422
|
+
auth_token (str, optional): Authentication token/key
|
423
|
+
auth_username (str, optional): Username for basic auth
|
424
|
+
auth_password (str, optional): Password for basic auth
|
425
|
+
api_key_header (str, optional): Header name for API key auth
|
386
426
|
|
387
427
|
Returns:
|
388
428
|
Dictionary containing:
|
@@ -409,6 +449,12 @@ class RESTClientNode(Node):
|
|
409
449
|
pagination_params = kwargs.get("pagination_params")
|
410
450
|
retry_count = kwargs.get("retry_count", 0)
|
411
451
|
retry_backoff = kwargs.get("retry_backoff", 0.5)
|
452
|
+
# Authentication parameters
|
453
|
+
auth_type = kwargs.get("auth_type")
|
454
|
+
auth_token = kwargs.get("auth_token")
|
455
|
+
auth_username = kwargs.get("auth_username")
|
456
|
+
auth_password = kwargs.get("auth_password")
|
457
|
+
api_key_header = kwargs.get("api_key_header", "X-API-Key")
|
412
458
|
|
413
459
|
# Build full URL with path parameters
|
414
460
|
url = self._build_url(base_url, resource, path_params, version)
|
@@ -438,6 +484,11 @@ class RESTClientNode(Node):
|
|
438
484
|
"verify_ssl": verify_ssl,
|
439
485
|
"retry_count": retry_count,
|
440
486
|
"retry_backoff": retry_backoff,
|
487
|
+
"auth_type": auth_type,
|
488
|
+
"auth_token": auth_token,
|
489
|
+
"auth_username": auth_username,
|
490
|
+
"auth_password": auth_password,
|
491
|
+
"api_key_header": api_key_header,
|
441
492
|
}
|
442
493
|
|
443
494
|
# Execute the HTTP request
|
@@ -445,30 +496,58 @@ class RESTClientNode(Node):
|
|
445
496
|
result = self.http_node.run(**http_params)
|
446
497
|
|
447
498
|
# Extract response data
|
448
|
-
response = result
|
449
|
-
status_code = result
|
450
|
-
success = result
|
499
|
+
response = result.get("response")
|
500
|
+
status_code = result.get("status_code")
|
501
|
+
success = result.get("success", False)
|
451
502
|
|
452
503
|
# Handle potential error responses
|
453
504
|
if not success:
|
454
|
-
error_message = "Unknown error"
|
455
|
-
|
505
|
+
error_message = result.get("error", "Unknown error")
|
506
|
+
|
507
|
+
# If we have a response object, try to extract error details
|
508
|
+
if response and isinstance(response.get("content"), dict):
|
456
509
|
# Try to extract error message from common formats
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
)
|
510
|
+
content = response["content"]
|
511
|
+
# Handle case where error is a string or dict
|
512
|
+
error_value = content.get("error")
|
513
|
+
if isinstance(error_value, dict):
|
514
|
+
error_message = error_value.get("message") or error_message
|
515
|
+
elif isinstance(error_value, str):
|
516
|
+
error_message = error_value
|
517
|
+
# Check for message at root level
|
518
|
+
if not error_message or error_message == result.get(
|
519
|
+
"error", "Unknown error"
|
520
|
+
):
|
521
|
+
error_message = content.get("message") or error_message
|
522
|
+
|
523
|
+
# If we have a status code, include it
|
524
|
+
if status_code:
|
525
|
+
error_message = f"{error_message} (status: {status_code})"
|
463
526
|
|
464
527
|
self.logger.error(f"REST API error: {error_message}")
|
465
528
|
|
529
|
+
# Return error response with recovery suggestions if available
|
530
|
+
error_result = {
|
531
|
+
"data": None,
|
532
|
+
"status_code": status_code,
|
533
|
+
"success": False,
|
534
|
+
"error": error_message,
|
535
|
+
"error_type": result.get("error_type", "APIError"),
|
536
|
+
"metadata": {},
|
537
|
+
}
|
538
|
+
|
539
|
+
# Include recovery suggestions if available
|
540
|
+
if "recovery_suggestions" in result:
|
541
|
+
error_result["recovery_suggestions"] = result["recovery_suggestions"]
|
542
|
+
|
543
|
+
return error_result
|
544
|
+
|
466
545
|
# Note: We don't raise an exception here, as the caller might want
|
467
546
|
# to handle error responses normally. Instead, we set success=False
|
468
547
|
# and include error details in the response.
|
469
548
|
|
470
549
|
# Handle pagination if requested
|
471
|
-
data = response["content"]
|
550
|
+
data = response["content"] if response else None
|
472
551
|
if paginate and method == "GET" and success:
|
473
552
|
try:
|
474
553
|
data = self._handle_pagination(data, query_params, pagination_params)
|
@@ -479,10 +558,15 @@ class RESTClientNode(Node):
|
|
479
558
|
metadata = {
|
480
559
|
"url": url,
|
481
560
|
"method": method,
|
482
|
-
"response_time_ms": response["response_time_ms"],
|
483
|
-
"headers": response["headers"],
|
484
561
|
}
|
485
562
|
|
563
|
+
# Add response metadata if available
|
564
|
+
if response:
|
565
|
+
metadata["response_time_ms"] = response.get("response_time_ms", 0)
|
566
|
+
metadata["headers"] = response.get("headers", {})
|
567
|
+
# Extract additional metadata
|
568
|
+
metadata.update(self._extract_metadata(response))
|
569
|
+
|
486
570
|
return {
|
487
571
|
"data": data,
|
488
572
|
"status_code": status_code,
|
@@ -490,8 +574,293 @@ class RESTClientNode(Node):
|
|
490
574
|
"metadata": metadata,
|
491
575
|
}
|
492
576
|
|
577
|
+
# Convenience methods for CRUD operations
|
578
|
+
def get(
|
579
|
+
self, base_url: str, resource: str, resource_id: Optional[str] = None, **kwargs
|
580
|
+
) -> Dict[str, Any]:
|
581
|
+
"""GET a resource or list of resources.
|
582
|
+
|
583
|
+
Args:
|
584
|
+
base_url: Base API URL
|
585
|
+
resource: Resource name (e.g., 'users', 'posts')
|
586
|
+
resource_id: Optional resource ID for single resource retrieval
|
587
|
+
**kwargs: Additional parameters (query_params, headers, etc.)
|
588
|
+
|
589
|
+
Returns:
|
590
|
+
API response dictionary
|
591
|
+
"""
|
592
|
+
if resource_id:
|
593
|
+
# Single resource retrieval
|
594
|
+
path_params = kwargs.pop("path_params", {})
|
595
|
+
path_params["id"] = resource_id
|
596
|
+
resource_path = f"{resource}/{{id}}"
|
597
|
+
else:
|
598
|
+
# List resources
|
599
|
+
resource_path = resource
|
600
|
+
path_params = kwargs.pop("path_params", {})
|
601
|
+
|
602
|
+
return self.run(
|
603
|
+
base_url=base_url,
|
604
|
+
resource=resource_path,
|
605
|
+
method="GET",
|
606
|
+
path_params=path_params,
|
607
|
+
**kwargs,
|
608
|
+
)
|
609
|
+
|
610
|
+
def create(
|
611
|
+
self, base_url: str, resource: str, data: Dict[str, Any], **kwargs
|
612
|
+
) -> Dict[str, Any]:
|
613
|
+
"""CREATE (POST) a new resource.
|
614
|
+
|
615
|
+
Args:
|
616
|
+
base_url: Base API URL
|
617
|
+
resource: Resource name (e.g., 'users', 'posts')
|
618
|
+
data: Resource data to create
|
619
|
+
**kwargs: Additional parameters (headers, etc.)
|
620
|
+
|
621
|
+
Returns:
|
622
|
+
API response dictionary
|
623
|
+
"""
|
624
|
+
return self.run(
|
625
|
+
base_url=base_url, resource=resource, method="POST", data=data, **kwargs
|
626
|
+
)
|
627
|
+
|
628
|
+
def update(
|
629
|
+
self,
|
630
|
+
base_url: str,
|
631
|
+
resource: str,
|
632
|
+
resource_id: str,
|
633
|
+
data: Dict[str, Any],
|
634
|
+
partial: bool = False,
|
635
|
+
**kwargs,
|
636
|
+
) -> Dict[str, Any]:
|
637
|
+
"""UPDATE (PUT/PATCH) an existing resource.
|
638
|
+
|
639
|
+
Args:
|
640
|
+
base_url: Base API URL
|
641
|
+
resource: Resource name (e.g., 'users', 'posts')
|
642
|
+
resource_id: Resource ID to update
|
643
|
+
data: Updated resource data
|
644
|
+
partial: If True, use PATCH for partial update; if False, use PUT
|
645
|
+
**kwargs: Additional parameters (headers, etc.)
|
646
|
+
|
647
|
+
Returns:
|
648
|
+
API response dictionary
|
649
|
+
"""
|
650
|
+
path_params = kwargs.pop("path_params", {})
|
651
|
+
path_params["id"] = resource_id
|
652
|
+
|
653
|
+
return self.run(
|
654
|
+
base_url=base_url,
|
655
|
+
resource=f"{resource}/{{id}}",
|
656
|
+
method="PATCH" if partial else "PUT",
|
657
|
+
path_params=path_params,
|
658
|
+
data=data,
|
659
|
+
**kwargs,
|
660
|
+
)
|
661
|
+
|
662
|
+
def delete(
|
663
|
+
self, base_url: str, resource: str, resource_id: str, **kwargs
|
664
|
+
) -> Dict[str, Any]:
|
665
|
+
"""DELETE a resource.
|
666
|
+
|
667
|
+
Args:
|
668
|
+
base_url: Base API URL
|
669
|
+
resource: Resource name (e.g., 'users', 'posts')
|
670
|
+
resource_id: Resource ID to delete
|
671
|
+
**kwargs: Additional parameters (headers, etc.)
|
672
|
+
|
673
|
+
Returns:
|
674
|
+
API response dictionary
|
675
|
+
"""
|
676
|
+
path_params = kwargs.pop("path_params", {})
|
677
|
+
path_params["id"] = resource_id
|
678
|
+
|
679
|
+
return self.run(
|
680
|
+
base_url=base_url,
|
681
|
+
resource=f"{resource}/{{id}}",
|
682
|
+
method="DELETE",
|
683
|
+
path_params=path_params,
|
684
|
+
**kwargs,
|
685
|
+
)
|
686
|
+
|
687
|
+
def _extract_metadata(self, response: Dict[str, Any]) -> Dict[str, Any]:
|
688
|
+
"""Extract additional metadata from response.
|
689
|
+
|
690
|
+
Args:
|
691
|
+
response: HTTP response dictionary
|
692
|
+
|
693
|
+
Returns:
|
694
|
+
Dictionary with extracted metadata
|
695
|
+
"""
|
696
|
+
metadata = {}
|
697
|
+
headers = response.get("headers", {})
|
698
|
+
|
699
|
+
# Extract rate limit information
|
700
|
+
rate_limit = self._extract_rate_limit_metadata(headers)
|
701
|
+
if rate_limit:
|
702
|
+
metadata["rate_limit"] = rate_limit
|
703
|
+
|
704
|
+
# Extract pagination metadata
|
705
|
+
pagination = self._extract_pagination_metadata(
|
706
|
+
headers, response.get("content", {})
|
707
|
+
)
|
708
|
+
if pagination:
|
709
|
+
metadata["pagination"] = pagination
|
710
|
+
|
711
|
+
# Extract HATEOAS links
|
712
|
+
links = self._extract_links(response.get("content", {}))
|
713
|
+
if links:
|
714
|
+
metadata["links"] = links
|
715
|
+
|
716
|
+
return metadata
|
717
|
+
|
718
|
+
def _extract_rate_limit_metadata(
|
719
|
+
self, headers: Dict[str, str]
|
720
|
+
) -> Optional[Dict[str, Any]]:
|
721
|
+
"""Extract rate limiting information from response headers.
|
722
|
+
|
723
|
+
Args:
|
724
|
+
headers: Response headers dictionary
|
725
|
+
|
726
|
+
Returns:
|
727
|
+
Rate limit metadata or None if not found
|
728
|
+
"""
|
729
|
+
rate_limit = {}
|
730
|
+
|
731
|
+
# Common rate limit headers
|
732
|
+
rate_limit_headers = {
|
733
|
+
"X-RateLimit-Limit": "limit",
|
734
|
+
"X-RateLimit-Remaining": "remaining",
|
735
|
+
"X-RateLimit-Reset": "reset",
|
736
|
+
"X-Rate-Limit-Limit": "limit",
|
737
|
+
"X-Rate-Limit-Remaining": "remaining",
|
738
|
+
"X-Rate-Limit-Reset": "reset",
|
739
|
+
"RateLimit-Limit": "limit",
|
740
|
+
"RateLimit-Remaining": "remaining",
|
741
|
+
"RateLimit-Reset": "reset",
|
742
|
+
}
|
743
|
+
|
744
|
+
for header, key in rate_limit_headers.items():
|
745
|
+
value = headers.get(header) or headers.get(header.lower())
|
746
|
+
if value:
|
747
|
+
try:
|
748
|
+
rate_limit[key] = int(value)
|
749
|
+
except ValueError:
|
750
|
+
rate_limit[key] = value
|
751
|
+
|
752
|
+
return rate_limit if rate_limit else None
|
493
753
|
|
494
|
-
|
754
|
+
def _extract_pagination_metadata(
|
755
|
+
self, headers: Dict[str, str], content: Any
|
756
|
+
) -> Optional[Dict[str, Any]]:
|
757
|
+
"""Extract pagination information from headers and response body.
|
758
|
+
|
759
|
+
Args:
|
760
|
+
headers: Response headers dictionary
|
761
|
+
content: Response body content
|
762
|
+
|
763
|
+
Returns:
|
764
|
+
Pagination metadata or None if not found
|
765
|
+
"""
|
766
|
+
pagination = {}
|
767
|
+
|
768
|
+
# Extract from headers (Link header parsing)
|
769
|
+
link_header = headers.get("Link") or headers.get("link")
|
770
|
+
if link_header:
|
771
|
+
links = self._parse_link_header(link_header)
|
772
|
+
pagination.update(links)
|
773
|
+
|
774
|
+
# Extract from response body (common patterns)
|
775
|
+
if isinstance(content, dict):
|
776
|
+
# Look for common pagination fields
|
777
|
+
pagination_fields = {
|
778
|
+
"page": ["page", "current_page", "pageNumber"],
|
779
|
+
"per_page": ["per_page", "page_size", "pageSize", "limit"],
|
780
|
+
"total": ["total", "totalCount", "total_count", "totalRecords"],
|
781
|
+
"total_pages": ["total_pages", "totalPages", "pageCount"],
|
782
|
+
"has_next": ["has_next", "hasNext", "has_more", "hasMore"],
|
783
|
+
"has_prev": ["has_prev", "hasPrev", "has_previous", "hasPrevious"],
|
784
|
+
}
|
785
|
+
|
786
|
+
for key, fields in pagination_fields.items():
|
787
|
+
for field in fields:
|
788
|
+
# Check in root
|
789
|
+
if field in content:
|
790
|
+
pagination[key] = content[field]
|
791
|
+
break
|
792
|
+
# Check in meta/metadata
|
793
|
+
meta = content.get("meta") or content.get("metadata", {})
|
794
|
+
if isinstance(meta, dict) and field in meta:
|
795
|
+
pagination[key] = meta[field]
|
796
|
+
break
|
797
|
+
|
798
|
+
return pagination if pagination else None
|
799
|
+
|
800
|
+
def _parse_link_header(self, link_header: str) -> Dict[str, str]:
|
801
|
+
"""Parse Link header for pagination URLs.
|
802
|
+
|
803
|
+
Args:
|
804
|
+
link_header: Link header value
|
805
|
+
|
806
|
+
Returns:
|
807
|
+
Dictionary of rel -> URL mappings
|
808
|
+
"""
|
809
|
+
links = {}
|
810
|
+
|
811
|
+
# Parse Link header format: <url>; rel="next", <url>; rel="prev"
|
812
|
+
for link in link_header.split(","):
|
813
|
+
link = link.strip()
|
814
|
+
if ";" in link:
|
815
|
+
url_part, rel_part = link.split(";", 1)
|
816
|
+
url = url_part.strip("<>")
|
817
|
+
rel_match = rel_part.split("=", 1)
|
818
|
+
if len(rel_match) == 2:
|
819
|
+
rel = rel_match[1].strip("\"'")
|
820
|
+
links[rel] = url
|
821
|
+
|
822
|
+
return links
|
823
|
+
|
824
|
+
def _extract_links(self, content: Any) -> Optional[Dict[str, Any]]:
|
825
|
+
"""Extract HATEOAS links from response content.
|
826
|
+
|
827
|
+
Args:
|
828
|
+
content: Response body content
|
829
|
+
|
830
|
+
Returns:
|
831
|
+
Links dictionary or None if not found
|
832
|
+
"""
|
833
|
+
if not isinstance(content, dict):
|
834
|
+
return None
|
835
|
+
|
836
|
+
links = {}
|
837
|
+
|
838
|
+
# Check common link locations
|
839
|
+
link_fields = ["links", "_links", "link", "href"]
|
840
|
+
|
841
|
+
for field in link_fields:
|
842
|
+
if field in content:
|
843
|
+
link_data = content[field]
|
844
|
+
if isinstance(link_data, dict):
|
845
|
+
# HAL format: {"self": {"href": "..."}, "next": {"href": "..."}}
|
846
|
+
for rel, link_obj in link_data.items():
|
847
|
+
if isinstance(link_obj, dict) and "href" in link_obj:
|
848
|
+
links[rel] = link_obj["href"]
|
849
|
+
elif isinstance(link_obj, str):
|
850
|
+
links[rel] = link_obj
|
851
|
+
elif isinstance(link_data, list):
|
852
|
+
# Array of link objects
|
853
|
+
for link_obj in link_data:
|
854
|
+
if isinstance(link_obj, dict):
|
855
|
+
rel = link_obj.get("rel", "related")
|
856
|
+
href = link_obj.get("href") or link_obj.get("url")
|
857
|
+
if href:
|
858
|
+
links[rel] = href
|
859
|
+
|
860
|
+
return links if links else None
|
861
|
+
|
862
|
+
|
863
|
+
@register_node()
|
495
864
|
class AsyncRESTClientNode(AsyncNode):
|
496
865
|
"""Asynchronous node for interacting with REST APIs.
|
497
866
|
|
@@ -499,17 +868,17 @@ class AsyncRESTClientNode(AsyncNode):
|
|
499
868
|
asynchronous I/O for better performance, especially for concurrent requests.
|
500
869
|
|
501
870
|
Design Purpose:
|
502
|
-
|
503
|
-
|
504
|
-
|
871
|
+
* Enable efficient, non-blocking REST API operations in workflows
|
872
|
+
* Provide the same interface as RESTClientNode but with async execution
|
873
|
+
* Support high-throughput API integrations with minimal overhead
|
505
874
|
|
506
875
|
Upstream Usage:
|
507
|
-
|
508
|
-
|
876
|
+
* AsyncLocalRuntime: Executes workflow with async support
|
877
|
+
* Specialized async API nodes: May extend this node
|
509
878
|
|
510
879
|
Downstream Consumers:
|
511
|
-
|
512
|
-
|
880
|
+
* Data processing nodes: Consume API response data
|
881
|
+
* Decision nodes: Route workflow based on API responses
|
513
882
|
"""
|
514
883
|
|
515
884
|
def __init__(self, **kwargs):
|
@@ -585,6 +954,12 @@ class AsyncRESTClientNode(AsyncNode):
|
|
585
954
|
pagination_params = kwargs.get("pagination_params")
|
586
955
|
retry_count = kwargs.get("retry_count", 0)
|
587
956
|
retry_backoff = kwargs.get("retry_backoff", 0.5)
|
957
|
+
# Authentication parameters
|
958
|
+
auth_type = kwargs.get("auth_type")
|
959
|
+
auth_token = kwargs.get("auth_token")
|
960
|
+
auth_username = kwargs.get("auth_username")
|
961
|
+
auth_password = kwargs.get("auth_password")
|
962
|
+
api_key_header = kwargs.get("api_key_header", "X-API-Key")
|
588
963
|
|
589
964
|
# Build full URL with path parameters (reuse from synchronous version)
|
590
965
|
url = self.rest_node._build_url(base_url, resource, path_params, version)
|
@@ -614,6 +989,11 @@ class AsyncRESTClientNode(AsyncNode):
|
|
614
989
|
"verify_ssl": verify_ssl,
|
615
990
|
"retry_count": retry_count,
|
616
991
|
"retry_backoff": retry_backoff,
|
992
|
+
"auth_type": auth_type,
|
993
|
+
"auth_token": auth_token,
|
994
|
+
"auth_username": auth_username,
|
995
|
+
"auth_password": auth_password,
|
996
|
+
"api_key_header": api_key_header,
|
617
997
|
}
|
618
998
|
|
619
999
|
# Execute the HTTP request asynchronously
|
@@ -621,24 +1001,52 @@ class AsyncRESTClientNode(AsyncNode):
|
|
621
1001
|
result = await self.http_node.async_run(**http_params)
|
622
1002
|
|
623
1003
|
# Extract response data
|
624
|
-
response = result
|
625
|
-
status_code = result
|
626
|
-
success = result
|
1004
|
+
response = result.get("response")
|
1005
|
+
status_code = result.get("status_code")
|
1006
|
+
success = result.get("success", False)
|
627
1007
|
|
628
1008
|
# Handle potential error responses
|
629
1009
|
if not success:
|
630
|
-
error_message = "Unknown error"
|
631
|
-
|
1010
|
+
error_message = result.get("error", "Unknown error")
|
1011
|
+
|
1012
|
+
# If we have a response object, try to extract error details
|
1013
|
+
if response and isinstance(response.get("content"), dict):
|
632
1014
|
# Try to extract error message from common formats
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
)
|
1015
|
+
content = response["content"]
|
1016
|
+
# Handle case where error is a string or dict
|
1017
|
+
error_value = content.get("error")
|
1018
|
+
if isinstance(error_value, dict):
|
1019
|
+
error_message = error_value.get("message") or error_message
|
1020
|
+
elif isinstance(error_value, str):
|
1021
|
+
error_message = error_value
|
1022
|
+
# Check for message at root level
|
1023
|
+
if not error_message or error_message == result.get(
|
1024
|
+
"error", "Unknown error"
|
1025
|
+
):
|
1026
|
+
error_message = content.get("message") or error_message
|
1027
|
+
|
1028
|
+
# If we have a status code, include it
|
1029
|
+
if status_code:
|
1030
|
+
error_message = f"{error_message} (status: {status_code})"
|
639
1031
|
|
640
1032
|
self.logger.error(f"REST API error: {error_message}")
|
641
1033
|
|
1034
|
+
# Return error response with recovery suggestions if available
|
1035
|
+
error_result = {
|
1036
|
+
"data": None,
|
1037
|
+
"status_code": status_code,
|
1038
|
+
"success": False,
|
1039
|
+
"error": error_message,
|
1040
|
+
"error_type": result.get("error_type", "APIError"),
|
1041
|
+
"metadata": {},
|
1042
|
+
}
|
1043
|
+
|
1044
|
+
# Include recovery suggestions if available
|
1045
|
+
if "recovery_suggestions" in result:
|
1046
|
+
error_result["recovery_suggestions"] = result["recovery_suggestions"]
|
1047
|
+
|
1048
|
+
return error_result
|
1049
|
+
|
642
1050
|
# Handle pagination if requested (simplified for now)
|
643
1051
|
data = response["content"]
|
644
1052
|
if paginate and method == "GET" and success:
|