kailash 0.8.0__py3-none-any.whl → 0.8.2__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/middleware/communication/realtime.py +50 -29
- kailash/nodes/code/python.py +5 -0
- kailash/runtime/local.py +66 -0
- kailash/runtime/secret_provider.py +293 -0
- kailash/workflow/visualization.py +36 -18
- kailash-0.8.2.dist-info/METADATA +532 -0
- {kailash-0.8.0.dist-info → kailash-0.8.2.dist-info}/RECORD +12 -11
- kailash-0.8.0.dist-info/METADATA +0 -744
- {kailash-0.8.0.dist-info → kailash-0.8.2.dist-info}/WHEEL +0 -0
- {kailash-0.8.0.dist-info → kailash-0.8.2.dist-info}/entry_points.txt +0 -0
- {kailash-0.8.0.dist-info → kailash-0.8.2.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.8.0.dist-info → kailash-0.8.2.dist-info}/top_level.txt +0 -0
kailash/__init__.py
CHANGED
@@ -177,87 +177,108 @@ class ConnectionManager:
|
|
177
177
|
"active_users": len(self.user_connections),
|
178
178
|
"total_messages_sent": total_messages,
|
179
179
|
}
|
180
|
-
|
181
|
-
def filter_events(
|
180
|
+
|
181
|
+
def filter_events(
|
182
|
+
self, events: List[BaseEvent], event_filter: EventFilter = None
|
183
|
+
) -> List[BaseEvent]:
|
182
184
|
"""Filter events based on event filter criteria."""
|
183
185
|
if not event_filter:
|
184
186
|
return events
|
185
|
-
|
187
|
+
|
186
188
|
filtered = []
|
187
189
|
for event in events:
|
188
190
|
# Apply session filter
|
189
|
-
if event_filter.session_id and hasattr(event,
|
191
|
+
if event_filter.session_id and hasattr(event, "session_id"):
|
190
192
|
if event.session_id != event_filter.session_id:
|
191
193
|
continue
|
192
|
-
|
194
|
+
|
193
195
|
# Apply user filter
|
194
|
-
if event_filter.user_id and hasattr(event,
|
196
|
+
if event_filter.user_id and hasattr(event, "user_id"):
|
195
197
|
if event.user_id != event_filter.user_id:
|
196
198
|
continue
|
197
|
-
|
199
|
+
|
198
200
|
# Apply event type filter
|
199
|
-
if
|
201
|
+
if (
|
202
|
+
event_filter.event_types
|
203
|
+
and event.event_type not in event_filter.event_types
|
204
|
+
):
|
200
205
|
continue
|
201
|
-
|
206
|
+
|
202
207
|
filtered.append(event)
|
203
|
-
|
208
|
+
|
204
209
|
return filtered
|
205
|
-
|
210
|
+
|
206
211
|
def set_event_filter(self, connection_id: str, event_filter: EventFilter):
|
207
212
|
"""Set event filter for a specific connection."""
|
208
213
|
if connection_id in self.connections:
|
209
214
|
self.connections[connection_id]["event_filter"] = event_filter
|
210
|
-
|
215
|
+
|
211
216
|
def get_event_filter(self, connection_id: str) -> Optional[EventFilter]:
|
212
217
|
"""Get event filter for a specific connection."""
|
213
218
|
if connection_id in self.connections:
|
214
219
|
return self.connections[connection_id].get("event_filter")
|
215
220
|
return None
|
216
|
-
|
221
|
+
|
217
222
|
# Alias methods for compatibility
|
218
|
-
def event_filter(
|
223
|
+
def event_filter(
|
224
|
+
self, events: List[BaseEvent], filter_criteria: EventFilter = None
|
225
|
+
) -> List[BaseEvent]:
|
219
226
|
"""Alias for filter_events method."""
|
220
227
|
return self.filter_events(events, filter_criteria)
|
221
|
-
|
228
|
+
|
222
229
|
async def on_event(self, event: BaseEvent):
|
223
230
|
"""Handle incoming event - route to appropriate connections."""
|
224
231
|
await self.handle_event(event)
|
225
|
-
|
232
|
+
|
226
233
|
async def handle_event(self, event: BaseEvent):
|
227
234
|
"""Handle and route event to matching connections."""
|
228
235
|
await self.process_event(event)
|
229
|
-
|
236
|
+
|
230
237
|
async def process_event(self, event: BaseEvent):
|
231
238
|
"""Process event and broadcast to matching connections."""
|
232
239
|
message = {
|
233
240
|
"type": "event",
|
234
|
-
"event_type":
|
241
|
+
"event_type": (
|
242
|
+
event.event_type.value
|
243
|
+
if hasattr(event.event_type, "value")
|
244
|
+
else str(event.event_type)
|
245
|
+
),
|
235
246
|
"data": event.data,
|
236
|
-
"timestamp":
|
237
|
-
|
238
|
-
|
247
|
+
"timestamp": (
|
248
|
+
event.timestamp.isoformat()
|
249
|
+
if hasattr(event, "timestamp")
|
250
|
+
else datetime.now(timezone.utc).isoformat()
|
251
|
+
),
|
252
|
+
"session_id": getattr(event, "session_id", None),
|
253
|
+
"user_id": getattr(event, "user_id", None),
|
239
254
|
}
|
240
|
-
|
255
|
+
|
241
256
|
# Broadcast to all matching connections
|
242
257
|
for connection_id, connection in self.connections.items():
|
243
258
|
event_filter = connection.get("event_filter")
|
244
|
-
|
259
|
+
|
245
260
|
# Check if this connection should receive this event
|
246
261
|
should_send = True
|
247
262
|
if event_filter:
|
248
263
|
# Apply session filter
|
249
|
-
if
|
264
|
+
if (
|
265
|
+
event_filter.session_id
|
266
|
+
and connection["session_id"] != event_filter.session_id
|
267
|
+
):
|
250
268
|
should_send = False
|
251
|
-
|
269
|
+
|
252
270
|
# Apply user filter
|
253
|
-
if
|
271
|
+
if (
|
272
|
+
event_filter.user_id
|
273
|
+
and connection["user_id"] != event_filter.user_id
|
274
|
+
):
|
254
275
|
should_send = False
|
255
|
-
|
276
|
+
|
256
277
|
# Apply event type filter
|
257
|
-
if hasattr(event_filter,
|
278
|
+
if hasattr(event_filter, "event_types") and event_filter.event_types:
|
258
279
|
if event.event_type not in event_filter.event_types:
|
259
280
|
should_send = False
|
260
|
-
|
281
|
+
|
261
282
|
if should_send:
|
262
283
|
await self.send_to_connection(connection_id, message)
|
263
284
|
|
kailash/nodes/code/python.py
CHANGED
@@ -50,6 +50,7 @@ import ast
|
|
50
50
|
import importlib.util
|
51
51
|
import inspect
|
52
52
|
import logging
|
53
|
+
import os
|
53
54
|
import resource
|
54
55
|
import traceback
|
55
56
|
from collections.abc import Callable
|
@@ -465,6 +466,10 @@ class CodeExecutor:
|
|
465
466
|
# Normal operation - eagerly load all modules
|
466
467
|
for module_name in self.allowed_modules:
|
467
468
|
try:
|
469
|
+
# Skip scipy in CI due to version conflicts
|
470
|
+
if module_name == "scipy" and os.environ.get("CI"):
|
471
|
+
logger.warning("Skipping scipy import in CI environment")
|
472
|
+
continue
|
468
473
|
module = importlib.import_module(module_name)
|
469
474
|
namespace[module_name] = module
|
470
475
|
except ImportError:
|
kailash/runtime/local.py
CHANGED
@@ -43,6 +43,7 @@ import networkx as nx
|
|
43
43
|
|
44
44
|
from kailash.nodes import Node
|
45
45
|
from kailash.runtime.parameter_injector import WorkflowParameterInjector
|
46
|
+
from kailash.runtime.secret_provider import EnvironmentSecretProvider, SecretProvider
|
46
47
|
from kailash.sdk_exceptions import (
|
47
48
|
RuntimeExecutionError,
|
48
49
|
WorkflowExecutionError,
|
@@ -84,6 +85,7 @@ class LocalRuntime:
|
|
84
85
|
enable_security: bool = False,
|
85
86
|
enable_audit: bool = False,
|
86
87
|
resource_limits: Optional[dict[str, Any]] = None,
|
88
|
+
secret_provider: Optional[Any] = None,
|
87
89
|
):
|
88
90
|
"""Initialize the unified runtime.
|
89
91
|
|
@@ -97,12 +99,14 @@ class LocalRuntime:
|
|
97
99
|
enable_security: Whether to enable security features.
|
98
100
|
enable_audit: Whether to enable audit logging.
|
99
101
|
resource_limits: Resource limits (memory_mb, cpu_cores, etc.).
|
102
|
+
secret_provider: Optional secret provider for runtime secret injection.
|
100
103
|
"""
|
101
104
|
self.debug = debug
|
102
105
|
self.enable_cycles = enable_cycles
|
103
106
|
self.enable_async = enable_async
|
104
107
|
self.max_concurrency = max_concurrency
|
105
108
|
self.user_context = user_context
|
109
|
+
self.secret_provider = secret_provider
|
106
110
|
self.enable_monitoring = enable_monitoring
|
107
111
|
self.enable_security = enable_security
|
108
112
|
self.enable_audit = enable_audit
|
@@ -132,6 +136,22 @@ class LocalRuntime:
|
|
132
136
|
"user_context": user_context,
|
133
137
|
}
|
134
138
|
|
139
|
+
def _extract_secret_requirements(self, workflow: "Workflow") -> list:
|
140
|
+
"""Extract secret requirements from workflow nodes.
|
141
|
+
|
142
|
+
Args:
|
143
|
+
workflow: Workflow to analyze
|
144
|
+
|
145
|
+
Returns:
|
146
|
+
List of secret requirements
|
147
|
+
"""
|
148
|
+
requirements = []
|
149
|
+
for node_id, node in workflow.nodes.items():
|
150
|
+
if hasattr(node, "get_secret_requirements"):
|
151
|
+
node_requirements = node.get_secret_requirements()
|
152
|
+
requirements.extend(node_requirements)
|
153
|
+
return requirements
|
154
|
+
|
135
155
|
def execute(
|
136
156
|
self,
|
137
157
|
workflow: Workflow,
|
@@ -1057,6 +1077,52 @@ class LocalRuntime:
|
|
1057
1077
|
for warning in warnings:
|
1058
1078
|
self.logger.warning(f"Parameter validation: {warning}")
|
1059
1079
|
|
1080
|
+
# Inject secrets into the processed parameters
|
1081
|
+
if self.secret_provider:
|
1082
|
+
# Get secret requirements from workflow nodes
|
1083
|
+
requirements = self._extract_secret_requirements(workflow)
|
1084
|
+
if requirements:
|
1085
|
+
# Fetch secrets from provider
|
1086
|
+
secrets = self.secret_provider.get_secrets(requirements)
|
1087
|
+
|
1088
|
+
# Inject secrets into workflow-level parameters
|
1089
|
+
if secrets:
|
1090
|
+
# If we have workflow-level parameters, add secrets to them
|
1091
|
+
if workflow_level_params:
|
1092
|
+
workflow_level_params.update(secrets)
|
1093
|
+
|
1094
|
+
# Re-inject workflow parameters with secrets
|
1095
|
+
injector = WorkflowParameterInjector(workflow, debug=self.debug)
|
1096
|
+
injected_params = injector.transform_workflow_parameters(
|
1097
|
+
workflow_level_params
|
1098
|
+
)
|
1099
|
+
|
1100
|
+
# Merge secret-enhanced parameters
|
1101
|
+
for node_id, node_params in injected_params.items():
|
1102
|
+
if node_id not in result:
|
1103
|
+
result[node_id] = {}
|
1104
|
+
for param_name, param_value in node_params.items():
|
1105
|
+
if param_name not in result[node_id]:
|
1106
|
+
result[node_id][param_name] = param_value
|
1107
|
+
else:
|
1108
|
+
# Create workflow-level parameters from secrets only
|
1109
|
+
injector = WorkflowParameterInjector(workflow, debug=self.debug)
|
1110
|
+
injected_params = injector.transform_workflow_parameters(
|
1111
|
+
secrets
|
1112
|
+
)
|
1113
|
+
|
1114
|
+
# Merge secret parameters
|
1115
|
+
for node_id, node_params in injected_params.items():
|
1116
|
+
if node_id not in result:
|
1117
|
+
result[node_id] = {}
|
1118
|
+
for param_name, param_value in node_params.items():
|
1119
|
+
if param_name not in result[node_id]:
|
1120
|
+
result[node_id][param_name] = param_value
|
1121
|
+
|
1122
|
+
# Ensure result is not None if we added secrets
|
1123
|
+
if result is None:
|
1124
|
+
result = {}
|
1125
|
+
|
1060
1126
|
return result if result else None
|
1061
1127
|
|
1062
1128
|
def _separate_parameter_formats(
|
@@ -0,0 +1,293 @@
|
|
1
|
+
"""Runtime secret management interface and providers.
|
2
|
+
|
3
|
+
This module provides the SecretProvider interface and implementations for
|
4
|
+
injecting secrets at runtime, eliminating the need to embed secrets in
|
5
|
+
environment variables or workflow parameters.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import json
|
9
|
+
import logging
|
10
|
+
import os
|
11
|
+
from abc import ABC, abstractmethod
|
12
|
+
from typing import Any, Dict, List, Optional
|
13
|
+
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
|
17
|
+
class SecretRequirement:
|
18
|
+
"""Metadata for a required secret."""
|
19
|
+
|
20
|
+
def __init__(
|
21
|
+
self,
|
22
|
+
name: str,
|
23
|
+
parameter_name: str,
|
24
|
+
version: Optional[str] = None,
|
25
|
+
optional: bool = False,
|
26
|
+
):
|
27
|
+
"""Initialize secret requirement.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
name: Secret name in the provider (e.g., "jwt-signing-key")
|
31
|
+
parameter_name: Parameter name in the node (e.g., "secret_key")
|
32
|
+
version: Optional version identifier
|
33
|
+
optional: Whether this secret is optional
|
34
|
+
"""
|
35
|
+
self.name = name
|
36
|
+
self.parameter_name = parameter_name
|
37
|
+
self.version = version
|
38
|
+
self.optional = optional
|
39
|
+
|
40
|
+
|
41
|
+
class SecretProvider(ABC):
|
42
|
+
"""Base interface for secret providers."""
|
43
|
+
|
44
|
+
@abstractmethod
|
45
|
+
def get_secret(self, name: str, version: Optional[str] = None) -> str:
|
46
|
+
"""Fetch a secret by name and optional version.
|
47
|
+
|
48
|
+
Args:
|
49
|
+
name: Secret name
|
50
|
+
version: Optional version identifier
|
51
|
+
|
52
|
+
Returns:
|
53
|
+
Secret value as string
|
54
|
+
|
55
|
+
Raises:
|
56
|
+
SecretNotFoundError: If secret doesn't exist
|
57
|
+
SecretProviderError: If provider operation fails
|
58
|
+
"""
|
59
|
+
pass
|
60
|
+
|
61
|
+
@abstractmethod
|
62
|
+
def list_secrets(self) -> List[str]:
|
63
|
+
"""List available secrets.
|
64
|
+
|
65
|
+
Returns:
|
66
|
+
List of secret names
|
67
|
+
"""
|
68
|
+
pass
|
69
|
+
|
70
|
+
def get_secrets(self, requirements: List[SecretRequirement]) -> Dict[str, str]:
|
71
|
+
"""Fetch multiple secrets based on requirements.
|
72
|
+
|
73
|
+
Args:
|
74
|
+
requirements: List of secret requirements
|
75
|
+
|
76
|
+
Returns:
|
77
|
+
Dictionary mapping parameter names to secret values
|
78
|
+
"""
|
79
|
+
secrets = {}
|
80
|
+
for req in requirements:
|
81
|
+
try:
|
82
|
+
secret_value = self.get_secret(req.name, req.version)
|
83
|
+
secrets[req.parameter_name] = secret_value
|
84
|
+
except Exception as e:
|
85
|
+
if req.optional:
|
86
|
+
logger.warning(f"Optional secret {req.name} not found: {e}")
|
87
|
+
continue
|
88
|
+
else:
|
89
|
+
raise
|
90
|
+
return secrets
|
91
|
+
|
92
|
+
|
93
|
+
class EnvironmentSecretProvider(SecretProvider):
|
94
|
+
"""Secret provider that fetches secrets from environment variables.
|
95
|
+
|
96
|
+
This provider maintains backward compatibility by reading secrets from
|
97
|
+
environment variables, but provides a secure interface for runtime injection.
|
98
|
+
"""
|
99
|
+
|
100
|
+
def __init__(self, prefix: str = "KAILASH_SECRET_"):
|
101
|
+
"""Initialize environment secret provider.
|
102
|
+
|
103
|
+
Args:
|
104
|
+
prefix: Prefix for environment variables containing secrets
|
105
|
+
"""
|
106
|
+
self.prefix = prefix
|
107
|
+
|
108
|
+
def get_secret(self, name: str, version: Optional[str] = None) -> str:
|
109
|
+
"""Get secret from environment variable.
|
110
|
+
|
111
|
+
Args:
|
112
|
+
name: Secret name (will be prefixed and uppercased)
|
113
|
+
version: Ignored for environment provider
|
114
|
+
|
115
|
+
Returns:
|
116
|
+
Secret value from environment
|
117
|
+
|
118
|
+
Raises:
|
119
|
+
SecretNotFoundError: If environment variable not found
|
120
|
+
"""
|
121
|
+
# Convert name to environment variable format
|
122
|
+
env_name = f"{self.prefix}{name.upper().replace('-', '_')}"
|
123
|
+
|
124
|
+
secret_value = os.environ.get(env_name)
|
125
|
+
if secret_value is None:
|
126
|
+
# Try without prefix for backward compatibility
|
127
|
+
secret_value = os.environ.get(name.upper().replace("-", "_"))
|
128
|
+
|
129
|
+
if secret_value is None:
|
130
|
+
raise SecretNotFoundError(
|
131
|
+
f"Secret '{name}' not found in environment variables"
|
132
|
+
)
|
133
|
+
|
134
|
+
return secret_value
|
135
|
+
|
136
|
+
def list_secrets(self) -> List[str]:
|
137
|
+
"""List all secrets available in environment.
|
138
|
+
|
139
|
+
Returns:
|
140
|
+
List of secret names (without prefix)
|
141
|
+
"""
|
142
|
+
secrets = []
|
143
|
+
for key in os.environ:
|
144
|
+
if key.startswith(self.prefix):
|
145
|
+
# Remove prefix and convert back to secret name format
|
146
|
+
secret_name = key[len(self.prefix) :].lower().replace("_", "-")
|
147
|
+
secrets.append(secret_name)
|
148
|
+
return secrets
|
149
|
+
|
150
|
+
|
151
|
+
class VaultSecretProvider(SecretProvider):
|
152
|
+
"""Secret provider for HashiCorp Vault.
|
153
|
+
|
154
|
+
This provider integrates with HashiCorp Vault for enterprise secret management.
|
155
|
+
"""
|
156
|
+
|
157
|
+
def __init__(self, vault_url: str, vault_token: str, mount_path: str = "secret"):
|
158
|
+
"""Initialize Vault secret provider.
|
159
|
+
|
160
|
+
Args:
|
161
|
+
vault_url: Vault server URL
|
162
|
+
vault_token: Vault authentication token
|
163
|
+
mount_path: Vault mount path for secrets
|
164
|
+
"""
|
165
|
+
self.vault_url = vault_url
|
166
|
+
self.vault_token = vault_token
|
167
|
+
self.mount_path = mount_path
|
168
|
+
self._client = None
|
169
|
+
|
170
|
+
@property
|
171
|
+
def client(self):
|
172
|
+
"""Lazy initialization of Vault client."""
|
173
|
+
if self._client is None:
|
174
|
+
try:
|
175
|
+
import hvac
|
176
|
+
|
177
|
+
self._client = hvac.Client(url=self.vault_url, token=self.vault_token)
|
178
|
+
except ImportError:
|
179
|
+
raise RuntimeError(
|
180
|
+
"hvac library not installed. Install with: pip install hvac"
|
181
|
+
)
|
182
|
+
return self._client
|
183
|
+
|
184
|
+
def get_secret(self, name: str, version: Optional[str] = None) -> str:
|
185
|
+
"""Get secret from Vault.
|
186
|
+
|
187
|
+
Args:
|
188
|
+
name: Secret path in Vault
|
189
|
+
version: Optional version (for KV v2)
|
190
|
+
|
191
|
+
Returns:
|
192
|
+
Secret value
|
193
|
+
"""
|
194
|
+
try:
|
195
|
+
# Try KV v2 first
|
196
|
+
response = self.client.secrets.kv.v2.read_secret_version(
|
197
|
+
path=name, version=version, mount_point=self.mount_path
|
198
|
+
)
|
199
|
+
return response["data"]["data"]["value"]
|
200
|
+
except Exception:
|
201
|
+
# Fall back to KV v1
|
202
|
+
response = self.client.secrets.kv.v1.read_secret(
|
203
|
+
path=name, mount_point=self.mount_path
|
204
|
+
)
|
205
|
+
return response["data"]["value"]
|
206
|
+
|
207
|
+
def list_secrets(self) -> List[str]:
|
208
|
+
"""List all secrets in Vault.
|
209
|
+
|
210
|
+
Returns:
|
211
|
+
List of secret paths
|
212
|
+
"""
|
213
|
+
try:
|
214
|
+
response = self.client.secrets.kv.v2.list_secrets(
|
215
|
+
path="", mount_point=self.mount_path
|
216
|
+
)
|
217
|
+
return response["data"]["keys"]
|
218
|
+
except Exception:
|
219
|
+
# Fall back to KV v1
|
220
|
+
response = self.client.secrets.kv.v1.list_secrets(
|
221
|
+
path="", mount_point=self.mount_path
|
222
|
+
)
|
223
|
+
return response["data"]["keys"]
|
224
|
+
|
225
|
+
|
226
|
+
class AWSSecretProvider(SecretProvider):
|
227
|
+
"""Secret provider for AWS Secrets Manager.
|
228
|
+
|
229
|
+
This provider integrates with AWS Secrets Manager for cloud-native secret management.
|
230
|
+
"""
|
231
|
+
|
232
|
+
def __init__(self, region_name: str = "us-east-1"):
|
233
|
+
"""Initialize AWS secret provider.
|
234
|
+
|
235
|
+
Args:
|
236
|
+
region_name: AWS region
|
237
|
+
"""
|
238
|
+
self.region_name = region_name
|
239
|
+
self._client = None
|
240
|
+
|
241
|
+
@property
|
242
|
+
def client(self):
|
243
|
+
"""Lazy initialization of AWS client."""
|
244
|
+
if self._client is None:
|
245
|
+
try:
|
246
|
+
import boto3
|
247
|
+
|
248
|
+
self._client = boto3.client(
|
249
|
+
"secretsmanager", region_name=self.region_name
|
250
|
+
)
|
251
|
+
except ImportError:
|
252
|
+
raise RuntimeError(
|
253
|
+
"boto3 library not installed. Install with: pip install boto3"
|
254
|
+
)
|
255
|
+
return self._client
|
256
|
+
|
257
|
+
def get_secret(self, name: str, version: Optional[str] = None) -> str:
|
258
|
+
"""Get secret from AWS Secrets Manager.
|
259
|
+
|
260
|
+
Args:
|
261
|
+
name: Secret name in AWS
|
262
|
+
version: Optional version ID
|
263
|
+
|
264
|
+
Returns:
|
265
|
+
Secret value
|
266
|
+
"""
|
267
|
+
kwargs = {"SecretId": name}
|
268
|
+
if version:
|
269
|
+
kwargs["VersionId"] = version
|
270
|
+
|
271
|
+
response = self.client.get_secret_value(**kwargs)
|
272
|
+
return response["SecretString"]
|
273
|
+
|
274
|
+
def list_secrets(self) -> List[str]:
|
275
|
+
"""List all secrets in AWS Secrets Manager.
|
276
|
+
|
277
|
+
Returns:
|
278
|
+
List of secret names
|
279
|
+
"""
|
280
|
+
response = self.client.list_secrets()
|
281
|
+
return [secret["Name"] for secret in response["SecretList"]]
|
282
|
+
|
283
|
+
|
284
|
+
class SecretNotFoundError(Exception):
|
285
|
+
"""Raised when a secret cannot be found."""
|
286
|
+
|
287
|
+
pass
|
288
|
+
|
289
|
+
|
290
|
+
class SecretProviderError(Exception):
|
291
|
+
"""Raised when a secret provider operation fails."""
|
292
|
+
|
293
|
+
pass
|