kailash 0.5.0__py3-none-any.whl → 0.6.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 (57) hide show
  1. kailash/__init__.py +1 -1
  2. kailash/client/__init__.py +12 -0
  3. kailash/client/enhanced_client.py +306 -0
  4. kailash/core/actors/__init__.py +16 -0
  5. kailash/core/actors/connection_actor.py +566 -0
  6. kailash/core/actors/supervisor.py +364 -0
  7. kailash/edge/__init__.py +16 -0
  8. kailash/edge/compliance.py +834 -0
  9. kailash/edge/discovery.py +659 -0
  10. kailash/edge/location.py +582 -0
  11. kailash/gateway/__init__.py +33 -0
  12. kailash/gateway/api.py +289 -0
  13. kailash/gateway/enhanced_gateway.py +357 -0
  14. kailash/gateway/resource_resolver.py +217 -0
  15. kailash/gateway/security.py +227 -0
  16. kailash/middleware/auth/models.py +2 -2
  17. kailash/middleware/database/base_models.py +1 -7
  18. kailash/middleware/gateway/__init__.py +22 -0
  19. kailash/middleware/gateway/checkpoint_manager.py +398 -0
  20. kailash/middleware/gateway/deduplicator.py +382 -0
  21. kailash/middleware/gateway/durable_gateway.py +417 -0
  22. kailash/middleware/gateway/durable_request.py +498 -0
  23. kailash/middleware/gateway/event_store.py +459 -0
  24. kailash/nodes/admin/permission_check.py +817 -33
  25. kailash/nodes/admin/role_management.py +1242 -108
  26. kailash/nodes/admin/schema_manager.py +438 -0
  27. kailash/nodes/admin/user_management.py +1124 -1582
  28. kailash/nodes/code/__init__.py +8 -1
  29. kailash/nodes/code/async_python.py +1035 -0
  30. kailash/nodes/code/python.py +1 -0
  31. kailash/nodes/data/async_sql.py +9 -3
  32. kailash/nodes/data/sql.py +20 -11
  33. kailash/nodes/data/workflow_connection_pool.py +643 -0
  34. kailash/nodes/rag/__init__.py +1 -4
  35. kailash/resources/__init__.py +40 -0
  36. kailash/resources/factory.py +533 -0
  37. kailash/resources/health.py +319 -0
  38. kailash/resources/reference.py +288 -0
  39. kailash/resources/registry.py +392 -0
  40. kailash/runtime/async_local.py +711 -302
  41. kailash/testing/__init__.py +34 -0
  42. kailash/testing/async_test_case.py +353 -0
  43. kailash/testing/async_utils.py +345 -0
  44. kailash/testing/fixtures.py +458 -0
  45. kailash/testing/mock_registry.py +495 -0
  46. kailash/workflow/__init__.py +8 -0
  47. kailash/workflow/async_builder.py +621 -0
  48. kailash/workflow/async_patterns.py +766 -0
  49. kailash/workflow/cyclic_runner.py +107 -16
  50. kailash/workflow/graph.py +7 -2
  51. kailash/workflow/resilience.py +11 -1
  52. {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/METADATA +7 -4
  53. {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/RECORD +57 -22
  54. {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/WHEEL +0 -0
  55. {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/entry_points.txt +0 -0
  56. {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/licenses/LICENSE +0 -0
  57. {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,566 @@
1
+ """Actor-based connection management for Kailash SDK.
2
+
3
+ This module implements an actor-based approach to database connections,
4
+ providing better isolation, fault tolerance, and concurrent handling
5
+ compared to traditional thread-based models.
6
+ """
7
+
8
+ import asyncio
9
+ import logging
10
+ import time
11
+ import uuid
12
+ from dataclasses import dataclass, field
13
+ from datetime import UTC, datetime, timedelta
14
+ from enum import Enum
15
+ from typing import Any, Callable, Dict, Optional, Union
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class ConnectionState(Enum):
21
+ """Connection lifecycle states."""
22
+
23
+ INITIALIZING = "initializing"
24
+ HEALTHY = "healthy"
25
+ DEGRADED = "degraded"
26
+ RECYCLING = "recycling"
27
+ FAILED = "failed"
28
+ TERMINATED = "terminated"
29
+
30
+
31
+ class MessageType(Enum):
32
+ """Actor message types."""
33
+
34
+ QUERY = "query"
35
+ HEALTH_CHECK = "health_check"
36
+ RECYCLE = "recycle"
37
+ TERMINATE = "terminate"
38
+ GET_STATS = "get_stats"
39
+ PING = "ping"
40
+
41
+
42
+ @dataclass
43
+ class Message:
44
+ """Message for actor communication."""
45
+
46
+ id: str = field(default_factory=lambda: uuid.uuid4().hex)
47
+ type: MessageType = MessageType.QUERY
48
+ payload: Any = None
49
+ reply_to: Optional[asyncio.Queue] = None
50
+ timestamp: datetime = field(default_factory=datetime.utcnow)
51
+
52
+
53
+ @dataclass
54
+ class QueryResult:
55
+ """Result of a database query."""
56
+
57
+ success: bool
58
+ data: Optional[Any] = None
59
+ error: Optional[str] = None
60
+ execution_time: float = 0.0
61
+ connection_id: Optional[str] = None
62
+
63
+
64
+ @dataclass
65
+ class ConnectionStats:
66
+ """Statistics for a connection."""
67
+
68
+ queries_executed: int = 0
69
+ errors_encountered: int = 0
70
+ total_execution_time: float = 0.0
71
+ health_checks_passed: int = 0
72
+ health_checks_failed: int = 0
73
+ created_at: datetime = field(default_factory=datetime.utcnow)
74
+ last_used_at: datetime = field(default_factory=datetime.utcnow)
75
+ health_score: float = 100.0
76
+
77
+
78
+ class ActorConnection:
79
+ """
80
+ Actor-based database connection with isolated state and message passing.
81
+
82
+ This class represents a single database connection as an independent actor
83
+ with its own mailbox, state, and lifecycle management.
84
+ """
85
+
86
+ def __init__(
87
+ self,
88
+ connection_id: str,
89
+ db_config: Dict[str, Any],
90
+ health_check_query: str = "SELECT 1",
91
+ health_check_interval: float = 30.0,
92
+ max_lifetime: float = 3600.0,
93
+ max_idle_time: float = 600.0,
94
+ ):
95
+ """
96
+ Initialize an actor connection.
97
+
98
+ Args:
99
+ connection_id: Unique identifier for this connection
100
+ db_config: Database configuration parameters
101
+ health_check_query: Query to run for health checks
102
+ health_check_interval: Seconds between health checks
103
+ max_lifetime: Maximum connection lifetime in seconds
104
+ max_idle_time: Maximum idle time before recycling
105
+ """
106
+ self.id = connection_id
107
+ self.db_config = db_config
108
+ self.health_check_query = health_check_query
109
+ self.health_check_interval = health_check_interval
110
+ self.max_lifetime = max_lifetime
111
+ self.max_idle_time = max_idle_time
112
+
113
+ # Actor state
114
+ self.state = ConnectionState.INITIALIZING
115
+ self.mailbox = asyncio.Queue(maxsize=100)
116
+ self.stats = ConnectionStats()
117
+ self.supervisor = None
118
+
119
+ # Physical connection (set during connect)
120
+ self._connection = None
121
+ self._adapter = None
122
+
123
+ # Background tasks
124
+ self._message_handler_task = None
125
+ self._health_monitor_task = None
126
+
127
+ # Lifecycle tracking
128
+ self._created_at = time.time()
129
+ self._last_activity = time.time()
130
+
131
+ async def start(self):
132
+ """Start the actor and its background tasks."""
133
+ try:
134
+ # Establish connection
135
+ await self._connect()
136
+
137
+ # Start message handler
138
+ self._message_handler_task = asyncio.create_task(self._handle_messages())
139
+
140
+ # Start health monitor
141
+ self._health_monitor_task = asyncio.create_task(self._monitor_health())
142
+
143
+ self.state = ConnectionState.HEALTHY
144
+ logger.info(f"Connection actor {self.id} started successfully")
145
+
146
+ except Exception as e:
147
+ self.state = ConnectionState.FAILED
148
+ logger.error(f"Failed to start connection actor {self.id}: {e}")
149
+ raise
150
+
151
+ async def stop(self):
152
+ """Stop the actor gracefully."""
153
+ self.state = ConnectionState.TERMINATED
154
+
155
+ # Cancel background tasks
156
+ if self._message_handler_task:
157
+ self._message_handler_task.cancel()
158
+ try:
159
+ await self._message_handler_task
160
+ except asyncio.CancelledError:
161
+ pass
162
+
163
+ if self._health_monitor_task:
164
+ self._health_monitor_task.cancel()
165
+ try:
166
+ await self._health_monitor_task
167
+ except asyncio.CancelledError:
168
+ pass
169
+
170
+ # Close connection
171
+ await self._disconnect()
172
+
173
+ logger.info(f"Connection actor {self.id} stopped")
174
+
175
+ async def send_message(self, message: Message) -> Any:
176
+ """
177
+ Send a message to the actor and wait for response.
178
+
179
+ Args:
180
+ message: Message to send
181
+
182
+ Returns:
183
+ Response from the actor
184
+ """
185
+ if self.state == ConnectionState.TERMINATED:
186
+ raise RuntimeError(f"Actor {self.id} is terminated")
187
+
188
+ # Create reply queue if not provided
189
+ if not message.reply_to:
190
+ message.reply_to = asyncio.Queue(maxsize=1)
191
+
192
+ # Send message
193
+ await self.mailbox.put(message)
194
+
195
+ # Wait for reply
196
+ response = await message.reply_to.get()
197
+ return response
198
+
199
+ async def _handle_messages(self):
200
+ """Main message handling loop."""
201
+ message = None
202
+ while self.state != ConnectionState.TERMINATED:
203
+ try:
204
+ # Check if event loop is still running
205
+ try:
206
+ asyncio.get_running_loop()
207
+ except RuntimeError:
208
+ # Event loop closed, terminate gracefully
209
+ break
210
+
211
+ # Wait for message with timeout
212
+ message = await asyncio.wait_for(self.mailbox.get(), timeout=1.0)
213
+
214
+ # Update activity timestamp
215
+ self._last_activity = time.time()
216
+
217
+ # Process message based on type
218
+ if message.type == MessageType.QUERY:
219
+ response = await self._handle_query(message.payload)
220
+ elif message.type == MessageType.HEALTH_CHECK:
221
+ response = await self._handle_health_check()
222
+ elif message.type == MessageType.RECYCLE:
223
+ response = await self._handle_recycle()
224
+ elif message.type == MessageType.GET_STATS:
225
+ response = self._get_stats()
226
+ elif message.type == MessageType.PING:
227
+ response = {"status": "pong", "state": self.state.value}
228
+ else:
229
+ response = {"error": f"Unknown message type: {message.type}"}
230
+
231
+ # Send reply if requested (check if we can still send replies)
232
+ if message and message.reply_to:
233
+ try:
234
+ await message.reply_to.put(response)
235
+ except RuntimeError:
236
+ # Queue is bound to different event loop, skip reply
237
+ pass
238
+
239
+ # Clear message reference
240
+ message = None
241
+
242
+ except asyncio.TimeoutError:
243
+ # Check if connection should be recycled
244
+ if self._should_recycle():
245
+ await self._initiate_recycle()
246
+ except (asyncio.CancelledError, GeneratorExit):
247
+ # Handle graceful shutdown
248
+ break
249
+ except Exception as e:
250
+ logger.error(f"Error handling message in actor {self.id}: {e}")
251
+ if message and message.reply_to:
252
+ try:
253
+ await message.reply_to.put({"error": str(e)})
254
+ except RuntimeError:
255
+ # Queue is bound to different event loop, skip reply
256
+ pass
257
+ message = None
258
+
259
+ async def _handle_query(self, query_params: Dict[str, Any]) -> QueryResult:
260
+ """Execute a database query."""
261
+ start_time = time.time()
262
+
263
+ try:
264
+ if self.state != ConnectionState.HEALTHY:
265
+ return QueryResult(
266
+ success=False,
267
+ error=f"Connection in {self.state.value} state",
268
+ connection_id=self.id,
269
+ )
270
+
271
+ # Import FetchMode enum
272
+ from kailash.nodes.data.async_sql import FetchMode
273
+
274
+ # Execute query using adapter
275
+ fetch_mode_str = query_params.get("fetch_mode", "all")
276
+ fetch_mode = FetchMode(fetch_mode_str.lower())
277
+
278
+ result = await self._adapter.execute(
279
+ query=query_params.get("query"),
280
+ params=query_params.get("params"),
281
+ fetch_mode=fetch_mode,
282
+ )
283
+
284
+ execution_time = time.time() - start_time
285
+
286
+ # Update stats
287
+ self.stats.queries_executed += 1
288
+ self.stats.total_execution_time += execution_time
289
+ self.stats.last_used_at = datetime.now(UTC)
290
+
291
+ return QueryResult(
292
+ success=True,
293
+ data=result,
294
+ execution_time=execution_time,
295
+ connection_id=self.id,
296
+ )
297
+
298
+ except Exception as e:
299
+ execution_time = time.time() - start_time
300
+ self.stats.errors_encountered += 1
301
+
302
+ # Degrade connection on errors
303
+ self._update_health_score(-10)
304
+
305
+ return QueryResult(
306
+ success=False,
307
+ error=str(e),
308
+ execution_time=execution_time,
309
+ connection_id=self.id,
310
+ )
311
+
312
+ async def _handle_health_check(self) -> Dict[str, Any]:
313
+ """Perform a health check."""
314
+ try:
315
+ start_time = time.time()
316
+
317
+ # Import FetchMode enum
318
+ from kailash.nodes.data.async_sql import FetchMode
319
+
320
+ # Run health check query
321
+ await self._adapter.execute(
322
+ query=self.health_check_query, params=None, fetch_mode=FetchMode.ONE
323
+ )
324
+
325
+ check_time = time.time() - start_time
326
+
327
+ # Update stats
328
+ self.stats.health_checks_passed += 1
329
+ self._update_health_score(5) # Boost score on success
330
+
331
+ return {
332
+ "healthy": True,
333
+ "check_time": check_time,
334
+ "health_score": self.stats.health_score,
335
+ }
336
+
337
+ except Exception as e:
338
+ self.stats.health_checks_failed += 1
339
+ self._update_health_score(-20) # Significant penalty on failure
340
+
341
+ return {
342
+ "healthy": False,
343
+ "error": str(e),
344
+ "health_score": self.stats.health_score,
345
+ }
346
+
347
+ async def _handle_recycle(self) -> Dict[str, Any]:
348
+ """Handle connection recycling request."""
349
+ if self.state == ConnectionState.RECYCLING:
350
+ return {"status": "already_recycling"}
351
+
352
+ self.state = ConnectionState.RECYCLING
353
+
354
+ # Notify supervisor if present
355
+ if self.supervisor:
356
+ await self.supervisor.notify_recycling(self.id)
357
+
358
+ return {"status": "recycling_initiated"}
359
+
360
+ def _get_stats(self) -> Dict[str, Any]:
361
+ """Get connection statistics."""
362
+ uptime = time.time() - self._created_at
363
+ idle_time = time.time() - self._last_activity
364
+
365
+ return {
366
+ "connection_id": self.id,
367
+ "state": self.state.value,
368
+ "stats": {
369
+ "queries_executed": self.stats.queries_executed,
370
+ "errors_encountered": self.stats.errors_encountered,
371
+ "avg_execution_time": (
372
+ self.stats.total_execution_time / self.stats.queries_executed
373
+ if self.stats.queries_executed > 0
374
+ else 0
375
+ ),
376
+ "health_checks_passed": self.stats.health_checks_passed,
377
+ "health_checks_failed": self.stats.health_checks_failed,
378
+ "health_score": self.stats.health_score,
379
+ "uptime_seconds": uptime,
380
+ "idle_seconds": idle_time,
381
+ },
382
+ }
383
+
384
+ async def _monitor_health(self):
385
+ """Background health monitoring task."""
386
+ while self.state not in [ConnectionState.TERMINATED, ConnectionState.RECYCLING]:
387
+ try:
388
+ await asyncio.sleep(self.health_check_interval)
389
+
390
+ # Send health check message to self
391
+ health_result = await self.send_message(
392
+ Message(type=MessageType.HEALTH_CHECK)
393
+ )
394
+
395
+ # Update state based on health
396
+ if not health_result.get("healthy"):
397
+ if self.stats.health_score < 30:
398
+ self.state = ConnectionState.FAILED
399
+ if self.supervisor:
400
+ await self.supervisor.notify_failure(self.id)
401
+ elif self.stats.health_score < 50:
402
+ self.state = ConnectionState.DEGRADED
403
+ else:
404
+ if (
405
+ self.state == ConnectionState.DEGRADED
406
+ and self.stats.health_score > 70
407
+ ):
408
+ self.state = ConnectionState.HEALTHY
409
+
410
+ except Exception as e:
411
+ logger.error(f"Health monitor error in actor {self.id}: {e}")
412
+
413
+ def _should_recycle(self) -> bool:
414
+ """Check if connection should be recycled."""
415
+ # Check lifetime
416
+ if time.time() - self._created_at > self.max_lifetime:
417
+ return True
418
+
419
+ # Check idle time
420
+ if time.time() - self._last_activity > self.max_idle_time:
421
+ return True
422
+
423
+ # Check health score
424
+ if self.stats.health_score < 20:
425
+ return True
426
+
427
+ return False
428
+
429
+ async def _initiate_recycle(self):
430
+ """Initiate connection recycling."""
431
+ await self.send_message(Message(type=MessageType.RECYCLE))
432
+
433
+ def _update_health_score(self, delta: float):
434
+ """Update health score with bounds checking."""
435
+ self.stats.health_score = max(0, min(100, self.stats.health_score + delta))
436
+
437
+ # Update state based on score
438
+ if self.stats.health_score < 30:
439
+ self.state = ConnectionState.DEGRADED
440
+ elif self.stats.health_score > 70 and self.state == ConnectionState.DEGRADED:
441
+ self.state = ConnectionState.HEALTHY
442
+
443
+ async def _connect(self):
444
+ """Establish physical database connection."""
445
+ # Import here to avoid circular dependencies
446
+ from kailash.nodes.data.async_sql import (
447
+ DatabaseConfig,
448
+ DatabaseType,
449
+ MySQLAdapter,
450
+ PostgreSQLAdapter,
451
+ SQLiteAdapter,
452
+ )
453
+
454
+ # Determine database type
455
+ db_type = DatabaseType(self.db_config["type"].lower())
456
+
457
+ # Create configuration
458
+ config = DatabaseConfig(
459
+ type=db_type,
460
+ host=self.db_config.get("host"),
461
+ port=self.db_config.get("port"),
462
+ database=self.db_config.get("database"),
463
+ user=self.db_config.get("user"),
464
+ password=self.db_config.get("password"),
465
+ connection_string=self.db_config.get("connection_string"),
466
+ pool_size=1, # Single connection per actor
467
+ max_pool_size=1,
468
+ )
469
+
470
+ # Create adapter
471
+ if db_type == DatabaseType.POSTGRESQL:
472
+ self._adapter = PostgreSQLAdapter(config)
473
+ elif db_type == DatabaseType.MYSQL:
474
+ self._adapter = MySQLAdapter(config)
475
+ elif db_type == DatabaseType.SQLITE:
476
+ self._adapter = SQLiteAdapter(config)
477
+ else:
478
+ raise ValueError(f"Unsupported database type: {db_type}")
479
+
480
+ # Connect
481
+ await self._adapter.connect()
482
+ logger.info(f"Connection actor {self.id} connected to {db_type.value}")
483
+
484
+ async def _disconnect(self):
485
+ """Close physical database connection."""
486
+ if self._adapter:
487
+ await self._adapter.disconnect()
488
+ self._adapter = None
489
+ logger.info(f"Connection actor {self.id} disconnected")
490
+
491
+
492
+ class ConnectionActor:
493
+ """
494
+ High-level interface for interacting with actor connections.
495
+
496
+ This class provides a convenient API for sending messages to
497
+ connection actors without dealing with low-level message passing.
498
+ """
499
+
500
+ def __init__(self, actor: ActorConnection):
501
+ """
502
+ Initialize connection actor interface.
503
+
504
+ Args:
505
+ actor: The underlying actor connection
506
+ """
507
+ self.actor = actor
508
+
509
+ async def execute(
510
+ self,
511
+ query: str,
512
+ params: Optional[Union[tuple, dict]] = None,
513
+ fetch_mode: str = "all",
514
+ ) -> QueryResult:
515
+ """
516
+ Execute a database query.
517
+
518
+ Args:
519
+ query: SQL query to execute
520
+ params: Query parameters
521
+ fetch_mode: How to fetch results (one, all, many)
522
+
523
+ Returns:
524
+ Query result with data or error
525
+ """
526
+ return await self.actor.send_message(
527
+ Message(
528
+ type=MessageType.QUERY,
529
+ payload={"query": query, "params": params, "fetch_mode": fetch_mode},
530
+ )
531
+ )
532
+
533
+ async def health_check(self) -> Dict[str, Any]:
534
+ """Perform a health check on the connection."""
535
+ return await self.actor.send_message(Message(type=MessageType.HEALTH_CHECK))
536
+
537
+ async def get_stats(self) -> Dict[str, Any]:
538
+ """Get connection statistics."""
539
+ return await self.actor.send_message(Message(type=MessageType.GET_STATS))
540
+
541
+ async def recycle(self) -> Dict[str, Any]:
542
+ """Request connection recycling."""
543
+ return await self.actor.send_message(Message(type=MessageType.RECYCLE))
544
+
545
+ async def ping(self) -> Dict[str, Any]:
546
+ """Ping the connection actor."""
547
+ return await self.actor.send_message(Message(type=MessageType.PING))
548
+
549
+ @property
550
+ def id(self) -> str:
551
+ """Get connection ID."""
552
+ return self.actor.id
553
+
554
+ @property
555
+ def state(self) -> ConnectionState:
556
+ """Get current connection state."""
557
+ return self.actor.state
558
+
559
+ @property
560
+ def health_score(self) -> float:
561
+ """Get current health score."""
562
+ return self.actor.stats.health_score
563
+
564
+ async def stop(self):
565
+ """Stop the underlying actor connection."""
566
+ await self.actor.stop()