kailash 0.2.2__py3-none-any.whl → 0.3.1__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 (117) hide show
  1. kailash/__init__.py +1 -1
  2. kailash/access_control.py +40 -39
  3. kailash/api/auth.py +26 -32
  4. kailash/api/custom_nodes.py +29 -29
  5. kailash/api/custom_nodes_secure.py +35 -35
  6. kailash/api/database.py +17 -17
  7. kailash/api/gateway.py +19 -19
  8. kailash/api/mcp_integration.py +24 -23
  9. kailash/api/studio.py +45 -45
  10. kailash/api/workflow_api.py +8 -8
  11. kailash/cli/commands.py +5 -8
  12. kailash/manifest.py +42 -42
  13. kailash/mcp/__init__.py +1 -1
  14. kailash/mcp/ai_registry_server.py +20 -20
  15. kailash/mcp/client.py +9 -11
  16. kailash/mcp/client_new.py +10 -10
  17. kailash/mcp/server.py +1 -2
  18. kailash/mcp/server_enhanced.py +449 -0
  19. kailash/mcp/servers/ai_registry.py +6 -6
  20. kailash/mcp/utils/__init__.py +31 -0
  21. kailash/mcp/utils/cache.py +267 -0
  22. kailash/mcp/utils/config.py +263 -0
  23. kailash/mcp/utils/formatters.py +293 -0
  24. kailash/mcp/utils/metrics.py +418 -0
  25. kailash/nodes/ai/agents.py +9 -9
  26. kailash/nodes/ai/ai_providers.py +33 -34
  27. kailash/nodes/ai/embedding_generator.py +31 -32
  28. kailash/nodes/ai/intelligent_agent_orchestrator.py +62 -66
  29. kailash/nodes/ai/iterative_llm_agent.py +48 -48
  30. kailash/nodes/ai/llm_agent.py +32 -33
  31. kailash/nodes/ai/models.py +13 -13
  32. kailash/nodes/ai/self_organizing.py +44 -44
  33. kailash/nodes/api/__init__.py +5 -0
  34. kailash/nodes/api/auth.py +11 -11
  35. kailash/nodes/api/graphql.py +13 -13
  36. kailash/nodes/api/http.py +19 -19
  37. kailash/nodes/api/monitoring.py +463 -0
  38. kailash/nodes/api/rate_limiting.py +9 -13
  39. kailash/nodes/api/rest.py +29 -29
  40. kailash/nodes/api/security.py +819 -0
  41. kailash/nodes/base.py +24 -26
  42. kailash/nodes/base_async.py +7 -7
  43. kailash/nodes/base_cycle_aware.py +12 -12
  44. kailash/nodes/base_with_acl.py +5 -5
  45. kailash/nodes/code/python.py +56 -55
  46. kailash/nodes/data/__init__.py +6 -0
  47. kailash/nodes/data/directory.py +6 -6
  48. kailash/nodes/data/event_generation.py +297 -0
  49. kailash/nodes/data/file_discovery.py +598 -0
  50. kailash/nodes/data/readers.py +8 -8
  51. kailash/nodes/data/retrieval.py +10 -10
  52. kailash/nodes/data/sharepoint_graph.py +17 -17
  53. kailash/nodes/data/sources.py +5 -5
  54. kailash/nodes/data/sql.py +13 -13
  55. kailash/nodes/data/streaming.py +25 -25
  56. kailash/nodes/data/vector_db.py +22 -22
  57. kailash/nodes/data/writers.py +7 -7
  58. kailash/nodes/logic/async_operations.py +17 -17
  59. kailash/nodes/logic/convergence.py +11 -11
  60. kailash/nodes/logic/loop.py +4 -4
  61. kailash/nodes/logic/operations.py +11 -11
  62. kailash/nodes/logic/workflow.py +8 -9
  63. kailash/nodes/mixins/mcp.py +17 -17
  64. kailash/nodes/mixins.py +8 -10
  65. kailash/nodes/transform/chunkers.py +3 -3
  66. kailash/nodes/transform/formatters.py +7 -7
  67. kailash/nodes/transform/processors.py +11 -11
  68. kailash/runtime/access_controlled.py +18 -18
  69. kailash/runtime/async_local.py +18 -20
  70. kailash/runtime/docker.py +24 -26
  71. kailash/runtime/local.py +55 -31
  72. kailash/runtime/parallel.py +25 -25
  73. kailash/runtime/parallel_cyclic.py +29 -29
  74. kailash/runtime/runner.py +6 -6
  75. kailash/runtime/testing.py +22 -22
  76. kailash/sdk_exceptions.py +0 -58
  77. kailash/security.py +14 -26
  78. kailash/tracking/manager.py +38 -38
  79. kailash/tracking/metrics_collector.py +15 -14
  80. kailash/tracking/models.py +53 -53
  81. kailash/tracking/storage/base.py +7 -17
  82. kailash/tracking/storage/database.py +22 -23
  83. kailash/tracking/storage/filesystem.py +38 -40
  84. kailash/utils/export.py +21 -21
  85. kailash/utils/templates.py +8 -9
  86. kailash/visualization/api.py +30 -34
  87. kailash/visualization/dashboard.py +17 -17
  88. kailash/visualization/performance.py +32 -19
  89. kailash/visualization/reports.py +30 -28
  90. kailash/workflow/builder.py +8 -8
  91. kailash/workflow/convergence.py +13 -12
  92. kailash/workflow/cycle_analyzer.py +38 -33
  93. kailash/workflow/cycle_builder.py +12 -12
  94. kailash/workflow/cycle_config.py +16 -15
  95. kailash/workflow/cycle_debugger.py +40 -40
  96. kailash/workflow/cycle_exceptions.py +29 -29
  97. kailash/workflow/cycle_profiler.py +21 -21
  98. kailash/workflow/cycle_state.py +20 -22
  99. kailash/workflow/cyclic_runner.py +45 -45
  100. kailash/workflow/graph.py +57 -45
  101. kailash/workflow/mermaid_visualizer.py +9 -11
  102. kailash/workflow/migration.py +22 -22
  103. kailash/workflow/mock_registry.py +6 -6
  104. kailash/workflow/runner.py +9 -9
  105. kailash/workflow/safety.py +12 -13
  106. kailash/workflow/state.py +8 -11
  107. kailash/workflow/templates.py +19 -19
  108. kailash/workflow/validation.py +14 -14
  109. kailash/workflow/visualization.py +32 -24
  110. kailash-0.3.1.dist-info/METADATA +476 -0
  111. kailash-0.3.1.dist-info/RECORD +136 -0
  112. kailash-0.2.2.dist-info/METADATA +0 -121
  113. kailash-0.2.2.dist-info/RECORD +0 -126
  114. {kailash-0.2.2.dist-info → kailash-0.3.1.dist-info}/WHEEL +0 -0
  115. {kailash-0.2.2.dist-info → kailash-0.3.1.dist-info}/entry_points.txt +0 -0
  116. {kailash-0.2.2.dist-info → kailash-0.3.1.dist-info}/licenses/LICENSE +0 -0
  117. {kailash-0.2.2.dist-info → kailash-0.3.1.dist-info}/top_level.txt +0 -0
