kailash 0.6.6__py3-none-any.whl → 0.8.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 +35 -5
- kailash/access_control.py +64 -46
- kailash/adapters/__init__.py +5 -0
- kailash/adapters/mcp_platform_adapter.py +273 -0
- kailash/api/workflow_api.py +34 -3
- kailash/channels/__init__.py +21 -0
- kailash/channels/api_channel.py +409 -0
- kailash/channels/base.py +271 -0
- kailash/channels/cli_channel.py +661 -0
- kailash/channels/event_router.py +496 -0
- kailash/channels/mcp_channel.py +648 -0
- kailash/channels/session.py +423 -0
- kailash/mcp_server/discovery.py +57 -18
- kailash/middleware/communication/api_gateway.py +23 -3
- kailash/middleware/communication/realtime.py +83 -0
- kailash/middleware/core/agent_ui.py +1 -1
- kailash/middleware/gateway/storage_backends.py +393 -0
- kailash/middleware/mcp/enhanced_server.py +22 -16
- kailash/nexus/__init__.py +21 -0
- kailash/nexus/cli/__init__.py +5 -0
- kailash/nexus/cli/__main__.py +6 -0
- kailash/nexus/cli/main.py +176 -0
- kailash/nexus/factory.py +413 -0
- kailash/nexus/gateway.py +545 -0
- kailash/nodes/__init__.py +8 -5
- kailash/nodes/ai/iterative_llm_agent.py +988 -17
- kailash/nodes/ai/llm_agent.py +29 -9
- kailash/nodes/api/__init__.py +2 -2
- kailash/nodes/api/monitoring.py +1 -1
- kailash/nodes/base.py +29 -5
- kailash/nodes/base_async.py +54 -14
- kailash/nodes/code/async_python.py +1 -1
- kailash/nodes/code/python.py +50 -6
- kailash/nodes/data/async_sql.py +90 -0
- kailash/nodes/data/bulk_operations.py +939 -0
- kailash/nodes/data/query_builder.py +373 -0
- kailash/nodes/data/query_cache.py +512 -0
- kailash/nodes/monitoring/__init__.py +10 -0
- kailash/nodes/monitoring/deadlock_detector.py +964 -0
- kailash/nodes/monitoring/performance_anomaly.py +1078 -0
- kailash/nodes/monitoring/race_condition_detector.py +1151 -0
- kailash/nodes/monitoring/transaction_metrics.py +790 -0
- kailash/nodes/monitoring/transaction_monitor.py +931 -0
- kailash/nodes/security/behavior_analysis.py +414 -0
- kailash/nodes/system/__init__.py +17 -0
- kailash/nodes/system/command_parser.py +820 -0
- kailash/nodes/transaction/__init__.py +48 -0
- kailash/nodes/transaction/distributed_transaction_manager.py +983 -0
- kailash/nodes/transaction/saga_coordinator.py +652 -0
- kailash/nodes/transaction/saga_state_storage.py +411 -0
- kailash/nodes/transaction/saga_step.py +467 -0
- kailash/nodes/transaction/transaction_context.py +756 -0
- kailash/nodes/transaction/two_phase_commit.py +978 -0
- kailash/nodes/transform/processors.py +17 -1
- kailash/nodes/validation/__init__.py +21 -0
- kailash/nodes/validation/test_executor.py +532 -0
- kailash/nodes/validation/validation_nodes.py +447 -0
- kailash/resources/factory.py +1 -1
- kailash/runtime/access_controlled.py +9 -7
- kailash/runtime/async_local.py +84 -21
- kailash/runtime/local.py +21 -2
- kailash/runtime/parameter_injector.py +187 -31
- kailash/runtime/runner.py +6 -4
- kailash/runtime/testing.py +1 -1
- kailash/security.py +22 -3
- kailash/servers/__init__.py +32 -0
- kailash/servers/durable_workflow_server.py +430 -0
- kailash/servers/enterprise_workflow_server.py +522 -0
- kailash/servers/gateway.py +183 -0
- kailash/servers/workflow_server.py +293 -0
- kailash/utils/data_validation.py +192 -0
- kailash/workflow/builder.py +382 -15
- kailash/workflow/cyclic_runner.py +102 -10
- kailash/workflow/validation.py +144 -8
- kailash/workflow/visualization.py +99 -27
- {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/METADATA +3 -2
- {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/RECORD +81 -40
- kailash/workflow/builder_improvements.py +0 -207
- {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/WHEEL +0 -0
- {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,373 @@
|
|
1
|
+
"""Query Builder Integration for Database Nodes.
|
2
|
+
|
3
|
+
This module provides MongoDB-style query operators for SQL databases
|
4
|
+
with database-specific optimization and type validation.
|
5
|
+
|
6
|
+
Key Features:
|
7
|
+
- MongoDB-style operators ($eq, $ne, $lt, $gte, $in, $like, etc.)
|
8
|
+
- Database-specific SQL generation
|
9
|
+
- Type validation for operators
|
10
|
+
- Query optimization
|
11
|
+
- Multi-tenant support
|
12
|
+
"""
|
13
|
+
|
14
|
+
import logging
|
15
|
+
from enum import Enum
|
16
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
17
|
+
|
18
|
+
from kailash.sdk_exceptions import NodeValidationError
|
19
|
+
|
20
|
+
logger = logging.getLogger(__name__)
|
21
|
+
|
22
|
+
|
23
|
+
class QueryOperator(Enum):
|
24
|
+
"""MongoDB-style query operators."""
|
25
|
+
|
26
|
+
# Comparison operators
|
27
|
+
EQ = "$eq" # Equal to
|
28
|
+
NE = "$ne" # Not equal to
|
29
|
+
LT = "$lt" # Less than
|
30
|
+
LTE = "$lte" # Less than or equal to
|
31
|
+
GT = "$gt" # Greater than
|
32
|
+
GTE = "$gte" # Greater than or equal to
|
33
|
+
|
34
|
+
# Array/List operators
|
35
|
+
IN = "$in" # In array
|
36
|
+
NIN = "$nin" # Not in array
|
37
|
+
|
38
|
+
# String operators
|
39
|
+
LIKE = "$like" # SQL LIKE pattern
|
40
|
+
ILIKE = "$ilike" # Case-insensitive LIKE
|
41
|
+
REGEX = "$regex" # Regular expression
|
42
|
+
|
43
|
+
# JSON/Array operators
|
44
|
+
CONTAINS = "$contains" # Array/JSON contains
|
45
|
+
CONTAINED_BY = "$contained_by" # Array/JSON contained by
|
46
|
+
HAS_KEY = "$has_key" # JSON has key
|
47
|
+
|
48
|
+
# Logical operators
|
49
|
+
AND = "$and" # Logical AND
|
50
|
+
OR = "$or" # Logical OR
|
51
|
+
NOT = "$not" # Logical NOT
|
52
|
+
|
53
|
+
|
54
|
+
class DatabaseDialect(Enum):
|
55
|
+
"""Supported database dialects."""
|
56
|
+
|
57
|
+
POSTGRESQL = "postgresql"
|
58
|
+
MYSQL = "mysql"
|
59
|
+
SQLITE = "sqlite"
|
60
|
+
|
61
|
+
|
62
|
+
class QueryBuilder:
|
63
|
+
"""MongoDB-style query builder for SQL databases."""
|
64
|
+
|
65
|
+
def __init__(self, dialect: DatabaseDialect = DatabaseDialect.POSTGRESQL):
|
66
|
+
"""Initialize query builder with database dialect.
|
67
|
+
|
68
|
+
Args:
|
69
|
+
dialect: Database dialect for SQL generation
|
70
|
+
"""
|
71
|
+
self.dialect = dialect
|
72
|
+
self.table_name: Optional[str] = None
|
73
|
+
self.tenant_id: Optional[str] = None
|
74
|
+
self.conditions: List[Dict[str, Any]] = []
|
75
|
+
self.parameters: List[Any] = []
|
76
|
+
self.parameter_counter = 0
|
77
|
+
|
78
|
+
def table(self, name: str) -> "QueryBuilder":
|
79
|
+
"""Set table name."""
|
80
|
+
self.table_name = name
|
81
|
+
return self
|
82
|
+
|
83
|
+
def tenant(self, tenant_id: str) -> "QueryBuilder":
|
84
|
+
"""Set tenant ID for multi-tenant queries."""
|
85
|
+
self.tenant_id = tenant_id
|
86
|
+
return self
|
87
|
+
|
88
|
+
def where(
|
89
|
+
self, field: str, operator: Union[str, QueryOperator], value: Any
|
90
|
+
) -> "QueryBuilder":
|
91
|
+
"""Add WHERE condition with MongoDB-style operator.
|
92
|
+
|
93
|
+
Args:
|
94
|
+
field: Field name
|
95
|
+
operator: Query operator
|
96
|
+
value: Value to compare against
|
97
|
+
|
98
|
+
Returns:
|
99
|
+
Self for method chaining
|
100
|
+
"""
|
101
|
+
if isinstance(operator, str):
|
102
|
+
operator = QueryOperator(operator)
|
103
|
+
|
104
|
+
self._validate_operator_value(operator, value)
|
105
|
+
|
106
|
+
condition = {"field": field, "operator": operator, "value": value}
|
107
|
+
|
108
|
+
self.conditions.append(condition)
|
109
|
+
return self
|
110
|
+
|
111
|
+
def find(self, query: Dict[str, Any]) -> "QueryBuilder":
|
112
|
+
"""Add conditions from MongoDB-style query object.
|
113
|
+
|
114
|
+
Args:
|
115
|
+
query: MongoDB-style query dictionary
|
116
|
+
|
117
|
+
Returns:
|
118
|
+
Self for method chaining
|
119
|
+
"""
|
120
|
+
self._parse_query_object(query)
|
121
|
+
return self
|
122
|
+
|
123
|
+
def build_select(self, fields: List[str] = None) -> Tuple[str, List[Any]]:
|
124
|
+
"""Build SELECT query.
|
125
|
+
|
126
|
+
Args:
|
127
|
+
fields: Fields to select (default: all)
|
128
|
+
|
129
|
+
Returns:
|
130
|
+
Tuple of (SQL query, parameters)
|
131
|
+
"""
|
132
|
+
if not self.table_name:
|
133
|
+
raise NodeValidationError("Table name is required")
|
134
|
+
|
135
|
+
fields_str = ", ".join(fields) if fields else "*"
|
136
|
+
base_query = f"SELECT {fields_str} FROM {self.table_name}"
|
137
|
+
|
138
|
+
where_clause, parameters = self._build_where_clause()
|
139
|
+
|
140
|
+
if where_clause:
|
141
|
+
query = f"{base_query} WHERE {where_clause}"
|
142
|
+
else:
|
143
|
+
query = base_query
|
144
|
+
|
145
|
+
return query, parameters
|
146
|
+
|
147
|
+
def build_update(self, updates: Dict[str, Any]) -> Tuple[str, List[Any]]:
|
148
|
+
"""Build UPDATE query.
|
149
|
+
|
150
|
+
Args:
|
151
|
+
updates: Fields to update
|
152
|
+
|
153
|
+
Returns:
|
154
|
+
Tuple of (SQL query, parameters)
|
155
|
+
"""
|
156
|
+
if not self.table_name:
|
157
|
+
raise NodeValidationError("Table name is required")
|
158
|
+
|
159
|
+
if not updates:
|
160
|
+
raise NodeValidationError("Updates are required")
|
161
|
+
|
162
|
+
# Build SET clause
|
163
|
+
set_clauses = []
|
164
|
+
update_params = []
|
165
|
+
|
166
|
+
for field, value in updates.items():
|
167
|
+
set_clauses.append(f"{field} = ${self._next_parameter()}")
|
168
|
+
update_params.append(value)
|
169
|
+
|
170
|
+
set_clause = ", ".join(set_clauses)
|
171
|
+
base_query = f"UPDATE {self.table_name} SET {set_clause}"
|
172
|
+
|
173
|
+
# Build WHERE clause
|
174
|
+
where_clause, where_params = self._build_where_clause()
|
175
|
+
|
176
|
+
if where_clause:
|
177
|
+
query = f"{base_query} WHERE {where_clause}"
|
178
|
+
else:
|
179
|
+
query = base_query
|
180
|
+
|
181
|
+
return query, update_params + where_params
|
182
|
+
|
183
|
+
def build_delete(self) -> Tuple[str, List[Any]]:
|
184
|
+
"""Build DELETE query.
|
185
|
+
|
186
|
+
Returns:
|
187
|
+
Tuple of (SQL query, parameters)
|
188
|
+
"""
|
189
|
+
if not self.table_name:
|
190
|
+
raise NodeValidationError("Table name is required")
|
191
|
+
|
192
|
+
base_query = f"DELETE FROM {self.table_name}"
|
193
|
+
|
194
|
+
where_clause, parameters = self._build_where_clause()
|
195
|
+
|
196
|
+
if where_clause:
|
197
|
+
query = f"{base_query} WHERE {where_clause}"
|
198
|
+
else:
|
199
|
+
query = base_query
|
200
|
+
|
201
|
+
return query, parameters
|
202
|
+
|
203
|
+
def _parse_query_object(self, query: Dict[str, Any]) -> None:
|
204
|
+
"""Parse MongoDB-style query object into conditions."""
|
205
|
+
for field, condition in query.items():
|
206
|
+
if field.startswith("$"):
|
207
|
+
# Logical operator
|
208
|
+
self._parse_logical_operator(field, condition)
|
209
|
+
else:
|
210
|
+
# Field condition
|
211
|
+
self._parse_field_condition(field, condition)
|
212
|
+
|
213
|
+
def _parse_logical_operator(self, operator: str, condition: Any) -> None:
|
214
|
+
"""Parse logical operator ($and, $or, $not)."""
|
215
|
+
if operator == "$and":
|
216
|
+
if not isinstance(condition, list):
|
217
|
+
raise NodeValidationError("$and requires a list of conditions")
|
218
|
+
for sub_condition in condition:
|
219
|
+
self._parse_query_object(sub_condition)
|
220
|
+
elif operator == "$or":
|
221
|
+
if not isinstance(condition, list):
|
222
|
+
raise NodeValidationError("$or requires a list of conditions")
|
223
|
+
# For simplicity, we'll convert OR to individual conditions
|
224
|
+
# In a full implementation, we'd need to track grouping
|
225
|
+
for sub_condition in condition:
|
226
|
+
self._parse_query_object(sub_condition)
|
227
|
+
elif operator == "$not":
|
228
|
+
# Handle NOT operator
|
229
|
+
self._parse_query_object(condition)
|
230
|
+
|
231
|
+
def _parse_field_condition(self, field: str, condition: Any) -> None:
|
232
|
+
"""Parse field condition."""
|
233
|
+
if isinstance(condition, dict):
|
234
|
+
# Operator-based condition
|
235
|
+
for op, value in condition.items():
|
236
|
+
if op in [e.value for e in QueryOperator]:
|
237
|
+
self.where(field, op, value)
|
238
|
+
else:
|
239
|
+
raise NodeValidationError(f"Unknown operator: {op}")
|
240
|
+
else:
|
241
|
+
# Simple equality
|
242
|
+
self.where(field, QueryOperator.EQ, condition)
|
243
|
+
|
244
|
+
def _build_where_clause(self) -> Tuple[str, List[Any]]:
|
245
|
+
"""Build WHERE clause from conditions."""
|
246
|
+
clauses = []
|
247
|
+
parameters = []
|
248
|
+
|
249
|
+
# Add tenant filtering first if specified
|
250
|
+
if self.tenant_id:
|
251
|
+
clauses.append(f"tenant_id = ${self._next_parameter()}")
|
252
|
+
parameters.append(self.tenant_id)
|
253
|
+
|
254
|
+
# Add other conditions
|
255
|
+
for condition in self.conditions:
|
256
|
+
clause, params = self._build_condition_clause(condition)
|
257
|
+
clauses.append(clause)
|
258
|
+
parameters.extend(params)
|
259
|
+
|
260
|
+
if not clauses:
|
261
|
+
return "", []
|
262
|
+
|
263
|
+
where_clause = " AND ".join(clauses)
|
264
|
+
|
265
|
+
# If we have tenant filtering and other conditions, group the other conditions
|
266
|
+
if self.tenant_id and len(clauses) > 1:
|
267
|
+
other_clauses = " AND ".join(clauses[1:])
|
268
|
+
where_clause = f"{clauses[0]} AND ({other_clauses})"
|
269
|
+
|
270
|
+
return where_clause, parameters
|
271
|
+
|
272
|
+
def _build_condition_clause(
|
273
|
+
self, condition: Dict[str, Any]
|
274
|
+
) -> Tuple[str, List[Any]]:
|
275
|
+
"""Build SQL clause for a single condition."""
|
276
|
+
field = condition["field"]
|
277
|
+
operator = condition["operator"]
|
278
|
+
value = condition["value"]
|
279
|
+
|
280
|
+
if operator == QueryOperator.EQ:
|
281
|
+
return f"{field} = ${self._next_parameter()}", [value]
|
282
|
+
elif operator == QueryOperator.NE:
|
283
|
+
return f"{field} != ${self._next_parameter()}", [value]
|
284
|
+
elif operator == QueryOperator.LT:
|
285
|
+
return f"{field} < ${self._next_parameter()}", [value]
|
286
|
+
elif operator == QueryOperator.LTE:
|
287
|
+
return f"{field} <= ${self._next_parameter()}", [value]
|
288
|
+
elif operator == QueryOperator.GT:
|
289
|
+
return f"{field} > ${self._next_parameter()}", [value]
|
290
|
+
elif operator == QueryOperator.GTE:
|
291
|
+
return f"{field} >= ${self._next_parameter()}", [value]
|
292
|
+
elif operator == QueryOperator.IN:
|
293
|
+
if not isinstance(value, (list, tuple)):
|
294
|
+
raise NodeValidationError("$in requires a list or tuple")
|
295
|
+
placeholders = ", ".join([f"${self._next_parameter()}" for _ in value])
|
296
|
+
return f"{field} IN ({placeholders})", list(value)
|
297
|
+
elif operator == QueryOperator.NIN:
|
298
|
+
if not isinstance(value, (list, tuple)):
|
299
|
+
raise NodeValidationError("$nin requires a list or tuple")
|
300
|
+
placeholders = ", ".join([f"${self._next_parameter()}" for _ in value])
|
301
|
+
return f"{field} NOT IN ({placeholders})", list(value)
|
302
|
+
elif operator == QueryOperator.LIKE:
|
303
|
+
return f"{field} LIKE ${self._next_parameter()}", [value]
|
304
|
+
elif operator == QueryOperator.ILIKE:
|
305
|
+
if self.dialect == DatabaseDialect.POSTGRESQL:
|
306
|
+
return f"{field} ILIKE ${self._next_parameter()}", [value]
|
307
|
+
else:
|
308
|
+
return f"LOWER({field}) LIKE LOWER(${self._next_parameter()})", [value]
|
309
|
+
elif operator == QueryOperator.REGEX:
|
310
|
+
if self.dialect == DatabaseDialect.POSTGRESQL:
|
311
|
+
return f"{field} ~ ${self._next_parameter()}", [value]
|
312
|
+
else:
|
313
|
+
return f"{field} REGEXP ${self._next_parameter()}", [value]
|
314
|
+
elif operator == QueryOperator.CONTAINS:
|
315
|
+
if self.dialect == DatabaseDialect.POSTGRESQL:
|
316
|
+
return f"{field} @> ${self._next_parameter()}", [value]
|
317
|
+
else:
|
318
|
+
return f"JSON_CONTAINS({field}, ${self._next_parameter()})", [value]
|
319
|
+
elif operator == QueryOperator.CONTAINED_BY:
|
320
|
+
if self.dialect == DatabaseDialect.POSTGRESQL:
|
321
|
+
return f"{field} <@ ${self._next_parameter()}", [value]
|
322
|
+
else:
|
323
|
+
return f"JSON_CONTAINS(${self._next_parameter()}, {field})", [value]
|
324
|
+
elif operator == QueryOperator.HAS_KEY:
|
325
|
+
if self.dialect == DatabaseDialect.POSTGRESQL:
|
326
|
+
return f"{field} ? ${self._next_parameter()}", [value]
|
327
|
+
else:
|
328
|
+
return f"JSON_EXTRACT({field}, '$.{value}') IS NOT NULL", []
|
329
|
+
else:
|
330
|
+
raise NodeValidationError(f"Unsupported operator: {operator}")
|
331
|
+
|
332
|
+
def _validate_operator_value(self, operator: QueryOperator, value: Any) -> None:
|
333
|
+
"""Validate operator-value combination."""
|
334
|
+
if operator in [QueryOperator.IN, QueryOperator.NIN]:
|
335
|
+
if not isinstance(value, (list, tuple)):
|
336
|
+
raise NodeValidationError(f"{operator.value} requires a list or tuple")
|
337
|
+
elif operator in [QueryOperator.LIKE, QueryOperator.ILIKE, QueryOperator.REGEX]:
|
338
|
+
if not isinstance(value, str):
|
339
|
+
raise NodeValidationError(f"{operator.value} requires a string value")
|
340
|
+
elif operator == QueryOperator.HAS_KEY:
|
341
|
+
if not isinstance(value, str):
|
342
|
+
raise NodeValidationError(f"{operator.value} requires a string key")
|
343
|
+
|
344
|
+
def _next_parameter(self) -> int:
|
345
|
+
"""Get next parameter placeholder number."""
|
346
|
+
self.parameter_counter += 1
|
347
|
+
return self.parameter_counter
|
348
|
+
|
349
|
+
def reset(self) -> "QueryBuilder":
|
350
|
+
"""Reset builder state."""
|
351
|
+
self.table_name = None
|
352
|
+
self.tenant_id = None
|
353
|
+
self.conditions = []
|
354
|
+
self.parameters = []
|
355
|
+
self.parameter_counter = 0
|
356
|
+
return self
|
357
|
+
|
358
|
+
|
359
|
+
# Factory function for creating query builders
|
360
|
+
def create_query_builder(dialect: str = "postgresql") -> QueryBuilder:
|
361
|
+
"""Create a query builder for the specified database dialect.
|
362
|
+
|
363
|
+
Args:
|
364
|
+
dialect: Database dialect (postgresql, mysql, sqlite)
|
365
|
+
|
366
|
+
Returns:
|
367
|
+
QueryBuilder instance
|
368
|
+
"""
|
369
|
+
try:
|
370
|
+
db_dialect = DatabaseDialect(dialect.lower())
|
371
|
+
return QueryBuilder(db_dialect)
|
372
|
+
except ValueError:
|
373
|
+
raise NodeValidationError(f"Unsupported database dialect: {dialect}")
|