kailash 0.3.2__py3-none-any.whl → 0.4.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 +33 -1
- kailash/access_control/__init__.py +129 -0
- kailash/access_control/managers.py +461 -0
- kailash/access_control/rule_evaluators.py +467 -0
- kailash/access_control_abac.py +825 -0
- kailash/config/__init__.py +27 -0
- kailash/config/database_config.py +359 -0
- kailash/database/__init__.py +28 -0
- kailash/database/execution_pipeline.py +499 -0
- kailash/middleware/__init__.py +306 -0
- kailash/middleware/auth/__init__.py +33 -0
- kailash/middleware/auth/access_control.py +436 -0
- kailash/middleware/auth/auth_manager.py +422 -0
- kailash/middleware/auth/jwt_auth.py +477 -0
- kailash/middleware/auth/kailash_jwt_auth.py +616 -0
- kailash/middleware/communication/__init__.py +37 -0
- kailash/middleware/communication/ai_chat.py +989 -0
- kailash/middleware/communication/api_gateway.py +802 -0
- kailash/middleware/communication/events.py +470 -0
- kailash/middleware/communication/realtime.py +710 -0
- kailash/middleware/core/__init__.py +21 -0
- kailash/middleware/core/agent_ui.py +890 -0
- kailash/middleware/core/schema.py +643 -0
- kailash/middleware/core/workflows.py +396 -0
- kailash/middleware/database/__init__.py +63 -0
- kailash/middleware/database/base.py +113 -0
- kailash/middleware/database/base_models.py +525 -0
- kailash/middleware/database/enums.py +106 -0
- kailash/middleware/database/migrations.py +12 -0
- kailash/{api/database.py → middleware/database/models.py} +183 -291
- kailash/middleware/database/repositories.py +685 -0
- kailash/middleware/database/session_manager.py +19 -0
- kailash/middleware/mcp/__init__.py +38 -0
- kailash/middleware/mcp/client_integration.py +585 -0
- kailash/middleware/mcp/enhanced_server.py +576 -0
- kailash/nodes/__init__.py +25 -3
- kailash/nodes/admin/__init__.py +35 -0
- kailash/nodes/admin/audit_log.py +794 -0
- kailash/nodes/admin/permission_check.py +864 -0
- kailash/nodes/admin/role_management.py +823 -0
- kailash/nodes/admin/security_event.py +1519 -0
- kailash/nodes/admin/user_management.py +944 -0
- kailash/nodes/ai/a2a.py +24 -7
- kailash/nodes/ai/ai_providers.py +1 -0
- kailash/nodes/ai/embedding_generator.py +11 -11
- kailash/nodes/ai/intelligent_agent_orchestrator.py +99 -11
- kailash/nodes/ai/llm_agent.py +407 -2
- kailash/nodes/ai/self_organizing.py +85 -10
- kailash/nodes/api/auth.py +287 -6
- kailash/nodes/api/rest.py +151 -0
- kailash/nodes/auth/__init__.py +17 -0
- kailash/nodes/auth/directory_integration.py +1228 -0
- kailash/nodes/auth/enterprise_auth_provider.py +1328 -0
- kailash/nodes/auth/mfa.py +2338 -0
- kailash/nodes/auth/risk_assessment.py +872 -0
- kailash/nodes/auth/session_management.py +1093 -0
- kailash/nodes/auth/sso.py +1040 -0
- kailash/nodes/base.py +344 -13
- kailash/nodes/base_cycle_aware.py +4 -2
- kailash/nodes/base_with_acl.py +1 -1
- kailash/nodes/code/python.py +283 -10
- kailash/nodes/compliance/__init__.py +9 -0
- kailash/nodes/compliance/data_retention.py +1888 -0
- kailash/nodes/compliance/gdpr.py +2004 -0
- kailash/nodes/data/__init__.py +22 -2
- kailash/nodes/data/async_connection.py +469 -0
- kailash/nodes/data/async_sql.py +757 -0
- kailash/nodes/data/async_vector.py +598 -0
- kailash/nodes/data/readers.py +767 -0
- kailash/nodes/data/retrieval.py +360 -1
- kailash/nodes/data/sharepoint_graph.py +397 -21
- kailash/nodes/data/sql.py +94 -5
- kailash/nodes/data/streaming.py +68 -8
- kailash/nodes/data/vector_db.py +54 -4
- kailash/nodes/enterprise/__init__.py +13 -0
- kailash/nodes/enterprise/batch_processor.py +741 -0
- kailash/nodes/enterprise/data_lineage.py +497 -0
- kailash/nodes/logic/convergence.py +31 -9
- kailash/nodes/logic/operations.py +14 -3
- kailash/nodes/mixins/__init__.py +8 -0
- kailash/nodes/mixins/event_emitter.py +201 -0
- kailash/nodes/mixins/mcp.py +9 -4
- kailash/nodes/mixins/security.py +165 -0
- kailash/nodes/monitoring/__init__.py +7 -0
- kailash/nodes/monitoring/performance_benchmark.py +2497 -0
- kailash/nodes/rag/__init__.py +284 -0
- kailash/nodes/rag/advanced.py +1615 -0
- kailash/nodes/rag/agentic.py +773 -0
- kailash/nodes/rag/conversational.py +999 -0
- kailash/nodes/rag/evaluation.py +875 -0
- kailash/nodes/rag/federated.py +1188 -0
- kailash/nodes/rag/graph.py +721 -0
- kailash/nodes/rag/multimodal.py +671 -0
- kailash/nodes/rag/optimized.py +933 -0
- kailash/nodes/rag/privacy.py +1059 -0
- kailash/nodes/rag/query_processing.py +1335 -0
- kailash/nodes/rag/realtime.py +764 -0
- kailash/nodes/rag/registry.py +547 -0
- kailash/nodes/rag/router.py +837 -0
- kailash/nodes/rag/similarity.py +1854 -0
- kailash/nodes/rag/strategies.py +566 -0
- kailash/nodes/rag/workflows.py +575 -0
- kailash/nodes/security/__init__.py +19 -0
- kailash/nodes/security/abac_evaluator.py +1411 -0
- kailash/nodes/security/audit_log.py +91 -0
- kailash/nodes/security/behavior_analysis.py +1893 -0
- kailash/nodes/security/credential_manager.py +401 -0
- kailash/nodes/security/rotating_credentials.py +760 -0
- kailash/nodes/security/security_event.py +132 -0
- kailash/nodes/security/threat_detection.py +1103 -0
- kailash/nodes/testing/__init__.py +9 -0
- kailash/nodes/testing/credential_testing.py +499 -0
- kailash/nodes/transform/__init__.py +10 -2
- kailash/nodes/transform/chunkers.py +592 -1
- kailash/nodes/transform/processors.py +484 -14
- kailash/nodes/validation.py +321 -0
- kailash/runtime/access_controlled.py +1 -1
- kailash/runtime/async_local.py +41 -7
- kailash/runtime/docker.py +1 -1
- kailash/runtime/local.py +474 -55
- kailash/runtime/parallel.py +1 -1
- kailash/runtime/parallel_cyclic.py +1 -1
- kailash/runtime/testing.py +210 -2
- kailash/utils/migrations/__init__.py +25 -0
- kailash/utils/migrations/generator.py +433 -0
- kailash/utils/migrations/models.py +231 -0
- kailash/utils/migrations/runner.py +489 -0
- kailash/utils/secure_logging.py +342 -0
- kailash/workflow/__init__.py +16 -0
- kailash/workflow/cyclic_runner.py +3 -4
- kailash/workflow/graph.py +70 -2
- kailash/workflow/resilience.py +249 -0
- kailash/workflow/templates.py +726 -0
- {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/METADATA +253 -20
- kailash-0.4.0.dist-info/RECORD +223 -0
- kailash/api/__init__.py +0 -17
- kailash/api/__main__.py +0 -6
- kailash/api/studio_secure.py +0 -893
- kailash/mcp/__main__.py +0 -13
- kailash/mcp/server_new.py +0 -336
- kailash/mcp/servers/__init__.py +0 -12
- kailash-0.3.2.dist-info/RECORD +0 -136
- {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/WHEEL +0 -0
- {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,499 @@
|
|
1
|
+
"""Database execution pipeline for clean separation of concerns.
|
2
|
+
|
3
|
+
This module provides a pipeline-based approach to database operations,
|
4
|
+
separating permission checking, query execution, and data masking into
|
5
|
+
clear, testable stages.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import logging
|
9
|
+
import time
|
10
|
+
from abc import ABC, abstractmethod
|
11
|
+
from dataclasses import dataclass
|
12
|
+
from typing import Any, Dict, List, Optional, Union
|
13
|
+
|
14
|
+
from kailash.access_control import NodePermission, UserContext
|
15
|
+
from kailash.sdk_exceptions import NodeExecutionError
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
@dataclass
|
21
|
+
class ExecutionContext:
|
22
|
+
"""Context for database execution pipeline."""
|
23
|
+
|
24
|
+
query: str
|
25
|
+
parameters: Optional[Union[Dict[str, Any], List[Any]]] = None
|
26
|
+
user_context: Optional[UserContext] = None
|
27
|
+
node_name: str = "unknown_node"
|
28
|
+
result_format: str = "dict"
|
29
|
+
runtime_context: Optional[Dict[str, Any]] = None
|
30
|
+
|
31
|
+
|
32
|
+
@dataclass
|
33
|
+
class ExecutionResult:
|
34
|
+
"""Result from database execution pipeline."""
|
35
|
+
|
36
|
+
data: Any
|
37
|
+
row_count: int
|
38
|
+
columns: List[str]
|
39
|
+
execution_time: float
|
40
|
+
metadata: Optional[Dict[str, Any]] = None
|
41
|
+
|
42
|
+
|
43
|
+
class PipelineStage(ABC):
|
44
|
+
"""Abstract base class for pipeline stages."""
|
45
|
+
|
46
|
+
@abstractmethod
|
47
|
+
async def process(
|
48
|
+
self, context: ExecutionContext, result: Optional[ExecutionResult] = None
|
49
|
+
) -> Optional[ExecutionResult]:
|
50
|
+
"""Process this stage of the pipeline.
|
51
|
+
|
52
|
+
Args:
|
53
|
+
context: Execution context
|
54
|
+
result: Result from previous stage (None for first stage)
|
55
|
+
|
56
|
+
Returns:
|
57
|
+
Result to pass to next stage, or None to stop pipeline
|
58
|
+
"""
|
59
|
+
pass
|
60
|
+
|
61
|
+
@abstractmethod
|
62
|
+
def get_stage_name(self) -> str:
|
63
|
+
"""Get the name of this pipeline stage."""
|
64
|
+
pass
|
65
|
+
|
66
|
+
|
67
|
+
class PermissionCheckStage(PipelineStage):
|
68
|
+
"""Pipeline stage for checking user permissions."""
|
69
|
+
|
70
|
+
def __init__(self, access_control_manager=None):
|
71
|
+
"""Initialize permission check stage.
|
72
|
+
|
73
|
+
Args:
|
74
|
+
access_control_manager: Access control manager for permission checks
|
75
|
+
"""
|
76
|
+
self.access_control_manager = access_control_manager
|
77
|
+
self.logger = logging.getLogger(f"{__name__}.PermissionCheckStage")
|
78
|
+
|
79
|
+
async def process(
|
80
|
+
self, context: ExecutionContext, result: Optional[ExecutionResult] = None
|
81
|
+
) -> Optional[ExecutionResult]:
|
82
|
+
"""Check user permissions before query execution."""
|
83
|
+
# Skip if no access control or no user context
|
84
|
+
if not self.access_control_manager or not context.user_context:
|
85
|
+
self.logger.debug(
|
86
|
+
"Skipping permission check - no access control or user context"
|
87
|
+
)
|
88
|
+
return result
|
89
|
+
|
90
|
+
# Check execute permission
|
91
|
+
decision = self.access_control_manager.check_node_access(
|
92
|
+
context.user_context,
|
93
|
+
context.node_name,
|
94
|
+
NodePermission.EXECUTE,
|
95
|
+
context.runtime_context,
|
96
|
+
)
|
97
|
+
|
98
|
+
if not decision.allowed:
|
99
|
+
raise NodeExecutionError(f"Access denied: {decision.reason}")
|
100
|
+
|
101
|
+
self.logger.debug(
|
102
|
+
f"Permission granted for {context.node_name}: {decision.reason}"
|
103
|
+
)
|
104
|
+
return result
|
105
|
+
|
106
|
+
def get_stage_name(self) -> str:
|
107
|
+
"""Get stage name."""
|
108
|
+
return "permission_check"
|
109
|
+
|
110
|
+
|
111
|
+
class QueryValidationStage(PipelineStage):
|
112
|
+
"""Pipeline stage for validating SQL queries."""
|
113
|
+
|
114
|
+
def __init__(self, validation_rules: Optional[Dict[str, Any]] = None):
|
115
|
+
"""Initialize query validation stage.
|
116
|
+
|
117
|
+
Args:
|
118
|
+
validation_rules: Custom validation rules
|
119
|
+
"""
|
120
|
+
self.validation_rules = validation_rules or {}
|
121
|
+
self.logger = logging.getLogger(f"{__name__}.QueryValidationStage")
|
122
|
+
|
123
|
+
async def process(
|
124
|
+
self, context: ExecutionContext, result: Optional[ExecutionResult] = None
|
125
|
+
) -> Optional[ExecutionResult]:
|
126
|
+
"""Validate query for security and safety."""
|
127
|
+
if not context.query:
|
128
|
+
raise NodeExecutionError("Query cannot be empty")
|
129
|
+
|
130
|
+
# Basic SQL injection checks
|
131
|
+
self._validate_query_safety(context.query)
|
132
|
+
|
133
|
+
self.logger.debug(f"Query validation passed for: {context.query[:100]}...")
|
134
|
+
return result
|
135
|
+
|
136
|
+
def _validate_query_safety(self, query: str) -> None:
|
137
|
+
"""Validate query for potential security issues."""
|
138
|
+
if not query:
|
139
|
+
return
|
140
|
+
|
141
|
+
query_upper = query.upper().strip()
|
142
|
+
|
143
|
+
# Check for dangerous operations
|
144
|
+
dangerous_keywords = [
|
145
|
+
"DROP",
|
146
|
+
"DELETE",
|
147
|
+
"TRUNCATE",
|
148
|
+
"ALTER",
|
149
|
+
"CREATE",
|
150
|
+
"GRANT",
|
151
|
+
"REVOKE",
|
152
|
+
"EXEC",
|
153
|
+
"EXECUTE",
|
154
|
+
"SHUTDOWN",
|
155
|
+
"BACKUP",
|
156
|
+
"RESTORE",
|
157
|
+
]
|
158
|
+
|
159
|
+
import re
|
160
|
+
|
161
|
+
for keyword in dangerous_keywords:
|
162
|
+
pattern = r"\b" + re.escape(keyword) + r"\b"
|
163
|
+
if re.search(pattern, query_upper):
|
164
|
+
self.logger.warning(
|
165
|
+
f"Query contains potentially dangerous keyword: {keyword}"
|
166
|
+
)
|
167
|
+
# In production, you might want to block these entirely
|
168
|
+
# raise NodeExecutionError(f"Query contains forbidden keyword: {keyword}")
|
169
|
+
|
170
|
+
def get_stage_name(self) -> str:
|
171
|
+
"""Get stage name."""
|
172
|
+
return "query_validation"
|
173
|
+
|
174
|
+
|
175
|
+
class QueryExecutionStage(PipelineStage):
|
176
|
+
"""Pipeline stage for executing SQL queries."""
|
177
|
+
|
178
|
+
def __init__(self, query_executor):
|
179
|
+
"""Initialize query execution stage.
|
180
|
+
|
181
|
+
Args:
|
182
|
+
query_executor: Object that can execute queries (engine, connection, etc.)
|
183
|
+
"""
|
184
|
+
self.query_executor = query_executor
|
185
|
+
self.logger = logging.getLogger(f"{__name__}.QueryExecutionStage")
|
186
|
+
|
187
|
+
async def process(
|
188
|
+
self, context: ExecutionContext, result: Optional[ExecutionResult] = None
|
189
|
+
) -> Optional[ExecutionResult]:
|
190
|
+
"""Execute the SQL query."""
|
191
|
+
start_time = time.time()
|
192
|
+
|
193
|
+
try:
|
194
|
+
# This is where the actual query execution happens
|
195
|
+
# The implementation depends on whether it's sync or async
|
196
|
+
if hasattr(self.query_executor, "execute_query"):
|
197
|
+
# Custom executor interface
|
198
|
+
query_result = await self.query_executor.execute_query(
|
199
|
+
context.query, context.parameters, context.result_format
|
200
|
+
)
|
201
|
+
else:
|
202
|
+
# Fallback - assume it's a callable
|
203
|
+
query_result = await self.query_executor(
|
204
|
+
context.query, context.parameters
|
205
|
+
)
|
206
|
+
|
207
|
+
execution_time = time.time() - start_time
|
208
|
+
|
209
|
+
# Format the result
|
210
|
+
if isinstance(query_result, dict):
|
211
|
+
# Structured result
|
212
|
+
return ExecutionResult(
|
213
|
+
data=query_result.get("data", []),
|
214
|
+
row_count=query_result.get("row_count", 0),
|
215
|
+
columns=query_result.get("columns", []),
|
216
|
+
execution_time=execution_time,
|
217
|
+
metadata=query_result.get("metadata", {}),
|
218
|
+
)
|
219
|
+
else:
|
220
|
+
# Raw result - format it
|
221
|
+
return ExecutionResult(
|
222
|
+
data=query_result,
|
223
|
+
row_count=(
|
224
|
+
len(query_result) if isinstance(query_result, list) else 1
|
225
|
+
),
|
226
|
+
columns=[],
|
227
|
+
execution_time=execution_time,
|
228
|
+
)
|
229
|
+
|
230
|
+
except Exception as e:
|
231
|
+
execution_time = time.time() - start_time
|
232
|
+
self.logger.error(
|
233
|
+
f"Query execution failed after {execution_time:.3f}s: {e}"
|
234
|
+
)
|
235
|
+
raise NodeExecutionError(f"Database query failed: {e}") from e
|
236
|
+
|
237
|
+
def get_stage_name(self) -> str:
|
238
|
+
"""Get stage name."""
|
239
|
+
return "query_execution"
|
240
|
+
|
241
|
+
|
242
|
+
class DataMaskingStage(PipelineStage):
|
243
|
+
"""Pipeline stage for applying data masking based on user attributes."""
|
244
|
+
|
245
|
+
def __init__(self, access_control_manager=None):
|
246
|
+
"""Initialize data masking stage.
|
247
|
+
|
248
|
+
Args:
|
249
|
+
access_control_manager: Access control manager with masking capabilities
|
250
|
+
"""
|
251
|
+
self.access_control_manager = access_control_manager
|
252
|
+
self.logger = logging.getLogger(f"{__name__}.DataMaskingStage")
|
253
|
+
|
254
|
+
async def process(
|
255
|
+
self, context: ExecutionContext, result: Optional[ExecutionResult] = None
|
256
|
+
) -> Optional[ExecutionResult]:
|
257
|
+
"""Apply data masking based on user attributes."""
|
258
|
+
if not result or not result.data:
|
259
|
+
return result
|
260
|
+
|
261
|
+
# Skip if no access control or no user context
|
262
|
+
if not self.access_control_manager or not context.user_context:
|
263
|
+
self.logger.debug(
|
264
|
+
"Skipping data masking - no access control or user context"
|
265
|
+
)
|
266
|
+
return result
|
267
|
+
|
268
|
+
# Skip if not dict format (masking only works on structured data)
|
269
|
+
if context.result_format != "dict" or not isinstance(result.data, list):
|
270
|
+
self.logger.debug("Skipping data masking - data format not supported")
|
271
|
+
return result
|
272
|
+
|
273
|
+
# Apply masking to each row
|
274
|
+
masked_data = []
|
275
|
+
for row in result.data:
|
276
|
+
if isinstance(row, dict):
|
277
|
+
# Apply masking if access control manager supports it
|
278
|
+
if hasattr(self.access_control_manager, "apply_data_masking"):
|
279
|
+
masked_row = self.access_control_manager.apply_data_masking(
|
280
|
+
context.user_context, context.node_name, row
|
281
|
+
)
|
282
|
+
masked_data.append(masked_row)
|
283
|
+
else:
|
284
|
+
masked_data.append(row)
|
285
|
+
else:
|
286
|
+
masked_data.append(row)
|
287
|
+
|
288
|
+
# Return result with masked data
|
289
|
+
return ExecutionResult(
|
290
|
+
data=masked_data,
|
291
|
+
row_count=result.row_count,
|
292
|
+
columns=result.columns,
|
293
|
+
execution_time=result.execution_time,
|
294
|
+
metadata=result.metadata,
|
295
|
+
)
|
296
|
+
|
297
|
+
def get_stage_name(self) -> str:
|
298
|
+
"""Get stage name."""
|
299
|
+
return "data_masking"
|
300
|
+
|
301
|
+
|
302
|
+
class DatabaseExecutionPipeline:
|
303
|
+
"""Pipeline for executing database operations with clean separation of concerns.
|
304
|
+
|
305
|
+
This pipeline provides:
|
306
|
+
- Permission checking
|
307
|
+
- Query validation
|
308
|
+
- Query execution
|
309
|
+
- Data masking
|
310
|
+
|
311
|
+
Example:
|
312
|
+
>>> pipeline = DatabaseExecutionPipeline(
|
313
|
+
... access_control_manager=access_manager,
|
314
|
+
... query_executor=my_executor
|
315
|
+
... )
|
316
|
+
>>>
|
317
|
+
>>> context = ExecutionContext(
|
318
|
+
... query="SELECT * FROM users",
|
319
|
+
... user_context=user,
|
320
|
+
... node_name="user_query"
|
321
|
+
... )
|
322
|
+
>>>
|
323
|
+
>>> result = await pipeline.execute(context)
|
324
|
+
"""
|
325
|
+
|
326
|
+
def __init__(
|
327
|
+
self,
|
328
|
+
access_control_manager=None,
|
329
|
+
query_executor=None,
|
330
|
+
validation_rules: Optional[Dict[str, Any]] = None,
|
331
|
+
custom_stages: Optional[List[PipelineStage]] = None,
|
332
|
+
):
|
333
|
+
"""Initialize database execution pipeline.
|
334
|
+
|
335
|
+
Args:
|
336
|
+
access_control_manager: Access control manager for permissions and masking
|
337
|
+
query_executor: Object that can execute database queries
|
338
|
+
validation_rules: Custom validation rules for queries
|
339
|
+
custom_stages: Additional custom pipeline stages
|
340
|
+
"""
|
341
|
+
self.access_control_manager = access_control_manager
|
342
|
+
self.query_executor = query_executor
|
343
|
+
self.logger = logging.getLogger(f"{__name__}.DatabaseExecutionPipeline")
|
344
|
+
|
345
|
+
# Build pipeline stages
|
346
|
+
self.stages: List[PipelineStage] = []
|
347
|
+
|
348
|
+
# 1. Permission check
|
349
|
+
self.stages.append(PermissionCheckStage(access_control_manager))
|
350
|
+
|
351
|
+
# 2. Query validation
|
352
|
+
self.stages.append(QueryValidationStage(validation_rules))
|
353
|
+
|
354
|
+
# 3. Custom stages (before execution)
|
355
|
+
if custom_stages:
|
356
|
+
for stage in custom_stages:
|
357
|
+
if stage.get_stage_name() != "query_execution":
|
358
|
+
self.stages.append(stage)
|
359
|
+
|
360
|
+
# 4. Query execution
|
361
|
+
if query_executor:
|
362
|
+
self.stages.append(QueryExecutionStage(query_executor))
|
363
|
+
|
364
|
+
# 5. Data masking
|
365
|
+
self.stages.append(DataMaskingStage(access_control_manager))
|
366
|
+
|
367
|
+
# 6. Custom stages (after execution)
|
368
|
+
if custom_stages:
|
369
|
+
for stage in custom_stages:
|
370
|
+
if stage.get_stage_name() == "post_processing":
|
371
|
+
self.stages.append(stage)
|
372
|
+
|
373
|
+
self.logger.info(f"Initialized pipeline with {len(self.stages)} stages")
|
374
|
+
|
375
|
+
async def execute(self, context: ExecutionContext) -> ExecutionResult:
|
376
|
+
"""Execute the full database pipeline.
|
377
|
+
|
378
|
+
Args:
|
379
|
+
context: Execution context with query, user, etc.
|
380
|
+
|
381
|
+
Returns:
|
382
|
+
Execution result with data, timing, etc.
|
383
|
+
|
384
|
+
Raises:
|
385
|
+
NodeExecutionError: If any stage fails
|
386
|
+
"""
|
387
|
+
self.logger.debug(f"Starting pipeline execution for {context.node_name}")
|
388
|
+
|
389
|
+
result = None
|
390
|
+
pipeline_start = time.time()
|
391
|
+
|
392
|
+
try:
|
393
|
+
# Execute each stage in sequence
|
394
|
+
for i, stage in enumerate(self.stages):
|
395
|
+
stage_start = time.time()
|
396
|
+
|
397
|
+
try:
|
398
|
+
result = await stage.process(context, result)
|
399
|
+
stage_time = time.time() - stage_start
|
400
|
+
|
401
|
+
self.logger.debug(
|
402
|
+
f"Stage {i+1}/{len(self.stages)} ({stage.get_stage_name()}) "
|
403
|
+
f"completed in {stage_time:.3f}s"
|
404
|
+
)
|
405
|
+
|
406
|
+
# Allow stages to stop the pipeline
|
407
|
+
if result is None and stage.get_stage_name() != "permission_check":
|
408
|
+
self.logger.warning(
|
409
|
+
f"Pipeline stopped at stage: {stage.get_stage_name()}"
|
410
|
+
)
|
411
|
+
break
|
412
|
+
|
413
|
+
except Exception as e:
|
414
|
+
self.logger.error(
|
415
|
+
f"Pipeline failed at stage {stage.get_stage_name()}: {e}"
|
416
|
+
)
|
417
|
+
raise
|
418
|
+
|
419
|
+
pipeline_time = time.time() - pipeline_start
|
420
|
+
self.logger.info(f"Pipeline execution completed in {pipeline_time:.3f}s")
|
421
|
+
|
422
|
+
# Ensure we have a result
|
423
|
+
if result is None:
|
424
|
+
result = ExecutionResult(
|
425
|
+
data=[],
|
426
|
+
row_count=0,
|
427
|
+
columns=[],
|
428
|
+
execution_time=pipeline_time,
|
429
|
+
)
|
430
|
+
|
431
|
+
return result
|
432
|
+
|
433
|
+
except Exception as e:
|
434
|
+
pipeline_time = time.time() - pipeline_start
|
435
|
+
self.logger.error(
|
436
|
+
f"Pipeline execution failed after {pipeline_time:.3f}s: {e}"
|
437
|
+
)
|
438
|
+
raise
|
439
|
+
|
440
|
+
def add_stage(self, stage: PipelineStage, position: Optional[int] = None) -> None:
|
441
|
+
"""Add a custom stage to the pipeline.
|
442
|
+
|
443
|
+
Args:
|
444
|
+
stage: Pipeline stage to add
|
445
|
+
position: Position to insert at (None = append)
|
446
|
+
"""
|
447
|
+
if position is None:
|
448
|
+
self.stages.append(stage)
|
449
|
+
else:
|
450
|
+
self.stages.insert(position, stage)
|
451
|
+
|
452
|
+
self.logger.info(
|
453
|
+
f"Added stage {stage.get_stage_name()} at position {position or len(self.stages)}"
|
454
|
+
)
|
455
|
+
|
456
|
+
def remove_stage(self, stage_name: str) -> bool:
|
457
|
+
"""Remove a stage from the pipeline.
|
458
|
+
|
459
|
+
Args:
|
460
|
+
stage_name: Name of stage to remove
|
461
|
+
|
462
|
+
Returns:
|
463
|
+
True if stage was found and removed
|
464
|
+
"""
|
465
|
+
initial_count = len(self.stages)
|
466
|
+
self.stages = [s for s in self.stages if s.get_stage_name() != stage_name]
|
467
|
+
removed = len(self.stages) < initial_count
|
468
|
+
|
469
|
+
if removed:
|
470
|
+
self.logger.info(f"Removed stage {stage_name}")
|
471
|
+
|
472
|
+
return removed
|
473
|
+
|
474
|
+
def get_stage_info(self) -> List[Dict[str, str]]:
|
475
|
+
"""Get information about all pipeline stages.
|
476
|
+
|
477
|
+
Returns:
|
478
|
+
List of stage information dictionaries
|
479
|
+
"""
|
480
|
+
return [
|
481
|
+
{
|
482
|
+
"name": stage.get_stage_name(),
|
483
|
+
"type": type(stage).__name__,
|
484
|
+
}
|
485
|
+
for stage in self.stages
|
486
|
+
]
|
487
|
+
|
488
|
+
|
489
|
+
# Export components
|
490
|
+
__all__ = [
|
491
|
+
"ExecutionContext",
|
492
|
+
"ExecutionResult",
|
493
|
+
"PipelineStage",
|
494
|
+
"PermissionCheckStage",
|
495
|
+
"QueryValidationStage",
|
496
|
+
"QueryExecutionStage",
|
497
|
+
"DataMaskingStage",
|
498
|
+
"DatabaseExecutionPipeline",
|
499
|
+
]
|