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,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