kailash/__init__.py CHANGED
@@ -15,7 +15,7 @@ from kailash.workflow.visualization import WorkflowVisualizer
15
15
  # For backward compatibility
16
16
  WorkflowGraph = Workflow
17
17
 
18
- __version__ = "0.2.0"
18
+ __version__ = "0.3.1"
19
19
 
20
20
  __all__ = [
21
21
  "Workflow",
kailash/access_control.py CHANGED
@@ -51,10 +51,11 @@ Future Enhancements:
51
51
 
52
52
  import logging
53
53
  import threading
54
+ from collections.abc import Callable
54
55
  from dataclasses import dataclass, field
55
- from datetime import datetime, timezone
56
+ from datetime import UTC, datetime
56
57
  from enum import Enum
57
- from typing import Any, Callable, Dict, List, Optional, Set, Union
58
+ from typing import Any
58
59
 
59
60
  logger = logging.getLogger(__name__)
60
61
 
@@ -136,11 +137,11 @@ class UserContext:
136
137
  user_id: str
137
138
  tenant_id: str
138
139
  email: str
139
- roles: List[str] = field(default_factory=list)
140
- permissions: List[str] = field(default_factory=list)
141
- attributes: Dict[str, Any] = field(default_factory=dict) # Custom attributes
142
- session_id: Optional[str] = None
143
- ip_address: Optional[str] = None
140
+ roles: list[str] = field(default_factory=list)
141
+ permissions: list[str] = field(default_factory=list)
142
+ attributes: dict[str, Any] = field(default_factory=dict) # Custom attributes
143
+ session_id: str | None = None
144
+ ip_address: str | None = None
144
145
 
145
146
 
146
147
  @dataclass
@@ -195,21 +196,21 @@ class PermissionRule:
195
196
  id: str
196
197
  resource_type: str # "workflow" or "node"
197
198
  resource_id: str # workflow_id or node_id
198
- permission: Union[WorkflowPermission, NodePermission]
199
+ permission: WorkflowPermission | NodePermission
199
200
  effect: PermissionEffect
200
201
 
201
202
  # Who does this apply to?
202
- user_id: Optional[str] = None # Specific user
203
- role: Optional[str] = None # Any user with role
204
- tenant_id: Optional[str] = None # All users in tenant
203
+ user_id: str | None = None # Specific user
204
+ role: str | None = None # Any user with role
205
+ tenant_id: str | None = None # All users in tenant
205
206
 
206
207
  # Conditions
207
- conditions: Dict[str, Any] = field(default_factory=dict)
208
+ conditions: dict[str, Any] = field(default_factory=dict)
208
209
 
209
210
  # Metadata
210
- created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
211
- created_by: Optional[str] = None
212
- expires_at: Optional[datetime] = None
211
+ created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
212
+ created_by: str | None = None
213
+ expires_at: datetime | None = None
213
214
  priority: int = 0 # Higher priority rules evaluated first
214
215
 
215
216
 
@@ -262,17 +263,17 @@ class AccessDecision:
262
263
 
263
264
  allowed: bool
264
265
  reason: str
265
- applied_rules: List[PermissionRule] = field(default_factory=list)
266
- conditions_met: Dict[str, bool] = field(default_factory=dict)
267
- masked_fields: List[str] = field(default_factory=list) # Fields to mask in output
268
- redirect_node: Optional[str] = None # Alternative node to execute
266
+ applied_rules: list[PermissionRule] = field(default_factory=list)
267
+ conditions_met: dict[str, bool] = field(default_factory=dict)
268
+ masked_fields: list[str] = field(default_factory=list) # Fields to mask in output
269
+ redirect_node: str | None = None # Alternative node to execute
269
270
 
270
271
 
271
272
  class ConditionEvaluator:
272
273
  """Evaluates conditions for conditional permissions"""
273
274
 
274
275
  def __init__(self):
275
- self.evaluators: Dict[str, Callable] = {
276
+ self.evaluators: dict[str, Callable] = {
276
277
  "time_range": self._eval_time_range,
277
278
  "data_contains": self._eval_data_contains,
278
279
  "user_attribute": self._eval_user_attribute,
@@ -281,7 +282,7 @@ class ConditionEvaluator:
281
282
  }
282
283
 
283
284
  def evaluate(
284
- self, condition_type: str, condition_value: Any, context: Dict[str, Any]
285
+ self, condition_type: str, condition_value: Any, context: dict[str, Any]
285
286
  ) -> bool:
286
287
  """Evaluate a condition"""
287
288
  evaluator = self.evaluators.get(condition_type)
@@ -295,7 +296,7 @@ class ConditionEvaluator:
295
296
  logger.error(f"Error evaluating condition {condition_type}: {e}")
296
297
  return False
297
298
 
298
- def _eval_time_range(self, value: Dict[str, str], context: Dict[str, Any]) -> bool:
299
+ def _eval_time_range(self, value: dict[str, str], context: dict[str, Any]) -> bool:
299
300
  """Check if current time is within range"""
300
301
  from datetime import datetime, time
301
302
 
@@ -305,7 +306,7 @@ class ConditionEvaluator:
305
306
  return start <= now <= end
306
307
 
307
308
  def _eval_data_contains(
308
- self, value: Dict[str, Any], context: Dict[str, Any]
309
+ self, value: dict[str, Any], context: dict[str, Any]
309
310
  ) -> bool:
310
311
  """Check if data contains specific values"""
311
312
  data = context.get("data", {})
@@ -317,7 +318,7 @@ class ConditionEvaluator:
317
318
  return False
318
319
 
319
320
  def _eval_user_attribute(
320
- self, value: Dict[str, Any], context: Dict[str, Any]
321
+ self, value: dict[str, Any], context: dict[str, Any]
321
322
  ) -> bool:
322
323
  """Check user attributes"""
323
324
  user = context.get("user")
@@ -329,7 +330,7 @@ class ConditionEvaluator:
329
330
 
330
331
  return user.attributes.get(attr_name) == expected
331
332
 
332
- def _eval_ip_range(self, value: Dict[str, Any], context: Dict[str, Any]) -> bool:
333
+ def _eval_ip_range(self, value: dict[str, Any], context: dict[str, Any]) -> bool:
333
334
  """Check if IP is in allowed range"""
334
335
  # Simplified IP check - in production use ipaddress module
335
336
  allowed_ips = value.get("allowed", [])
@@ -337,7 +338,7 @@ class ConditionEvaluator:
337
338
 
338
339
  return user_ip in allowed_ips
339
340
 
340
- def _eval_custom(self, value: Dict[str, Any], context: Dict[str, Any]) -> bool:
341
+ def _eval_custom(self, value: dict[str, Any], context: dict[str, Any]) -> bool:
341
342
  """Evaluate custom condition"""
342
343
  # This would call a custom function registered by the user
343
344
  return True
@@ -409,7 +410,7 @@ class AccessControlManager:
409
410
 
410
411
  def __init__(self, enabled: bool = False):
411
412
  self.enabled = enabled # Disabled by default
412
- self.rules: List[PermissionRule] = []
413
+ self.rules: list[PermissionRule] = []
413
414
  self.condition_evaluator = ConditionEvaluator()
414
415
  self._cache = {} # Cache access decisions
415
416
  self._cache_lock = threading.Lock()
@@ -458,7 +459,7 @@ class AccessControlManager:
458
459
  user: UserContext,
459
460
  node_id: str,
460
461
  permission: NodePermission,
461
- runtime_context: Dict[str, Any] = None,
462
+ runtime_context: dict[str, Any] = None,
462
463
  ) -> AccessDecision:
463
464
  """Check if user has permission on node"""
464
465
  cache_key = f"node:{node_id}:{user.user_id}:{permission.value}"
@@ -494,7 +495,7 @@ class AccessControlManager:
494
495
 
495
496
  def get_accessible_nodes(
496
497
  self, user: UserContext, workflow_id: str, permission: NodePermission
497
- ) -> Set[str]:
498
+ ) -> set[str]:
498
499
  """Get all nodes user can access in a workflow"""
