kailash 0.4.2__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 (64) 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/database/repositories.py +3 -1
  19. kailash/middleware/gateway/__init__.py +22 -0
  20. kailash/middleware/gateway/checkpoint_manager.py +398 -0
  21. kailash/middleware/gateway/deduplicator.py +382 -0
  22. kailash/middleware/gateway/durable_gateway.py +417 -0
  23. kailash/middleware/gateway/durable_request.py +498 -0
  24. kailash/middleware/gateway/event_store.py +459 -0
  25. kailash/nodes/admin/audit_log.py +364 -6
  26. kailash/nodes/admin/permission_check.py +817 -33
  27. kailash/nodes/admin/role_management.py +1242 -108
  28. kailash/nodes/admin/schema_manager.py +438 -0
  29. kailash/nodes/admin/user_management.py +1209 -681
  30. kailash/nodes/api/http.py +95 -71
  31. kailash/nodes/base.py +281 -164
  32. kailash/nodes/base_async.py +30 -31
  33. kailash/nodes/code/__init__.py +8 -1
  34. kailash/nodes/code/async_python.py +1035 -0
  35. kailash/nodes/code/python.py +1 -0
  36. kailash/nodes/data/async_sql.py +12 -25
  37. kailash/nodes/data/sql.py +20 -11
  38. kailash/nodes/data/workflow_connection_pool.py +643 -0
  39. kailash/nodes/rag/__init__.py +1 -4
  40. kailash/resources/__init__.py +40 -0
  41. kailash/resources/factory.py +533 -0
  42. kailash/resources/health.py +319 -0
  43. kailash/resources/reference.py +288 -0
  44. kailash/resources/registry.py +392 -0
  45. kailash/runtime/async_local.py +711 -302
  46. kailash/testing/__init__.py +34 -0
  47. kailash/testing/async_test_case.py +353 -0
  48. kailash/testing/async_utils.py +345 -0
  49. kailash/testing/fixtures.py +458 -0
  50. kailash/testing/mock_registry.py +495 -0
  51. kailash/utils/resource_manager.py +420 -0
  52. kailash/workflow/__init__.py +8 -0
  53. kailash/workflow/async_builder.py +621 -0
  54. kailash/workflow/async_patterns.py +766 -0
  55. kailash/workflow/builder.py +93 -10
  56. kailash/workflow/cyclic_runner.py +111 -41
  57. kailash/workflow/graph.py +7 -2
  58. kailash/workflow/resilience.py +11 -1
  59. {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/METADATA +12 -7
  60. {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/RECORD +64 -28
  61. {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/WHEEL +0 -0
  62. {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/entry_points.txt +0 -0
  63. {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/licenses/LICENSE +0 -0
  64. {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,533 @@
1
+ """
2
+ Resource Factories - Create and configure resources for the registry.
3
+
4
+ This module provides abstract factory interface and concrete implementations
5
+ for common resource types used in Kailash workflows.
6
+ """
7
+
8
+ import asyncio
9
+ import logging
10
+ from abc import ABC, abstractmethod
11
+ from typing import Any, Dict, Optional
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class ResourceFactory(ABC):
17
+ """
18
+ Abstract factory for creating resources.
19
+
20
+ All resource factories must implement this interface to be used
21
+ with the ResourceRegistry.
22
+ """
23
+
24
+ @abstractmethod
25
+ async def create(self) -> Any:
26
+ """
27
+ Create and return a resource.
28
+
29
+ This method should:
30
+ - Create the resource (e.g., database connection pool)
31
+ - Perform any initialization
32
+ - Return the ready-to-use resource
33
+
34
+ Returns:
35
+ The created resource
36
+ """
37
+ pass
38
+
39
+ @abstractmethod
40
+ def get_config(self) -> Dict[str, Any]:
41
+ """
42
+ Get factory configuration for serialization.
43
+
44
+ This is used for debugging and documentation purposes.
45
+
46
+ Returns:
47
+ Dictionary of configuration parameters
48
+ """
49
+ pass
50
+
51
+
52
+ class DatabasePoolFactory(ResourceFactory):
53
+ """
54
+ Factory for creating database connection pools.
55
+
56
+ Supports multiple database backends:
57
+ - PostgreSQL (asyncpg)
58
+ - MySQL (aiomysql)
59
+ - SQLite (aiosqlite)
60
+
61
+ Example:
62
+ ```python
63
+ factory = DatabasePoolFactory(
64
+ backend='postgresql',
65
+ host='localhost',
66
+ port=5432,
67
+ database='myapp',
68
+ user='dbuser',
69
+ password='secret',
70
+ min_size=5,
71
+ max_size=20
72
+ )
73
+ ```
74
+ """
75
+
76
+ def __init__(
77
+ self,
78
+ backend: str = "postgresql",
79
+ host: str = "localhost",
80
+ port: Optional[int] = None,
81
+ database: str = "test",
82
+ user: Optional[str] = None,
83
+ password: Optional[str] = None,
84
+ min_size: int = 5,
85
+ max_size: int = 20,
86
+ **kwargs,
87
+ ):
88
+ """Initialize database pool factory."""
89
+ self.backend = backend.lower()
90
+ self.host = host
91
+ self.port = port or self._default_port()
92
+ self.database = database
93
+ self.user = user
94
+ self.password = password
95
+ self.min_size = min_size
96
+ self.max_size = max_size
97
+ self.extra_config = kwargs
98
+
99
+ def _default_port(self) -> int:
100
+ """Get default port for backend."""
101
+ return {
102
+ "postgresql": 5432,
103
+ "postgres": 5432,
104
+ "mysql": 3306,
105
+ "sqlite": None,
106
+ }.get(self.backend, 5432)
107
+
108
+ async def create(self) -> Any:
109
+ """Create database connection pool."""
110
+ if self.backend in ("postgresql", "postgres"):
111
+ return await self._create_postgres_pool()
112
+ elif self.backend == "mysql":
113
+ return await self._create_mysql_pool()
114
+ elif self.backend == "sqlite":
115
+ return await self._create_sqlite_pool()
116
+ else:
117
+ raise ValueError(f"Unsupported database backend: {self.backend}")
118
+
119
+ async def _create_postgres_pool(self):
120
+ """Create PostgreSQL connection pool."""
121
+ try:
122
+ import asyncpg
123
+ except ImportError:
124
+ raise ImportError(
125
+ "asyncpg is required for PostgreSQL. "
126
+ "Install with: pip install asyncpg"
127
+ )
128
+
129
+ # Use default user if not provided
130
+ user = self.user or "postgres"
131
+ password = self.password or ""
132
+
133
+ # Extract options if present
134
+ options = self.extra_config.pop("options", None)
135
+
136
+ # Build DSN
137
+ dsn = f"postgresql://{user}:{password}@{self.host}:{self.port}/{self.database}"
138
+ if options:
139
+ # Add options to DSN as query parameters
140
+ dsn += f"?options={options}"
141
+
142
+ logger.info(
143
+ f"Creating PostgreSQL pool: {self.host}:{self.port}/{self.database}"
144
+ )
145
+
146
+ return await asyncpg.create_pool(
147
+ dsn, min_size=self.min_size, max_size=self.max_size, **self.extra_config
148
+ )
149
+
150
+ async def _create_mysql_pool(self):
151
+ """Create MySQL connection pool."""
152
+ try:
153
+ import aiomysql
154
+ except ImportError:
155
+ raise ImportError(
156
+ "aiomysql is required for MySQL. " "Install with: pip install aiomysql"
157
+ )
158
+
159
+ logger.info(f"Creating MySQL pool: {self.host}:{self.port}/{self.database}")
160
+
161
+ return await aiomysql.create_pool(
162
+ host=self.host,
163
+ port=self.port,
164
+ user=self.user,
165
+ password=self.password,
166
+ db=self.database,
167
+ minsize=self.min_size,
168
+ maxsize=self.max_size,
169
+ **self.extra_config,
170
+ )
171
+
172
+ async def _create_sqlite_pool(self):
173
+ """Create SQLite connection."""
174
+ try:
175
+ import aiosqlite
176
+ except ImportError:
177
+ raise ImportError(
178
+ "aiosqlite is required for SQLite. "
179
+ "Install with: pip install aiosqlite"
180
+ )
181
+
182
+ logger.info(f"Creating SQLite connection: {self.database}")
183
+
184
+ # SQLite doesn't have pools, return a connection
185
+ return await aiosqlite.connect(self.database, **self.extra_config)
186
+
187
+ def get_config(self) -> Dict[str, Any]:
188
+ """Get factory configuration."""
189
+ config = {
190
+ "backend": self.backend,
191
+ "host": self.host,
192
+ "port": self.port,
193
+ "database": self.database,
194
+ "user": self.user,
195
+ "min_size": self.min_size,
196
+ "max_size": self.max_size,
197
+ }
198
+ config.update(self.extra_config)
199
+ # Don't include password in config
200
+ return {k: v for k, v in config.items() if k != "password"}
201
+
202
+
203
+ class HttpClientFactory(ResourceFactory):
204
+ """
205
+ Factory for creating HTTP clients.
206
+
207
+ Supports:
208
+ - aiohttp
209
+ - httpx
210
+
211
+ Example:
212
+ ```python
213
+ factory = HttpClientFactory(
214
+ backend='aiohttp',
215
+ base_url='https://api.example.com',
216
+ timeout=30,
217
+ headers={'Authorization': 'Bearer token'}
218
+ )
219
+ ```
220
+ """
221
+
222
+ def __init__(
223
+ self,
224
+ backend: str = "aiohttp",
225
+ base_url: Optional[str] = None,
226
+ timeout: int = 30,
227
+ headers: Optional[Dict[str, str]] = None,
228
+ **kwargs,
229
+ ):
230
+ """Initialize HTTP client factory."""
231
+ self.backend = backend.lower()
232
+ self.base_url = base_url
233
+ self.timeout = timeout
234
+ self.headers = headers or {}
235
+ self.extra_config = kwargs
236
+
237
+ async def create(self) -> Any:
238
+ """Create HTTP client."""
239
+ if self.backend == "aiohttp":
240
+ return await self._create_aiohttp_client()
241
+ elif self.backend == "httpx":
242
+ return self._create_httpx_client()
243
+ else:
244
+ raise ValueError(f"Unsupported HTTP backend: {self.backend}")
245
+
246
+ async def _create_aiohttp_client(self):
247
+ """Create aiohttp client session."""
248
+ try:
249
+ import aiohttp
250
+ except ImportError:
251
+ raise ImportError("aiohttp is required. Install with: pip install aiohttp")
252
+
253
+ logger.info(f"Creating aiohttp client: {self.base_url}")
254
+
255
+ timeout = aiohttp.ClientTimeout(total=self.timeout)
256
+ connector = aiohttp.TCPConnector(limit=100)
257
+
258
+ return aiohttp.ClientSession(
259
+ base_url=self.base_url,
260
+ timeout=timeout,
261
+ headers=self.headers,
262
+ connector=connector,
263
+ **self.extra_config,
264
+ )
265
+
266
+ def _create_httpx_client(self):
267
+ """Create httpx async client."""
268
+ try:
269
+ import httpx
270
+ except ImportError:
271
+ raise ImportError("httpx is required. Install with: pip install httpx")
272
+
273
+ logger.info(f"Creating httpx client: {self.base_url}")
274
+
275
+ return httpx.AsyncClient(
276
+ base_url=self.base_url,
277
+ timeout=self.timeout,
278
+ headers=self.headers,
279
+ **self.extra_config,
280
+ )
281
+
282
+ def get_config(self) -> Dict[str, Any]:
283
+ """Get factory configuration."""
284
+ config = {
285
+ "backend": self.backend,
286
+ "base_url": self.base_url,
287
+ "timeout": self.timeout,
288
+ "headers": {
289
+ k: "***" if "auth" in k.lower() else v for k, v in self.headers.items()
290
+ },
291
+ }
292
+ config.update(self.extra_config)
293
+ return config
294
+
295
+
296
+ class CacheFactory(ResourceFactory):
297
+ """
298
+ Factory for creating cache clients.
299
+
300
+ Supports:
301
+ - Redis (aioredis)
302
+ - Memcached (aiomemcache)
303
+ - In-memory cache
304
+
305
+ Example:
306
+ ```python
307
+ factory = CacheFactory(
308
+ backend='redis',
309
+ host='localhost',
310
+ port=6379,
311
+ db=0
312
+ )
313
+ ```
314
+ """
315
+
316
+ def __init__(
317
+ self,
318
+ backend: str = "redis",
319
+ host: str = "localhost",
320
+ port: Optional[int] = None,
321
+ **kwargs,
322
+ ):
323
+ """Initialize cache factory."""
324
+ self.backend = backend.lower()
325
+ self.host = host
326
+ self.port = port or self._default_port()
327
+ self.extra_config = kwargs
328
+
329
+ def _default_port(self) -> int:
330
+ """Get default port for backend."""
331
+ return {"redis": 6379, "memcached": 11211, "memory": None}.get(
332
+ self.backend, 6379
333
+ )
334
+
335
+ async def create(self) -> Any:
336
+ """Create cache client."""
337
+ if self.backend == "redis":
338
+ return await self._create_redis_client()
339
+ elif self.backend == "memcached":
340
+ return await self._create_memcached_client()
341
+ elif self.backend == "memory":
342
+ return self._create_memory_cache()
343
+ else:
344
+ raise ValueError(f"Unsupported cache backend: {self.backend}")
345
+
346
+ async def _create_redis_client(self):
347
+ """Create Redis client."""
348
+ try:
349
+ import redis.asyncio as aioredis
350
+ except ImportError:
351
+ try:
352
+ import aioredis
353
+ except ImportError:
354
+ raise ImportError(
355
+ "redis or aioredis is required. "
356
+ "Install with: pip install redis[async] or pip install aioredis"
357
+ )
358
+
359
+ logger.info(f"Creating Redis client: {self.host}:{self.port}")
360
+
361
+ return await aioredis.from_url(
362
+ f"redis://{self.host}:{self.port}", **self.extra_config
363
+ )
364
+
365
+ async def _create_memcached_client(self):
366
+ """Create Memcached client."""
367
+ try:
368
+ import aiomemcache
369
+ except ImportError:
370
+ raise ImportError(
371
+ "aiomemcache is required. " "Install with: pip install aiomemcache"
372
+ )
373
+
374
+ logger.info(f"Creating Memcached client: {self.host}:{self.port}")
375
+
376
+ client = aiomemcache.Client(self.host, self.port)
377
+ await client.connect()
378
+ return client
379
+
380
+ def _create_memory_cache(self):
381
+ """Create in-memory cache."""
382
+ logger.info("Creating in-memory cache")
383
+
384
+ class MemoryCache:
385
+ """Simple in-memory cache implementation."""
386
+
387
+ def __init__(self):
388
+ self._cache = {}
389
+
390
+ async def get(self, key: str) -> Any:
391
+ return self._cache.get(key)
392
+
393
+ async def set(self, key: str, value: Any, expire: int = None) -> None:
394
+ self._cache[key] = value
395
+ # TODO: Implement expiration
396
+
397
+ async def delete(self, key: str) -> None:
398
+ self._cache.pop(key, None)
399
+
400
+ async def clear(self) -> None:
401
+ self._cache.clear()
402
+
403
+ return MemoryCache()
404
+
405
+ def get_config(self) -> Dict[str, Any]:
406
+ """Get factory configuration."""
407
+ config = {"backend": self.backend, "host": self.host, "port": self.port}
408
+ config.update(self.extra_config)
409
+ return config
410
+
411
+
412
+ class MessageQueueFactory(ResourceFactory):
413
+ """
414
+ Factory for creating message queue clients.
415
+
416
+ Supports:
417
+ - RabbitMQ (aio-pika)
418
+ - Kafka (aiokafka)
419
+ - Redis Pub/Sub
420
+
421
+ Example:
422
+ ```python
423
+ factory = MessageQueueFactory(
424
+ backend='rabbitmq',
425
+ host='localhost',
426
+ port=5672,
427
+ username='guest',
428
+ password='guest'
429
+ )
430
+ ```
431
+ """
432
+
433
+ def __init__(
434
+ self,
435
+ backend: str = "rabbitmq",
436
+ host: str = "localhost",
437
+ port: Optional[int] = None,
438
+ username: Optional[str] = None,
439
+ password: Optional[str] = None,
440
+ **kwargs,
441
+ ):
442
+ """Initialize message queue factory."""
443
+ self.backend = backend.lower()
444
+ self.host = host
445
+ self.port = port or self._default_port()
446
+ self.username = username
447
+ self.password = password
448
+ self.extra_config = kwargs
449
+
450
+ def _default_port(self) -> int:
451
+ """Get default port for backend."""
452
+ return {"rabbitmq": 5672, "kafka": 9092, "redis": 6379}.get(self.backend, 5672)
453
+
454
+ async def create(self) -> Any:
455
+ """Create message queue client."""
456
+ if self.backend == "rabbitmq":
457
+ return await self._create_rabbitmq_client()
458
+ elif self.backend == "kafka":
459
+ return await self._create_kafka_client()
460
+ elif self.backend == "redis":
461
+ return await self._create_redis_pubsub()
462
+ else:
463
+ raise ValueError(f"Unsupported message queue backend: {self.backend}")
464
+
465
+ async def _create_rabbitmq_client(self):
466
+ """Create RabbitMQ client."""
467
+ try:
468
+ import aio_pika
469
+ except ImportError:
470
+ raise ImportError(
471
+ "aio-pika is required for RabbitMQ. "
472
+ "Install with: pip install aio-pika"
473
+ )
474
+
475
+ logger.info(f"Creating RabbitMQ connection: {self.host}:{self.port}")
476
+
477
+ url = f"amqp://{self.username}:{self.password}@{self.host}:{self.port}/"
478
+
479
+ return await aio_pika.connect_robust(url, **self.extra_config)
480
+
481
+ async def _create_kafka_client(self):
482
+ """Create Kafka producer/consumer."""
483
+ try:
484
+ from aiokafka import AIOKafkaConsumer, AIOKafkaProducer
485
+ except ImportError:
486
+ raise ImportError(
487
+ "aiokafka is required for Kafka. " "Install with: pip install aiokafka"
488
+ )
489
+
490
+ logger.info(f"Creating Kafka client: {self.host}:{self.port}")
491
+
492
+ # Return both producer and consumer
493
+ producer = AIOKafkaProducer(
494
+ bootstrap_servers=f"{self.host}:{self.port}", **self.extra_config
495
+ )
496
+
497
+ consumer = AIOKafkaConsumer(
498
+ bootstrap_servers=f"{self.host}:{self.port}", **self.extra_config
499
+ )
500
+
501
+ await producer.start()
502
+ await consumer.start()
503
+
504
+ class KafkaClient:
505
+ def __init__(self, producer, consumer):
506
+ self.producer = producer
507
+ self.consumer = consumer
508
+
509
+ async def close(self):
510
+ await self.producer.stop()
511
+ await self.consumer.stop()
512
+
513
+ return KafkaClient(producer, consumer)
514
+
515
+ async def _create_redis_pubsub(self):
516
+ """Create Redis Pub/Sub client."""
517
+ # Reuse cache factory for Redis
518
+ cache_factory = CacheFactory(
519
+ backend="redis", host=self.host, port=self.port, **self.extra_config
520
+ )
521
+ return await cache_factory.create()
522
+
523
+ def get_config(self) -> Dict[str, Any]:
524
+ """Get factory configuration."""
525
+ config = {
526
+ "backend": self.backend,
527
+ "host": self.host,
528
+ "port": self.port,
529
+ "username": self.username,
530
+ }
531
+ config.update(self.extra_config)
532
+ # Don't include password
533
+ return {k: v for k, v in config.items() if k != "password"}