kailash 0.1.5__py3-none-any.whl → 0.2.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 (75) hide show
  1. kailash/__init__.py +1 -1
  2. kailash/access_control.py +740 -0
  3. kailash/api/__main__.py +6 -0
  4. kailash/api/auth.py +668 -0
  5. kailash/api/custom_nodes.py +285 -0
  6. kailash/api/custom_nodes_secure.py +377 -0
  7. kailash/api/database.py +620 -0
  8. kailash/api/studio.py +915 -0
  9. kailash/api/studio_secure.py +893 -0
  10. kailash/mcp/__init__.py +53 -0
  11. kailash/mcp/__main__.py +13 -0
  12. kailash/mcp/ai_registry_server.py +712 -0
  13. kailash/mcp/client.py +447 -0
  14. kailash/mcp/client_new.py +334 -0
  15. kailash/mcp/server.py +293 -0
  16. kailash/mcp/server_new.py +336 -0
  17. kailash/mcp/servers/__init__.py +12 -0
  18. kailash/mcp/servers/ai_registry.py +289 -0
  19. kailash/nodes/__init__.py +4 -2
  20. kailash/nodes/ai/__init__.py +2 -0
  21. kailash/nodes/ai/a2a.py +714 -67
  22. kailash/nodes/ai/intelligent_agent_orchestrator.py +31 -37
  23. kailash/nodes/ai/iterative_llm_agent.py +1280 -0
  24. kailash/nodes/ai/llm_agent.py +324 -1
  25. kailash/nodes/ai/self_organizing.py +5 -6
  26. kailash/nodes/base.py +15 -2
  27. kailash/nodes/base_async.py +45 -0
  28. kailash/nodes/base_cycle_aware.py +374 -0
  29. kailash/nodes/base_with_acl.py +338 -0
  30. kailash/nodes/code/python.py +135 -27
  31. kailash/nodes/data/readers.py +16 -6
  32. kailash/nodes/data/writers.py +16 -6
  33. kailash/nodes/logic/__init__.py +8 -0
  34. kailash/nodes/logic/convergence.py +642 -0
  35. kailash/nodes/logic/loop.py +153 -0
  36. kailash/nodes/logic/operations.py +187 -27
  37. kailash/nodes/mixins/__init__.py +11 -0
  38. kailash/nodes/mixins/mcp.py +228 -0
  39. kailash/nodes/mixins.py +387 -0
  40. kailash/runtime/__init__.py +2 -1
  41. kailash/runtime/access_controlled.py +458 -0
  42. kailash/runtime/local.py +106 -33
  43. kailash/runtime/parallel_cyclic.py +529 -0
  44. kailash/sdk_exceptions.py +90 -5
  45. kailash/security.py +845 -0
  46. kailash/tracking/manager.py +38 -15
  47. kailash/tracking/models.py +1 -1
  48. kailash/tracking/storage/filesystem.py +30 -2
  49. kailash/utils/__init__.py +8 -0
  50. kailash/workflow/__init__.py +18 -0
  51. kailash/workflow/convergence.py +270 -0
  52. kailash/workflow/cycle_analyzer.py +768 -0
  53. kailash/workflow/cycle_builder.py +573 -0
  54. kailash/workflow/cycle_config.py +709 -0
  55. kailash/workflow/cycle_debugger.py +760 -0
  56. kailash/workflow/cycle_exceptions.py +601 -0
  57. kailash/workflow/cycle_profiler.py +671 -0
  58. kailash/workflow/cycle_state.py +338 -0
  59. kailash/workflow/cyclic_runner.py +985 -0
  60. kailash/workflow/graph.py +500 -39
  61. kailash/workflow/migration.py +768 -0
  62. kailash/workflow/safety.py +365 -0
  63. kailash/workflow/templates.py +744 -0
  64. kailash/workflow/validation.py +693 -0
  65. {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/METADATA +256 -12
  66. kailash-0.2.0.dist-info/RECORD +125 -0
  67. kailash/nodes/mcp/__init__.py +0 -11
  68. kailash/nodes/mcp/client.py +0 -554
  69. kailash/nodes/mcp/resource.py +0 -682
  70. kailash/nodes/mcp/server.py +0 -577
  71. kailash-0.1.5.dist-info/RECORD +0 -88
  72. {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/WHEEL +0 -0
  73. {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/entry_points.txt +0 -0
  74. {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/licenses/LICENSE +0 -0
  75. {kailash-0.1.5.dist-info → kailash-0.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,620 @@
1
+ """
2
+ Database models and storage backend for Kailash Workflow Studio.
3
+
4
+ This module provides:
5
+ - SQLAlchemy models for workflows, nodes, executions, and user data
6
+ - Database initialization and migration support
7
+ - Repository classes for data access
8
+ """
9
+
10
+ import uuid
11
+ from contextlib import contextmanager
12
+ from datetime import datetime, timezone
13
+ from pathlib import Path
14
+ from typing import Any, Dict, List, Optional
15
+
16
+ from sqlalchemy import (
17
+ JSON,
18
+ Boolean,
19
+ Column,
20
+ DateTime,
21
+ ForeignKey,
22
+ Index,
23
+ Integer,
24
+ String,
25
+ Text,
26
+ create_engine,
27
+ )
28
+ from sqlalchemy.engine import Engine
29
+ from sqlalchemy.ext.declarative import declarative_base
30
+ from sqlalchemy.orm import Session, relationship, sessionmaker
31
+ from sqlalchemy.sql import func
32
+
33
+ Base = declarative_base()
34
+
35
+
36
+ class Workflow(Base):
37
+ """Workflow database model"""
38
+
39
+ __tablename__ = "workflows"
40
+
41
+ id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
42
+ tenant_id = Column(String(36), nullable=False, index=True)
43
+ name = Column(String(255), nullable=False)
44
+ description = Column(Text)
45
+ definition = Column(JSON, nullable=False)
46
+ version = Column(Integer, default=1)
47
+ is_published = Column(Boolean, default=False)
48
+ created_by = Column(String(255))
49
+ created_at = Column(DateTime, default=func.now())
50
+ updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
51
+
52
+ # Relationships
53
+ executions = relationship(
54
+ "WorkflowExecution", back_populates="workflow", cascade="all, delete-orphan"
55
+ )
56
+ versions = relationship(
57
+ "WorkflowVersion", back_populates="workflow", cascade="all, delete-orphan"
58
+ )
59
+
60
+ __table_args__ = (
61
+ Index("idx_workflow_tenant_created", "tenant_id", "created_at"),
62
+ Index("idx_workflow_tenant_name", "tenant_id", "name"),
63
+ )
64
+
65
+
66
+ class WorkflowVersion(Base):
67
+ """Workflow version history"""
68
+
69
+ __tablename__ = "workflow_versions"
70
+
71
+ id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
72
+ workflow_id = Column(String(36), ForeignKey("workflows.id"), nullable=False)
73
+ version = Column(Integer, nullable=False)
74
+ definition = Column(JSON, nullable=False)
75
+ change_message = Column(Text)
76
+ created_by = Column(String(255))
77
+ created_at = Column(DateTime, default=func.now())
78
+
79
+ # Relationships
80
+ workflow = relationship("Workflow", back_populates="versions")
81
+
82
+ __table_args__ = (
83
+ Index("idx_version_workflow_version", "workflow_id", "version", unique=True),
84
+ )
85
+
86
+
87
+ class CustomNode(Base):
88
+ """Custom node definitions created by users"""
89
+
90
+ __tablename__ = "custom_nodes"
91
+
92
+ id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
93
+ tenant_id = Column(String(36), nullable=False, index=True)
94
+ name = Column(String(255), nullable=False)
95
+ category = Column(String(100), default="custom")
96
+ description = Column(Text)
97
+ icon = Column(String(50))
98
+ color = Column(String(7)) # Hex color
99
+
100
+ # Node configuration
101
+ parameters = Column(JSON) # Parameter definitions
102
+ inputs = Column(JSON) # Input port definitions
103
+ outputs = Column(JSON) # Output port definitions
104
+
105
+ # Implementation
106
+ implementation_type = Column(String(50)) # 'python', 'workflow', 'api'
107
+ implementation = Column(JSON) # Implementation details
108
+
109
+ # Metadata
110
+ is_published = Column(Boolean, default=False)
111
+ created_by = Column(String(255))
112
+ created_at = Column(DateTime, default=func.now())
113
+ updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
114
+
115
+ __table_args__ = (
116
+ Index("idx_node_tenant_name", "tenant_id", "name", unique=True),
117
+ Index("idx_node_tenant_category", "tenant_id", "category"),
118
+ )
119
+
120
+
121
+ class WorkflowExecution(Base):
122
+ """Workflow execution history"""
123
+
124
+ __tablename__ = "workflow_executions"
125
+
126
+ id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
127
+ workflow_id = Column(String(36), ForeignKey("workflows.id"), nullable=False)
128
+ tenant_id = Column(String(36), nullable=False, index=True)
129
+ status = Column(
130
+ String(50), nullable=False
131
+ ) # 'pending', 'running', 'completed', 'failed'
132
+
133
+ # Execution details
134
+ parameters = Column(JSON)
135
+ result = Column(JSON)
136
+ error = Column(Text)
137
+
138
+ # Performance metrics
139
+ started_at = Column(DateTime)
140
+ completed_at = Column(DateTime)
141
+ execution_time_ms = Column(Integer)
142
+
143
+ # Node execution details
144
+ node_executions = Column(JSON) # Detailed per-node execution data
145
+
146
+ # Relationships
147
+ workflow = relationship("Workflow", back_populates="executions")
148
+
149
+ __table_args__ = (
150
+ Index("idx_execution_tenant_started", "tenant_id", "started_at"),
151
+ Index("idx_execution_workflow_started", "workflow_id", "started_at"),
152
+ Index("idx_execution_status", "status"),
153
+ )
154
+
155
+
156
+ class UserPreferences(Base):
157
+ """User preferences and settings"""
158
+
159
+ __tablename__ = "user_preferences"
160
+
161
+ id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
162
+ tenant_id = Column(String(36), nullable=False)
163
+ user_id = Column(String(255), nullable=False)
164
+
165
+ # UI preferences
166
+ theme = Column(String(20), default="light")
167
+ canvas_settings = Column(JSON) # Zoom level, grid settings, etc.
168
+
169
+ # Workflow preferences
170
+ default_parameters = Column(JSON)
171
+ favorite_nodes = Column(JSON)
172
+ recent_workflows = Column(JSON)
173
+
174
+ created_at = Column(DateTime, default=func.now())
175
+ updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
176
+
177
+ __table_args__ = (
178
+ Index("idx_pref_tenant_user", "tenant_id", "user_id", unique=True),
179
+ )
180
+
181
+
182
+ class WorkflowTemplate(Base):
183
+ """Pre-built workflow templates"""
184
+
185
+ __tablename__ = "workflow_templates"
186
+
187
+ id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
188
+ tenant_id = Column(String(36), nullable=True) # Null for global templates
189
+ name = Column(String(255), nullable=False)
190
+ category = Column(String(100))
191
+ description = Column(Text)
192
+ thumbnail = Column(String(255))
193
+
194
+ # Template definition
195
+ definition = Column(JSON, nullable=False)
196
+ default_parameters = Column(JSON)
197
+
198
+ # Metadata
199
+ is_public = Column(Boolean, default=False)
200
+ usage_count = Column(Integer, default=0)
201
+ created_by = Column(String(255))
202
+ created_at = Column(DateTime, default=func.now())
203
+ updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
204
+
205
+ __table_args__ = (
206
+ Index("idx_template_category", "category"),
207
+ Index("idx_template_public", "is_public"),
208
+ )
209
+
210
+
211
+ class WorkflowPermission(Base):
212
+ """Workflow-level permissions"""
213
+
214
+ __tablename__ = "workflow_permissions"
215
+
216
+ id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
217
+ workflow_id = Column(String(36), ForeignKey("workflows.id"), nullable=False)
218
+ tenant_id = Column(String(36), nullable=False, index=True)
219
+
220
+ # Who does this permission apply to?
221
+ user_id = Column(String(36), nullable=True) # Specific user
222
+ role = Column(String(50), nullable=True) # Role-based
223
+ group_id = Column(String(36), nullable=True) # Group-based
224
+
225
+ # What permission?
226
+ permission = Column(
227
+ String(50), nullable=False
228
+ ) # view, execute, modify, delete, share, admin
229
+ effect = Column(
230
+ String(20), nullable=False, default="allow"
231
+ ) # allow, deny, conditional
232
+
233
+ # Conditions (JSON object for flexibility)
234
+ conditions = Column(JSON)
235
+
236
+ # Metadata
237
+ created_by = Column(String(255))
238
+ created_at = Column(DateTime, default=func.now())
239
+ expires_at = Column(DateTime, nullable=True)
240
+
241
+ # Relationships
242
+ workflow = relationship("Workflow", backref="permissions")
243
+
244
+ __table_args__ = (
245
+ Index("idx_workflow_perm_workflow", "workflow_id"),
246
+ Index("idx_workflow_perm_user", "user_id"),
247
+ Index("idx_workflow_perm_tenant", "tenant_id"),
248
+ )
249
+
250
+
251
+ class NodePermission(Base):
252
+ """Node-level permissions"""
253
+
254
+ __tablename__ = "node_permissions"
255
+
256
+ id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
257
+ workflow_id = Column(String(36), ForeignKey("workflows.id"), nullable=False)
258
+ node_id = Column(String(255), nullable=False) # Node ID within workflow
259
+ tenant_id = Column(String(36), nullable=False, index=True)
260
+
261
+ # Who does this permission apply to?
262
+ user_id = Column(String(36), nullable=True)
263
+ role = Column(String(50), nullable=True)
264
+ group_id = Column(String(36), nullable=True)
265
+
266
+ # What permission?
267
+ permission = Column(
268
+ String(50), nullable=False
269
+ ) # execute, read_output, write_input, skip, mask_output
270
+ effect = Column(String(20), nullable=False, default="allow")
271
+
272
+ # Conditions and special handling
273
+ conditions = Column(JSON)
274
+ masked_fields = Column(JSON) # Fields to mask in output
275
+ redirect_node = Column(String(255)) # Alternative node if access denied
276
+
277
+ # Metadata
278
+ created_by = Column(String(255))
279
+ created_at = Column(DateTime, default=func.now())
280
+ expires_at = Column(DateTime, nullable=True)
281
+
282
+ # Relationships
283
+ workflow = relationship("Workflow", backref="node_permissions")
284
+
285
+ __table_args__ = (
286
+ Index("idx_node_perm_workflow", "workflow_id"),
287
+ Index("idx_node_perm_node", "workflow_id", "node_id"),
288
+ Index("idx_node_perm_user", "user_id"),
289
+ Index("idx_node_perm_tenant", "tenant_id"),
290
+ )
291
+
292
+
293
+ class AccessLog(Base):
294
+ """Audit log for access attempts"""
295
+
296
+ __tablename__ = "access_logs"
297
+
298
+ id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
299
+ tenant_id = Column(String(36), nullable=False, index=True)
300
+ user_id = Column(String(36), nullable=False, index=True)
301
+
302
+ # What was accessed?
303
+ resource_type = Column(String(50), nullable=False) # workflow, node
304
+ resource_id = Column(String(255), nullable=False)
305
+ permission = Column(String(50), nullable=False)
306
+
307
+ # Result
308
+ allowed = Column(Boolean, nullable=False)
309
+ reason = Column(Text)
310
+
311
+ # Context
312
+ ip_address = Column(String(50))
313
+ user_agent = Column(String(255))
314
+ session_id = Column(String(36))
315
+
316
+ # Timestamp
317
+ timestamp = Column(DateTime, default=func.now(), index=True)
318
+
319
+ __table_args__ = (
320
+ Index("idx_access_log_user_time", "user_id", "timestamp"),
321
+ Index("idx_access_log_resource", "resource_type", "resource_id", "timestamp"),
322
+ )
323
+
324
+
325
+ class UserGroup(Base):
326
+ """User groups for permission management"""
327
+
328
+ __tablename__ = "user_groups"
329
+
330
+ id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
331
+ tenant_id = Column(String(36), nullable=False, index=True)
332
+ name = Column(String(255), nullable=False)
333
+ description = Column(Text)
334
+
335
+ # Group permissions (can be inherited)
336
+ permissions = Column(JSON)
337
+
338
+ # Metadata
339
+ created_by = Column(String(255))
340
+ created_at = Column(DateTime, default=func.now())
341
+ updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
342
+
343
+ # Relationships
344
+ members = relationship(
345
+ "UserGroupMember", back_populates="group", cascade="all, delete-orphan"
346
+ )
347
+
348
+ __table_args__ = (Index("idx_group_tenant_name", "tenant_id", "name", unique=True),)
349
+
350
+
351
+ class UserGroupMember(Base):
352
+ """User membership in groups"""
353
+
354
+ __tablename__ = "user_group_members"
355
+
356
+ id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
357
+ group_id = Column(String(36), ForeignKey("user_groups.id"), nullable=False)
358
+ user_id = Column(String(36), nullable=False)
359
+
360
+ # Membership details
361
+ role = Column(String(50), default="member") # member, admin
362
+ joined_at = Column(DateTime, default=func.now())
363
+ added_by = Column(String(255))
364
+
365
+ # Relationships
366
+ group = relationship("UserGroup", back_populates="members")
367
+
368
+ __table_args__ = (
369
+ Index("idx_group_member", "group_id", "user_id", unique=True),
370
+ Index("idx_member_user", "user_id"),
371
+ )
372
+
373
+
374
+ # Repository classes for data access
375
+ class WorkflowRepository:
376
+ """Repository for workflow operations"""
377
+
378
+ def __init__(self, session: Session):
379
+ self.session = session
380
+
381
+ def create(
382
+ self,
383
+ tenant_id: str,
384
+ name: str,
385
+ description: str,
386
+ definition: Dict[str, Any],
387
+ created_by: str = None,
388
+ ) -> Workflow:
389
+ """Create a new workflow"""
390
+ workflow = Workflow(
391
+ tenant_id=tenant_id,
392
+ name=name,
393
+ description=description,
394
+ definition=definition,
395
+ created_by=created_by,
396
+ )
397
+ self.session.add(workflow)
398
+ self.session.commit()
399
+
400
+ # Create initial version
401
+ self.create_version(workflow.id, 1, definition, "Initial version", created_by)
402
+
403
+ return workflow
404
+
405
+ def update(
406
+ self, workflow_id: str, updates: Dict[str, Any], updated_by: str = None
407
+ ) -> Workflow:
408
+ """Update a workflow"""
409
+ workflow = self.session.query(Workflow).filter_by(id=workflow_id).first()
410
+ if not workflow:
411
+ raise ValueError(f"Workflow {workflow_id} not found")
412
+
413
+ # Update fields
414
+ for key, value in updates.items():
415
+ if hasattr(workflow, key):
416
+ setattr(workflow, key, value)
417
+
418
+ # If definition changed, create new version
419
+ if "definition" in updates:
420
+ workflow.version += 1
421
+ self.create_version(
422
+ workflow_id,
423
+ workflow.version,
424
+ updates["definition"],
425
+ updates.get("change_message", "Updated workflow"),
426
+ updated_by,
427
+ )
428
+
429
+ self.session.commit()
430
+ return workflow
431
+
432
+ def create_version(
433
+ self,
434
+ workflow_id: str,
435
+ version: int,
436
+ definition: Dict[str, Any],
437
+ change_message: str,
438
+ created_by: str = None,
439
+ ):
440
+ """Create a workflow version"""
441
+ version_record = WorkflowVersion(
442
+ workflow_id=workflow_id,
443
+ version=version,
444
+ definition=definition,
445
+ change_message=change_message,
446
+ created_by=created_by,
447
+ )
448
+ self.session.add(version_record)
449
+ self.session.commit()
450
+
451
+ def get(self, workflow_id: str) -> Optional[Workflow]:
452
+ """Get a workflow by ID"""
453
+ return self.session.query(Workflow).filter_by(id=workflow_id).first()
454
+
455
+ def list(self, tenant_id: str, limit: int = 100, offset: int = 0) -> List[Workflow]:
456
+ """List workflows for a tenant"""
457
+ return (
458
+ self.session.query(Workflow)
459
+ .filter_by(tenant_id=tenant_id)
460
+ .order_by(Workflow.created_at.desc())
461
+ .limit(limit)
462
+ .offset(offset)
463
+ .all()
464
+ )
465
+
466
+ def delete(self, workflow_id: str):
467
+ """Delete a workflow"""
468
+ workflow = self.get(workflow_id)
469
+ if workflow:
470
+ self.session.delete(workflow)
471
+ self.session.commit()
472
+
473
+
474
+ class CustomNodeRepository:
475
+ """Repository for custom node operations"""
476
+
477
+ def __init__(self, session: Session):
478
+ self.session = session
479
+
480
+ def create(self, tenant_id: str, node_data: Dict[str, Any]) -> CustomNode:
481
+ """Create a custom node"""
482
+ node = CustomNode(tenant_id=tenant_id, **node_data)
483
+ self.session.add(node)
484
+ self.session.commit()
485
+ return node
486
+
487
+ def update(self, node_id: str, updates: Dict[str, Any]) -> CustomNode:
488
+ """Update a custom node"""
489
+ node = self.session.query(CustomNode).filter_by(id=node_id).first()
490
+ if not node:
491
+ raise ValueError(f"Custom node {node_id} not found")
492
+
493
+ for key, value in updates.items():
494
+ if hasattr(node, key):
495
+ setattr(node, key, value)
496
+
497
+ self.session.commit()
498
+ return node
499
+
500
+ def list(self, tenant_id: str) -> List[CustomNode]:
501
+ """List custom nodes for a tenant"""
502
+ return (
503
+ self.session.query(CustomNode)
504
+ .filter_by(tenant_id=tenant_id)
505
+ .order_by(CustomNode.category, CustomNode.name)
506
+ .all()
507
+ )
508
+
509
+ def get(self, node_id: str) -> Optional[CustomNode]:
510
+ """Get a custom node by ID"""
511
+ return self.session.query(CustomNode).filter_by(id=node_id).first()
512
+
513
+ def delete(self, node_id: str):
514
+ """Delete a custom node"""
515
+ node = self.get(node_id)
516
+ if node:
517
+ self.session.delete(node)
518
+ self.session.commit()
519
+
520
+
521
+ class ExecutionRepository:
522
+ """Repository for execution operations"""
523
+
524
+ def __init__(self, session: Session):
525
+ self.session = session
526
+
527
+ def create(
528
+ self, workflow_id: str, tenant_id: str, parameters: Dict[str, Any] = None
529
+ ) -> WorkflowExecution:
530
+ """Create an execution record"""
531
+ execution = WorkflowExecution(
532
+ workflow_id=workflow_id,
533
+ tenant_id=tenant_id,
534
+ status="pending",
535
+ parameters=parameters,
536
+ started_at=datetime.now(timezone.utc),
537
+ )
538
+ self.session.add(execution)
539
+ self.session.commit()
540
+ return execution
541
+
542
+ def update_status(
543
+ self,
544
+ execution_id: str,
545
+ status: str,
546
+ result: Dict[str, Any] = None,
547
+ error: str = None,
548
+ ):
549
+ """Update execution status"""
550
+ execution = (
551
+ self.session.query(WorkflowExecution).filter_by(id=execution_id).first()
552
+ )
553
+ if not execution:
554
+ raise ValueError(f"Execution {execution_id} not found")
555
+
556
+ execution.status = status
557
+ if result is not None:
558
+ execution.result = result
559
+ if error is not None:
560
+ execution.error = error
561
+
562
+ if status in ["completed", "failed"]:
563
+ execution.completed_at = datetime.now(timezone.utc)
564
+ if execution.started_at:
565
+ execution.execution_time_ms = int(
566
+ (execution.completed_at - execution.started_at).total_seconds()
567
+ * 1000
568
+ )
569
+
570
+ self.session.commit()
571
+
572
+ def get(self, execution_id: str) -> Optional[WorkflowExecution]:
573
+ """Get execution by ID"""
574
+ return self.session.query(WorkflowExecution).filter_by(id=execution_id).first()
575
+
576
+ def list_for_workflow(
577
+ self, workflow_id: str, limit: int = 50
578
+ ) -> List[WorkflowExecution]:
579
+ """List executions for a workflow"""
580
+ return (
581
+ self.session.query(WorkflowExecution)
582
+ .filter_by(workflow_id=workflow_id)
583
+ .order_by(WorkflowExecution.started_at.desc())
584
+ .limit(limit)
585
+ .all()
586
+ )
587
+
588
+
589
+ # Database initialization
590
+ def init_database(db_path: str = None) -> tuple[sessionmaker, Engine]:
591
+ """Initialize the database"""
592
+ if db_path is None:
593
+ db_path = Path.home() / ".kailash" / "studio.db"
594
+
595
+ db_path = Path(db_path)
596
+ db_path.parent.mkdir(parents=True, exist_ok=True)
597
+
598
+ engine = create_engine(f"sqlite:///{db_path}", echo=False)
599
+ Base.metadata.create_all(engine)
600
+
601
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
602
+
603
+ return SessionLocal, engine
604
+
605
+
606
+ # Context manager for database sessions
607
+
608
+
609
+ @contextmanager
610
+ def get_db_session(SessionLocal):
611
+ """Provide a transactional scope for database operations"""
612
+ session = SessionLocal()
613
+ try:
614
+ yield session
615
+ session.commit()
616
+ except Exception:
617
+ session.rollback()
618
+ raise
619
+ finally:
620
+ session.close()