499
500
  accessible = set()
500
501
 
@@ -518,9 +519,9 @@ class AccessControlManager:
518
519
  self,
519
520
  user: UserContext,
520
521
  conditional_node_id: str,
521
- true_path_nodes: List[str],
522
- false_path_nodes: List[str],
523
- ) -> List[str]:
522
+ true_path_nodes: list[str],
523
+ false_path_nodes: list[str],
524
+ ) -> list[str]:
524
525
  """Determine which path user should take based on permissions"""
525
526
  # Check if user has access to nodes in true path
526
527
  true_path_accessible = all(
@@ -544,8 +545,8 @@ class AccessControlManager:
544
545
  return []
545
546
 
546
547
  def mask_node_output(
547
- self, user: UserContext, node_id: str, output: Dict[str, Any]
548
- ) -> Dict[str, Any]:
548
+ self, user: UserContext, node_id: str, output: dict[str, Any]
549
+ ) -> dict[str, Any]:
549
550
  """Mask sensitive fields in node output"""
550
551
  decision = self.check_node_access(user, node_id, NodePermission.READ_OUTPUT)
551
552
 
@@ -568,8 +569,8 @@ class AccessControlManager:
568
569
  user: UserContext,
569
570
  resource_type: str,
570
571
  resource_id: str,
