kailash 0.3.0__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.
- kailash/__init__.py +1 -1
- kailash/access_control.py +40 -39
- kailash/api/auth.py +26 -32
- kailash/api/custom_nodes.py +29 -29
- kailash/api/custom_nodes_secure.py +35 -35
- kailash/api/database.py +17 -17
- kailash/api/gateway.py +19 -19
- kailash/api/mcp_integration.py +24 -23
- kailash/api/studio.py +45 -45
- kailash/api/workflow_api.py +8 -8
- kailash/cli/commands.py +5 -8
- kailash/manifest.py +42 -42
- kailash/mcp/__init__.py +1 -1
- kailash/mcp/ai_registry_server.py +20 -20
- kailash/mcp/client.py +9 -11
- kailash/mcp/client_new.py +10 -10
- kailash/mcp/server.py +1 -2
- kailash/mcp/server_enhanced.py +449 -0
- kailash/mcp/servers/ai_registry.py +6 -6
- kailash/mcp/utils/__init__.py +31 -0
- kailash/mcp/utils/cache.py +267 -0
- kailash/mcp/utils/config.py +263 -0
- kailash/mcp/utils/formatters.py +293 -0
- kailash/mcp/utils/metrics.py +418 -0
- kailash/nodes/ai/agents.py +9 -9
- kailash/nodes/ai/ai_providers.py +33 -34
- kailash/nodes/ai/embedding_generator.py +31 -32
- kailash/nodes/ai/intelligent_agent_orchestrator.py +62 -66
- kailash/nodes/ai/iterative_llm_agent.py +48 -48
- kailash/nodes/ai/llm_agent.py +32 -33
- kailash/nodes/ai/models.py +13 -13
- kailash/nodes/ai/self_organizing.py +44 -44
- kailash/nodes/api/auth.py +11 -11
- kailash/nodes/api/graphql.py +13 -13
- kailash/nodes/api/http.py +19 -19
- kailash/nodes/api/monitoring.py +20 -20
- kailash/nodes/api/rate_limiting.py +9 -13
- kailash/nodes/api/rest.py +29 -29
- kailash/nodes/api/security.py +44 -47
- kailash/nodes/base.py +21 -23
- kailash/nodes/base_async.py +7 -7
- kailash/nodes/base_cycle_aware.py +12 -12
- kailash/nodes/base_with_acl.py +5 -5
- kailash/nodes/code/python.py +56 -55
- kailash/nodes/data/directory.py +6 -6
- kailash/nodes/data/event_generation.py +10 -10
- kailash/nodes/data/file_discovery.py +28 -31
- kailash/nodes/data/readers.py +8 -8
- kailash/nodes/data/retrieval.py +10 -10
- kailash/nodes/data/sharepoint_graph.py +17 -17
- kailash/nodes/data/sources.py +5 -5
- kailash/nodes/data/sql.py +13 -13
- kailash/nodes/data/streaming.py +25 -25
- kailash/nodes/data/vector_db.py +22 -22
- kailash/nodes/data/writers.py +7 -7
- kailash/nodes/logic/async_operations.py +17 -17
- kailash/nodes/logic/convergence.py +11 -11
- kailash/nodes/logic/loop.py +4 -4
- kailash/nodes/logic/operations.py +11 -11
- kailash/nodes/logic/workflow.py +8 -9
- kailash/nodes/mixins/mcp.py +17 -17
- kailash/nodes/mixins.py +8 -10
- kailash/nodes/transform/chunkers.py +3 -3
- kailash/nodes/transform/formatters.py +7 -7
- kailash/nodes/transform/processors.py +10 -10
- kailash/runtime/access_controlled.py +18 -18
- kailash/runtime/async_local.py +17 -19
- kailash/runtime/docker.py +20 -22
- kailash/runtime/local.py +16 -16
- kailash/runtime/parallel.py +23 -23
- kailash/runtime/parallel_cyclic.py +27 -27
- kailash/runtime/runner.py +6 -6
- kailash/runtime/testing.py +20 -20
- kailash/sdk_exceptions.py +0 -58
- kailash/security.py +14 -26
- kailash/tracking/manager.py +38 -38
- kailash/tracking/metrics_collector.py +15 -14
- kailash/tracking/models.py +53 -53
- kailash/tracking/storage/base.py +7 -17
- kailash/tracking/storage/database.py +22 -23
- kailash/tracking/storage/filesystem.py +38 -40
- kailash/utils/export.py +21 -21
- kailash/utils/templates.py +2 -3
- kailash/visualization/api.py +30 -34
- kailash/visualization/dashboard.py +17 -17
- kailash/visualization/performance.py +16 -16
- kailash/visualization/reports.py +25 -27
- kailash/workflow/builder.py +8 -8
- kailash/workflow/convergence.py +13 -12
- kailash/workflow/cycle_analyzer.py +30 -32
- kailash/workflow/cycle_builder.py +12 -12
- kailash/workflow/cycle_config.py +16 -15
- kailash/workflow/cycle_debugger.py +40 -40
- kailash/workflow/cycle_exceptions.py +29 -29
- kailash/workflow/cycle_profiler.py +21 -21
- kailash/workflow/cycle_state.py +20 -22
- kailash/workflow/cyclic_runner.py +44 -44
- kailash/workflow/graph.py +40 -40
- kailash/workflow/mermaid_visualizer.py +9 -11
- kailash/workflow/migration.py +22 -22
- kailash/workflow/mock_registry.py +6 -6
- kailash/workflow/runner.py +9 -9
- kailash/workflow/safety.py +12 -13
- kailash/workflow/state.py +8 -11
- kailash/workflow/templates.py +19 -19
- kailash/workflow/validation.py +14 -14
- kailash/workflow/visualization.py +22 -22
- {kailash-0.3.0.dist-info → kailash-0.3.1.dist-info}/METADATA +53 -5
- kailash-0.3.1.dist-info/RECORD +136 -0
- kailash-0.3.0.dist-info/RECORD +0 -130
- {kailash-0.3.0.dist-info → kailash-0.3.1.dist-info}/WHEEL +0 -0
- {kailash-0.3.0.dist-info → kailash-0.3.1.dist-info}/entry_points.txt +0 -0
- {kailash-0.3.0.dist-info → kailash-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.3.0.dist-info → kailash-0.3.1.dist-info}/top_level.txt +0 -0
kailash/__init__.py
CHANGED
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
|
56
|
+
from datetime import UTC, datetime
|
56
57
|
from enum import Enum
|
57
|
-
from typing import Any
|
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:
|
140
|
-
permissions:
|
141
|
-
attributes:
|
142
|
-
session_id:
|
143
|
-
ip_address:
|
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:
|
199
|
+
permission: WorkflowPermission | NodePermission
|
199
200
|
effect: PermissionEffect
|
200
201
|
|
201
202
|
# Who does this apply to?
|
202
|
-
user_id:
|
203
|
-
role:
|
204
|
-
tenant_id:
|
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:
|
208
|
+
conditions: dict[str, Any] = field(default_factory=dict)
|
208
209
|
|
209
210
|
# Metadata
|
210
|
-
created_at: datetime = field(default_factory=lambda: datetime.now(
|
211
|
-
created_by:
|
212
|
-
expires_at:
|
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:
|
266
|
-
conditions_met:
|
267
|
-
masked_fields:
|
268
|
-
redirect_node:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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
|
-
) ->
|
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:
|
522
|
-
false_path_nodes:
|
523
|
-
) ->
|
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:
|
548
|
-
) ->
|
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:
|
572
|
-
runtime_context:
|
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(
|
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(
|
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:
|
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
|
54
|
-
from typing import Any
|
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=
|
104
|
+
permissions = Column(JSON, default=list)
|
105
105
|
|
106
106
|
# Timestamps
|
107
|
-
created_at = Column(DateTime, default=lambda: datetime.now(
|
107
|
+
created_at = Column(DateTime, default=lambda: datetime.now(UTC))
|
108
108
|
updated_at = Column(
|
109
109
|
DateTime,
|
110
|
-
default=lambda: datetime.now(
|
111
|
-
onupdate=lambda: datetime.now(
|
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(
|
153
|
+
created_at = Column(DateTime, default=lambda: datetime.now(UTC))
|
154
154
|
updated_at = Column(
|
155
155
|
DateTime,
|
156
|
-
default=lambda: datetime.now(
|
157
|
-
onupdate=lambda: datetime.now(
|
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(
|
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:
|
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:
|
231
|
-
permissions:
|
232
|
-
exp:
|
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:
|
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(
|
249
|
+
expire = datetime.now(UTC) + expires_delta
|
250
250
|
else:
|
251
|
-
expire = datetime.now(
|
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:
|
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(
|
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(
|
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(
|
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() ->
|
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(
|
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:
|
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()
|
kailash/api/custom_nodes.py
CHANGED
@@ -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
|
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:
|
23
|
-
icon:
|
24
|
-
color:
|
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:
|
28
|
-
inputs:
|
29
|
-
outputs:
|
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:
|
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:
|
40
|
-
category:
|
41
|
-
description:
|
42
|
-
icon:
|
43
|
-
color:
|
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:
|
47
|
-
inputs:
|
48
|
-
outputs:
|
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:
|
52
|
-
implementation:
|
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:
|
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:
|
66
|
-
icon:
|
67
|
-
color:
|
65
|
+
description: str | None
|
66
|
+
icon: str | None
|
67
|
+
color: str | None
|
68
68
|
|
69
69
|
# Node configuration
|
70
|
-
parameters:
|
71
|
-
inputs:
|
72
|
-
outputs:
|
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:
|
76
|
+
implementation: dict[str, Any]
|
77
77
|
|
78
78
|
# Metadata
|
79
79
|
is_published: bool
|
80
|
-
created_by:
|
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=
|
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:
|
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
|
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:
|
28
|
-
icon:
|
29
|
-
color:
|
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:
|
33
|
-
inputs:
|
34
|
-
outputs:
|
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:
|
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:
|
45
|
-
category:
|
46
|
-
description:
|
47
|
-
icon:
|
48
|
-
color:
|
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:
|
52
|
-
inputs:
|
53
|
-
outputs:
|
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:
|
57
|
-
implementation:
|
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:
|
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:
|
71
|
-
icon:
|
72
|
-
color:
|
70
|
+
description: str | None
|
71
|
+
icon: str | None
|
72
|
+
color: str | None
|
73
73
|
|
74
74
|
# Node configuration
|
75
|
-
parameters:
|
76
|
-
inputs:
|
77
|
-
outputs:
|
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:
|
81
|
+
implementation: dict[str, Any]
|
82
82
|
|
83
83
|
# Metadata
|
84
84
|
is_published: bool
|
85
|
-
created_by:
|
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=
|
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:
|
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:
|
297
|
-
) ->
|
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:
|
327
|
-
) ->
|
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:
|
350
|
-
) ->
|
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
|