571
- permission: Union[WorkflowPermission, NodePermission],
572
- runtime_context: Dict[str, Any],
572
+ permission: WorkflowPermission | NodePermission,
573
+ runtime_context: dict[str, Any],
573
574
  ) -> AccessDecision:
574
575
  """Evaluate all applicable rules"""
575
576
  applicable_rules = []
@@ -584,7 +585,7 @@ class AccessControlManager:
584
585
  ):
585
586
 
586
587
  # Check expiration
587
- if rule.expires_at and rule.expires_at < datetime.now(timezone.utc):
588
+ if rule.expires_at and rule.expires_at < datetime.now(UTC):
588
589
  continue
589
590
 
590
591
  applicable_rules.append(rule)
@@ -593,7 +594,7 @@ class AccessControlManager:
593
594
  context = {
594
595
  "user": user,
595
596
  "runtime": runtime_context,
596
- "timestamp": datetime.now(timezone.utc),
597
+ "timestamp": datetime.now(UTC),
597
598
  }
598
599
 
599
600
  final_effect = PermissionEffect.DENY # Default deny
@@ -660,7 +661,7 @@ class AccessControlManager:
660
661
  user: UserContext,
661
662
  resource_type: str,
662
663
  resource_id: str,
663
- permission: Union[WorkflowPermission, NodePermission],
664
+ permission: WorkflowPermission | NodePermission,
664
665
  decision: AccessDecision,
665
666
  ):
666
667
  """Log access attempt for audit"""
kailash/api/auth.py CHANGED
@@ -50,8 +50,8 @@ Future Enhancements:
50
50
  import os
51
51
  import secrets
52
52
  import threading
53
- from datetime import datetime, timedelta, timezone
54
- from typing import Any, Dict, List, Optional
53
+ from datetime import UTC, datetime, timedelta
54
+ from typing import Any
55
55
 
56
56
  from fastapi import Depends, HTTPException, Request, status
57
57
  from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
@@ -101,14 +101,14 @@ class User(Base):
101
101
 
102
102
  # User roles and permissions
103
103
  roles = Column(JSON, default=lambda: ["user"])
104
- permissions = Column(JSON, default=lambda: [])
104
+ permissions = Column(JSON, default=list)
105
105
 
106
106
  # Timestamps
107
- created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
107
+ created_at = Column(DateTime, default=lambda: datetime.now(UTC))
108
108
  updated_at = Column(
109
109
  DateTime,
110
- default=lambda: datetime.now(timezone.utc),
111
- onupdate=lambda: datetime.now(timezone.utc),
110
+ default=lambda: datetime.now(UTC),
111
+ onupdate=lambda: datetime.now(UTC),
112
112
  )
113
113
  last_login = Column(DateTime)
114
114
 
@@ -150,11 +150,11 @@ class Tenant(Base):
150
150
  subscription_tier = Column(String(50), default="free")
151
151
 
152
152
  # Timestamps
153
- created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
153
+ created_at = Column(DateTime, default=lambda: datetime.now(UTC))
154
154
  updated_at = Column(
155
155
  DateTime,
156
- default=lambda: datetime.now(timezone.utc),
157
- onupdate=lambda: datetime.now(timezone.utc),
156
+ default=lambda: datetime.now(UTC),
157
+ onupdate=lambda: datetime.now(UTC),
158
158
  )
159
159
 
160
160
  # Relationships
@@ -187,7 +187,7 @@ class APIKey(Base):
187
187
  usage_count = Column(JSON, default=lambda: {"total": 0, "monthly": 0})
188
188
 
189
189
  # Timestamps
190
- created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
190
+ created_at = Column(DateTime, default=lambda: datetime.now(UTC))
191
191
 
192
192
  # Relationships
193
193
  user = relationship("User", back_populates="api_keys")
@@ -203,7 +203,7 @@ class UserCreate(BaseModel):
203
203
  email: EmailStr
204
204
  username: str = Field(..., min_length=3, max_length=100)
205
205
  password: str = Field(..., min_length=8)
206
- tenant_id: Optional[str] = None # If None, create new tenant
206
+ tenant_id: str | None = None # If None, create new tenant
207
207
 
208
208
 
209
209
  class UserLogin(BaseModel):
@@ -227,9 +227,9 @@ class TokenData(BaseModel):
227
227
 
228
228
  sub: str
229
229
  tenant_id: str
230
- roles: List[str] = ["user"]
231
- permissions: List[str] = []
232
- exp: Optional[datetime] = None
230
+ roles: list[str] = ["user"]
231
+ permissions: list[str] = []
232
+ exp: datetime | None = None
233
233
 
234
234
 
235
235
  class JWTAuth:
@@ -240,33 +240,27 @@ class JWTAuth:
240
240
  self.algorithm = ALGORITHM
241
241
 
242
242
  def create_access_token(
243
- self, data: Dict[str, Any], expires_delta: Optional[timedelta] = None
243
+ self, data: dict[str, Any], expires_delta: timedelta | None = None
244
244
  ) -> str:
245
245
  """Create a JWT access token"""
246
246
  to_encode = data.copy()
247
247
 
248
248
  if expires_delta:
249
- expire = datetime.now(timezone.utc) + expires_delta
249
+ expire = datetime.now(UTC) + expires_delta
250
250
  else:
251
- expire = datetime.now(timezone.utc) + timedelta(
252
- hours=ACCESS_TOKEN_EXPIRE_HOURS
253
- )
251
+ expire = datetime.now(UTC) + timedelta(hours=ACCESS_TOKEN_EXPIRE_HOURS)
254
252
 
255
- to_encode.update(
256
- {"exp": expire, "iat": datetime.now(timezone.utc), "type": "access"}
257
- )
253
+ to_encode.update({"exp": expire, "iat": datetime.now(UTC), "type": "access"})
258
254
 
259
255
  encoded_jwt = jwt.encode(to_encode, self.secret_key, algorithm=self.algorithm)
260
256
  return encoded_jwt
261
257
 
262
- def create_refresh_token(self, data: Dict[str, Any]) -> str:
258
+ def create_refresh_token(self, data: dict[str, Any]) -> str:
263
259
  """Create a JWT refresh token"""
264
260
  to_encode = data.copy()
265
- expire = datetime.now(timezone.utc) + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
261
+ expire = datetime.now(UTC) + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
266
262
 
267
- to_encode.update(
268
- {"exp": expire, "iat": datetime.now(timezone.utc), "type": "refresh"}
269
- )
263
+ to_encode.update({"exp": expire, "iat": datetime.now(UTC), "type": "refresh"})
270
264
 
271
265
  encoded_jwt = jwt.encode(to_encode, self.secret_key, algorithm=self.algorithm)
272
266
  return encoded_jwt
@@ -421,13 +415,13 @@ async def verify_api_key(
421
415
  )
422
416
 
423
417
  # Check expiration
424
- if valid_key.expires_at and valid_key.expires_at < datetime.now(timezone.utc):
418
+ if valid_key.expires_at and valid_key.expires_at < datetime.now(UTC):
425
419
  raise HTTPException(
426
420
  status_code=status.HTTP_401_UNAUTHORIZED, detail="API key expired"
427
421
  )
428
422
 
429
423
  # Update usage
430
- valid_key.last_used_at = datetime.now(timezone.utc)
424
+ valid_key.last_used_at = datetime.now(UTC)
431
425
  valid_key.usage_count["total"] += 1
432
426
  valid_key.usage_count["monthly"] += 1
433
427
  session.commit()
@@ -514,7 +508,7 @@ class TenantContext:
514
508
  _tenant_context = threading.local()
515
509
 
516
510
 
517
- def get_current_tenant_id() -> Optional[str]:
511
+ def get_current_tenant_id() -> str | None:
518
512
  """Get current tenant ID from context"""
519
513
  return getattr(_tenant_context, "tenant_id", None)
520
514
 
@@ -615,7 +609,7 @@ class AuthService:
615
609
  )
616
610
 
617
611
  # Update last login
618
- user.last_login = datetime.now(timezone.utc)
612
+ user.last_login = datetime.now(UTC)
619
613
  self.session.commit()
620
614
 
621
615
  # Generate tokens
@@ -646,7 +640,7 @@ class AuthService:
646
640
  return self.auth.create_tokens(user)
647
641
 
648
642
  def create_api_key(
649
- self, name: str, user: User, scopes: List[str] = None
643
+ self, name: str, user: User, scopes: list[str] = None
650
644
  ) -> tuple[str, APIKey]:
651
645
  """Create an API key for a user"""
652
646
  key, key_hash = create_api_key()
@@ -9,7 +9,7 @@ This module provides endpoints for users to:
9
9
  """
10
10
 
11
11
  from datetime import datetime
12
- from typing import Any, Dict, List, Optional
12
+ from typing import Any
13
13
 
14
14
  from pydantic import BaseModel, Field
15
15
 
@@ -19,40 +19,40 @@ class CustomNodeCreate(BaseModel):
19
19
 
20
20
  name: str = Field(..., min_length=1, max_length=255)
21
21
  category: str = Field(default="custom", max_length=100)
22
- description: Optional[str] = None
23
- icon: Optional[str] = Field(None, max_length=50)
24
- color: Optional[str] = Field(None, pattern="^#[0-9A-Fa-f]{6}$")
22
+ description: str | None = None
23
+ icon: str | None = Field(None, max_length=50)
24
+ color: str | None = Field(None, pattern="^#[0-9A-Fa-f]{6}$")
25
25
 
26
26
  # Node configuration
27
- parameters: List[Dict[str, Any]] = Field(default_factory=list)
28
- inputs: List[Dict[str, Any]] = Field(default_factory=list)
29
- outputs: List[Dict[str, Any]] = Field(default_factory=list)
27
+ parameters: list[dict[str, Any]] = Field(default_factory=list)
28
+ inputs: list[dict[str, Any]] = Field(default_factory=list)
29
+ outputs: list[dict[str, Any]] = Field(default_factory=list)
30
30
 
31
31
  # Implementation
32
32
  implementation_type: str = Field(..., pattern="^(python|workflow|api)$")
33
- implementation: Dict[str, Any]
33
+ implementation: dict[str, Any]
34
34
 
35
35
 
36
36
  class CustomNodeUpdate(BaseModel):
37
37
  """Request model for updating a custom node"""
38
38
 
39
- name: Optional[str] = Field(None, min_length=1, max_length=255)
40
- category: Optional[str] = Field(None, max_length=100)
41
- description: Optional[str] = None
42
- icon: Optional[str] = Field(None, max_length=50)
43
- color: Optional[str] = Field(None, pattern="^#[0-9A-Fa-f]{6}$")
39
+ name: str | None = Field(None, min_length=1, max_length=255)
40
+ category: str | None = Field(None, max_length=100)
41
+ description: str | None = None
42
+ icon: str | None = Field(None, max_length=50)
43
+ color: str | None = Field(None, pattern="^#[0-9A-Fa-f]{6}$")
44
44
 
45
45
  # Node configuration
46
- parameters: Optional[List[Dict[str, Any]]] = None
47
- inputs: Optional[List[Dict[str, Any]]] = None
48
- outputs: Optional[List[Dict[str, Any]]] = None
46
+ parameters: list[dict[str, Any]] | None = None
47
+ inputs: list[dict[str, Any]] | None = None
48
+ outputs: list[dict[str, Any]] | None = None
49
49
 
50
50
  # Implementation
51
- implementation_type: Optional[str] = Field(None, pattern="^(python|workflow|api)$")
52
- implementation: Optional[Dict[str, Any]] = None
51
+ implementation_type: str | None = Field(None, pattern="^(python|workflow|api)$")
52
+ implementation: dict[str, Any] | None = None
53
53
 
54
54
  # Publishing
55
- is_published: Optional[bool] = None
55
+ is_published: bool | None = None
56
56
 
57
57
 
58
58
  class CustomNodeResponse(BaseModel):
@@ -62,22 +62,22 @@ class CustomNodeResponse(BaseModel):
62
62
  tenant_id: str
63
63
  name: str
64
64
  category: str
65
- description: Optional[str]
66
- icon: Optional[str]
67
- color: Optional[str]
65
+ description: str | None
66
+ icon: str | None
67
+ color: str | None
68
68
 
69
69
  # Node configuration
70
- parameters: List[Dict[str, Any]]
71
- inputs: List[Dict[str, Any]]
72
- outputs: List[Dict[str, Any]]
70
+ parameters: list[dict[str, Any]]
71
+ inputs: list[dict[str, Any]]
72
+ outputs: list[dict[str, Any]]
73
73
 
74
74
  # Implementation
75
75
  implementation_type: str
76
- implementation: Dict[str, Any]
76
+ implementation: dict[str, Any]
77
77
 
78
78
  # Metadata
79
79
  is_published: bool
80
- created_by: Optional[str]
80
+ created_by: str | None
81
81
  created_at: datetime
82
82
  updated_at: datetime
83
83
 
@@ -88,7 +88,7 @@ def setup_custom_node_routes(app, SessionLocal, tenant_id: str):
88
88
 
89
89
  from .database import CustomNodeRepository, get_db_session
90
90
 
91
- @app.get("/api/custom-nodes", response_model=List[CustomNodeResponse])
91
+ @app.get("/api/custom-nodes", response_model=list[CustomNodeResponse])
92
92
  async def list_custom_nodes():
93
93
  """List all custom nodes for the tenant"""
94
94
  with get_db_session(SessionLocal) as session:
@@ -230,7 +230,7 @@ def setup_custom_node_routes(app, SessionLocal, tenant_id: str):
230
230
  return {"message": "Custom node deleted successfully"}
231
231
 
232
232
  @app.post("/api/custom-nodes/{node_id}/test")
233
- async def test_custom_node(node_id: str, test_data: Dict[str, Any]):
233
+ async def test_custom_node(node_id: str, test_data: dict[str, Any]):
234
234
  """Test a custom node with sample data"""
235
235
  with get_db_session(SessionLocal) as session:
236
236
  repo = CustomNodeRepository(session)
@@ -9,7 +9,7 @@ This module provides secure endpoints for users to:
9
9
  """
10
10
 
11
11
  from datetime import datetime
12
- from typing import Any, Dict, List, Optional
12
+ from typing import Any
13
13
 
14
14
  from fastapi import Depends, HTTPException
15
15
  from pydantic import BaseModel, Field
@@ -24,40 +24,40 @@ class CustomNodeCreate(BaseModel):
24
24
 
25
25
  name: str = Field(..., min_length=1, max_length=255)
26
26
  category: str = Field(default="custom", max_length=100)
27
- description: Optional[str] = None
28
- icon: Optional[str] = Field(None, max_length=50)
29
- color: Optional[str] = Field(None, pattern="^#[0-9A-Fa-f]{6}$")
27
+ description: str | None = None
28
+ icon: str | None = Field(None, max_length=50)
29
+ color: str | None = Field(None, pattern="^#[0-9A-Fa-f]{6}$")
30
30
 
31
31
  # Node configuration
32
- parameters: List[Dict[str, Any]] = Field(default_factory=list)
33
- inputs: List[Dict[str, Any]] = Field(default_factory=list)
34
- outputs: List[Dict[str, Any]] = Field(default_factory=list)
32
+ parameters: list[dict[str, Any]] = Field(default_factory=list)
33
+ inputs: list[dict[str, Any]] = Field(default_factory=list)
34
+ outputs: list[dict[str, Any]] = Field(default_factory=list)
35
35
 
36
36
  # Implementation
37
37
  implementation_type: str = Field(..., pattern="^(python|workflow|api)$")
38
- implementation: Dict[str, Any]
38
+ implementation: dict[str, Any]
39
39
 
40
40
 
41
41
  class CustomNodeUpdate(BaseModel):
42
42
  """Request model for updating a custom node"""
43
43
 
44
- name: Optional[str] = Field(None, min_length=1, max_length=255)
45
- category: Optional[str] = Field(None, max_length=100)
46
- description: Optional[str] = None
47
- icon: Optional[str] = Field(None, max_length=50)
48
- color: Optional[str] = Field(None, pattern="^#[0-9A-Fa-f]{6}$")
44
+ name: str | None = Field(None, min_length=1, max_length=255)
45
+ category: str | None = Field(None, max_length=100)
46
+ description: str | None = None
47
+ icon: str | None = Field(None, max_length=50)
48
+ color: str | None = Field(None, pattern="^#[0-9A-Fa-f]{6}$")
49
49
 
50
50
  # Node configuration
51
- parameters: Optional[List[Dict[str, Any]]] = None
52
- inputs: Optional[List[Dict[str, Any]]] = None
53
- outputs: Optional[List[Dict[str, Any]]] = None
51
+ parameters: list[dict[str, Any]] | None = None
52
+ inputs: list[dict[str, Any]] | None = None
53
+ outputs: list[dict[str, Any]] | None = None
54
54
 
55
55
  # Implementation
56
- implementation_type: Optional[str] = Field(None, pattern="^(python|workflow|api)$")
57
- implementation: Optional[Dict[str, Any]] = None
56
+ implementation_type: str | None = Field(None, pattern="^(python|workflow|api)$")
57
+ implementation: dict[str, Any] | None = None
58
58
 
59
59
  # Publishing
60
- is_published: Optional[bool] = None
60
+ is_published: bool | None = None
61
61
 
62
62
 
63
63
  class CustomNodeResponse(BaseModel):
@@ -67,22 +67,22 @@ class CustomNodeResponse(BaseModel):
67
67
  tenant_id: str
68
68
  name: str
69
69
  category: str
70
- description: Optional[str]
71
- icon: Optional[str]
72
- color: Optional[str]
70
+ description: str | None
71
+ icon: str | None
72
+ color: str | None
73
73
 
74
74
  # Node configuration
75
- parameters: List[Dict[str, Any]]
76
- inputs: List[Dict[str, Any]]
77
- outputs: List[Dict[str, Any]]
75
+ parameters: list[dict[str, Any]]
76
+ inputs: list[dict[str, Any]]
77
+ outputs: list[dict[str, Any]]
78
78
 
79
79
  # Implementation
80
80
  implementation_type: str
81
- implementation: Dict[str, Any]
81
+ implementation: dict[str, Any]
82
82
 
83
83
  # Metadata
84
84
  is_published: bool
85
- created_by: Optional[str]
85
+ created_by: str | None
86
86
  created_at: datetime
87
87
  updated_at: datetime
88
88
 
@@ -90,7 +90,7 @@ class CustomNodeResponse(BaseModel):
90
90
  def setup_custom_node_routes(app, SessionLocal):
91
91
  """Setup custom node API routes with authentication"""
92
92
 
93
- @app.get("/api/custom-nodes", response_model=List[CustomNodeResponse])
93
+ @app.get("/api/custom-nodes", response_model=list[CustomNodeResponse])
94
94
  async def list_custom_nodes(
95
95
  user: User = Depends(require_permission("read:nodes")),
96
96
  tenant: Tenant = Depends(get_current_tenant),
@@ -255,7 +255,7 @@ def setup_custom_node_routes(app, SessionLocal):
255
255
  @app.post("/api/custom-nodes/{node_id}/test")
256
256
  async def test_custom_node(
257
257
  node_id: str,
258
- test_data: Dict[str, Any],
258
+ test_data: dict[str, Any],
259
259
  user: User = Depends(require_permission("execute:nodes")),
260
260
  tenant: Tenant = Depends(get_current_tenant),
261
261
  session: Session = Depends(get_db_session),
@@ -293,8 +293,8 @@ def setup_custom_node_routes(app, SessionLocal):
293
293
 
294
294
 
295
295
  async def _execute_python_node(
296
- node: CustomNode, test_data: Dict[str, Any], tenant_id: str
297
- ) -> Dict[str, Any]:
296
+ node: CustomNode, test_data: dict[str, Any], tenant_id: str
297
+ ) -> dict[str, Any]:
298
298
  """Execute a Python-based custom node with security sandboxing"""
299
299
  from kailash.nodes.code.python import PythonCodeNode
300
300
  from kailash.security import SecurityConfig, TenantContext
@@ -323,8 +323,8 @@ async def _execute_python_node(
323
323
 
324
324
 
325
325
  async def _execute_workflow_node(
326
- node: CustomNode, test_data: Dict[str, Any], tenant_id: str
327
- ) -> Dict[str, Any]:
326
+ node: CustomNode, test_data: dict[str, Any], tenant_id: str
327
+ ) -> dict[str, Any]:
328
328
  """Execute a workflow-based custom node"""
329
329
  from kailash.runtime.local import LocalRuntime
330
330
  from kailash.security import TenantContext
@@ -346,8 +346,8 @@ async def _execute_workflow_node(
346
346
 
347
347
 
348
348
  async def _execute_api_node(
349
- node: CustomNode, test_data: Dict[str, Any], tenant_id: str
350
- ) -> Dict[str, Any]:
349
+ node: CustomNode, test_data: dict[str, Any], tenant_id: str
350
+ ) -> dict[str, Any]:
351
351
  """Execute an API-based custom node"""
352
352
 
353
353
  from kailash.nodes.api.http import HTTPRequestNode