daita-agents 0.1.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.
Potentially problematic release.
This version of daita-agents might be problematic. Click here for more details.
- daita/__init__.py +208 -0
- daita/agents/__init__.py +33 -0
- daita/agents/base.py +722 -0
- daita/agents/substrate.py +895 -0
- daita/cli/__init__.py +145 -0
- daita/cli/__main__.py +7 -0
- daita/cli/ascii_art.py +44 -0
- daita/cli/core/__init__.py +0 -0
- daita/cli/core/create.py +254 -0
- daita/cli/core/deploy.py +473 -0
- daita/cli/core/deployments.py +309 -0
- daita/cli/core/import_detector.py +219 -0
- daita/cli/core/init.py +382 -0
- daita/cli/core/logs.py +239 -0
- daita/cli/core/managed_deploy.py +709 -0
- daita/cli/core/run.py +648 -0
- daita/cli/core/status.py +421 -0
- daita/cli/core/test.py +239 -0
- daita/cli/core/webhooks.py +172 -0
- daita/cli/main.py +588 -0
- daita/cli/utils.py +541 -0
- daita/config/__init__.py +62 -0
- daita/config/base.py +159 -0
- daita/config/settings.py +184 -0
- daita/core/__init__.py +262 -0
- daita/core/decision_tracing.py +701 -0
- daita/core/exceptions.py +480 -0
- daita/core/focus.py +251 -0
- daita/core/interfaces.py +76 -0
- daita/core/plugin_tracing.py +550 -0
- daita/core/relay.py +695 -0
- daita/core/reliability.py +381 -0
- daita/core/scaling.py +444 -0
- daita/core/tools.py +402 -0
- daita/core/tracing.py +770 -0
- daita/core/workflow.py +1084 -0
- daita/display/__init__.py +1 -0
- daita/display/console.py +160 -0
- daita/execution/__init__.py +58 -0
- daita/execution/client.py +856 -0
- daita/execution/exceptions.py +92 -0
- daita/execution/models.py +317 -0
- daita/llm/__init__.py +60 -0
- daita/llm/anthropic.py +166 -0
- daita/llm/base.py +373 -0
- daita/llm/factory.py +101 -0
- daita/llm/gemini.py +152 -0
- daita/llm/grok.py +114 -0
- daita/llm/mock.py +135 -0
- daita/llm/openai.py +109 -0
- daita/plugins/__init__.py +141 -0
- daita/plugins/base.py +37 -0
- daita/plugins/base_db.py +167 -0
- daita/plugins/elasticsearch.py +844 -0
- daita/plugins/mcp.py +481 -0
- daita/plugins/mongodb.py +510 -0
- daita/plugins/mysql.py +351 -0
- daita/plugins/postgresql.py +331 -0
- daita/plugins/redis_messaging.py +500 -0
- daita/plugins/rest.py +529 -0
- daita/plugins/s3.py +761 -0
- daita/plugins/slack.py +729 -0
- daita/utils/__init__.py +18 -0
- daita_agents-0.1.0.dist-info/METADATA +350 -0
- daita_agents-0.1.0.dist-info/RECORD +69 -0
- daita_agents-0.1.0.dist-info/WHEEL +5 -0
- daita_agents-0.1.0.dist-info/entry_points.txt +2 -0
- daita_agents-0.1.0.dist-info/licenses/LICENSE +56 -0
- daita_agents-0.1.0.dist-info/top_level.txt +1 -0
daita/core/exceptions.py
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core exceptions for Daita Agents.
|
|
3
|
+
|
|
4
|
+
Provides a hierarchy of exceptions with built-in retry behavior hints
|
|
5
|
+
to help agents make intelligent retry decisions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
class DaitaError(Exception):
|
|
9
|
+
"""Base exception for all Daita errors."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, message: str, retry_hint: str = "unknown", context: dict = None):
|
|
12
|
+
"""
|
|
13
|
+
Initialize Daita error.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
message: Error message
|
|
17
|
+
retry_hint: Hint for retry behavior ("transient", "retryable", "permanent", "unknown")
|
|
18
|
+
context: Additional error context
|
|
19
|
+
"""
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
self.retry_hint = retry_hint
|
|
22
|
+
self.context = context or {}
|
|
23
|
+
|
|
24
|
+
def is_transient(self) -> bool:
|
|
25
|
+
"""Check if this error is likely transient."""
|
|
26
|
+
return self.retry_hint == "transient"
|
|
27
|
+
|
|
28
|
+
def is_retryable(self) -> bool:
|
|
29
|
+
"""Check if this error might be retryable."""
|
|
30
|
+
return self.retry_hint in ["transient", "retryable"]
|
|
31
|
+
|
|
32
|
+
def is_permanent(self) -> bool:
|
|
33
|
+
"""Check if this error is permanent."""
|
|
34
|
+
return self.retry_hint == "permanent"
|
|
35
|
+
|
|
36
|
+
class AgentError(DaitaError):
|
|
37
|
+
"""Exception raised by agents during operation."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, message: str, agent_id: str = None, task: str = None, retry_hint: str = "retryable", context: dict = None):
|
|
40
|
+
"""
|
|
41
|
+
Initialize agent error.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
message: Error message
|
|
45
|
+
agent_id: ID of the agent that failed
|
|
46
|
+
task: Task that was being executed
|
|
47
|
+
retry_hint: Hint for retry behavior
|
|
48
|
+
context: Additional error context
|
|
49
|
+
"""
|
|
50
|
+
super().__init__(message, retry_hint, context)
|
|
51
|
+
self.agent_id = agent_id
|
|
52
|
+
self.task = task
|
|
53
|
+
|
|
54
|
+
class ConfigError(DaitaError):
|
|
55
|
+
"""Exception raised for configuration issues."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, message: str, config_section: str = None, context: dict = None):
|
|
58
|
+
"""
|
|
59
|
+
Initialize configuration error.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
message: Error message
|
|
63
|
+
config_section: Section of config that caused the error
|
|
64
|
+
context: Additional error context
|
|
65
|
+
"""
|
|
66
|
+
super().__init__(message, retry_hint="permanent", context=context)
|
|
67
|
+
self.config_section = config_section
|
|
68
|
+
|
|
69
|
+
class LLMError(DaitaError):
|
|
70
|
+
"""Exception raised by LLM providers."""
|
|
71
|
+
|
|
72
|
+
def __init__(self, message: str, provider: str = None, model: str = None, retry_hint: str = "retryable", context: dict = None):
|
|
73
|
+
"""
|
|
74
|
+
Initialize LLM error.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
message: Error message
|
|
78
|
+
provider: LLM provider name
|
|
79
|
+
model: Model name
|
|
80
|
+
retry_hint: Hint for retry behavior
|
|
81
|
+
context: Additional error context
|
|
82
|
+
"""
|
|
83
|
+
super().__init__(message, retry_hint, context)
|
|
84
|
+
self.provider = provider
|
|
85
|
+
self.model = model
|
|
86
|
+
|
|
87
|
+
class PluginError(DaitaError):
|
|
88
|
+
"""Exception raised by plugins."""
|
|
89
|
+
|
|
90
|
+
def __init__(self, message: str, plugin_name: str = None, retry_hint: str = "retryable", context: dict = None):
|
|
91
|
+
"""
|
|
92
|
+
Initialize plugin error.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
message: Error message
|
|
96
|
+
plugin_name: Name of the plugin that failed
|
|
97
|
+
retry_hint: Hint for retry behavior
|
|
98
|
+
context: Additional error context
|
|
99
|
+
"""
|
|
100
|
+
super().__init__(message, retry_hint, context)
|
|
101
|
+
self.plugin_name = plugin_name
|
|
102
|
+
|
|
103
|
+
class WorkflowError(DaitaError):
|
|
104
|
+
"""Exception raised by workflow operations."""
|
|
105
|
+
|
|
106
|
+
def __init__(self, message: str, workflow_name: str = None, retry_hint: str = "retryable", context: dict = None):
|
|
107
|
+
"""
|
|
108
|
+
Initialize workflow error.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
message: Error message
|
|
112
|
+
workflow_name: Name of the workflow that failed
|
|
113
|
+
retry_hint: Hint for retry behavior
|
|
114
|
+
context: Additional error context
|
|
115
|
+
"""
|
|
116
|
+
super().__init__(message, retry_hint, context)
|
|
117
|
+
self.workflow_name = workflow_name
|
|
118
|
+
|
|
119
|
+
# ======= Retry-Specific Exception Classes =======
|
|
120
|
+
|
|
121
|
+
class TransientError(DaitaError):
|
|
122
|
+
"""
|
|
123
|
+
Exception for temporary issues that are likely to resolve quickly.
|
|
124
|
+
|
|
125
|
+
Examples: Network timeouts, rate limits, temporary service unavailability
|
|
126
|
+
These errors should be retried with minimal delay.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __init__(self, message: str, context: dict = None):
|
|
130
|
+
super().__init__(message, retry_hint="transient", context=context)
|
|
131
|
+
|
|
132
|
+
class RetryableError(DaitaError):
|
|
133
|
+
"""
|
|
134
|
+
Exception for issues that might be resolved with a different approach or after delay.
|
|
135
|
+
|
|
136
|
+
Examples: Resource temporarily unavailable, processing queue full,
|
|
137
|
+
temporary data inconsistency. These errors should be retried with backoff.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
def __init__(self, message: str, context: dict = None):
|
|
141
|
+
super().__init__(message, retry_hint="retryable", context=context)
|
|
142
|
+
|
|
143
|
+
class PermanentError(DaitaError):
|
|
144
|
+
"""
|
|
145
|
+
Exception for issues that will not be resolved by retrying.
|
|
146
|
+
|
|
147
|
+
Examples: Authentication failures, permission errors, invalid configuration,
|
|
148
|
+
malformed data. These errors should not be retried.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
def __init__(self, message: str, context: dict = None):
|
|
152
|
+
super().__init__(message, retry_hint="permanent", context=context)
|
|
153
|
+
|
|
154
|
+
# ======= Specific Transient Errors =======
|
|
155
|
+
|
|
156
|
+
class RateLimitError(TransientError):
|
|
157
|
+
"""Exception for API rate limiting."""
|
|
158
|
+
|
|
159
|
+
def __init__(self, message: str = "Rate limit exceeded", retry_after: int = None, context: dict = None):
|
|
160
|
+
context = context or {}
|
|
161
|
+
if retry_after:
|
|
162
|
+
context['retry_after'] = retry_after
|
|
163
|
+
message = f"{message} (retry after {retry_after}s)"
|
|
164
|
+
super().__init__(message, context)
|
|
165
|
+
self.retry_after = retry_after
|
|
166
|
+
|
|
167
|
+
class TimeoutError(TransientError):
|
|
168
|
+
"""Exception for timeout issues."""
|
|
169
|
+
|
|
170
|
+
def __init__(self, message: str = "Operation timed out", timeout_duration: float = None, context: dict = None):
|
|
171
|
+
context = context or {}
|
|
172
|
+
if timeout_duration:
|
|
173
|
+
context['timeout_duration'] = timeout_duration
|
|
174
|
+
message = f"{message} (after {timeout_duration}s)"
|
|
175
|
+
super().__init__(message, context)
|
|
176
|
+
self.timeout_duration = timeout_duration
|
|
177
|
+
|
|
178
|
+
class ConnectionError(TransientError):
|
|
179
|
+
"""Exception for connection issues."""
|
|
180
|
+
|
|
181
|
+
def __init__(self, message: str = "Connection failed", host: str = None, port: int = None, context: dict = None):
|
|
182
|
+
context = context or {}
|
|
183
|
+
if host:
|
|
184
|
+
context['host'] = host
|
|
185
|
+
if port:
|
|
186
|
+
context['port'] = port
|
|
187
|
+
message = f"{message} (to {host}:{port})" if host else f"{message} (port {port})"
|
|
188
|
+
super().__init__(message, context)
|
|
189
|
+
self.host = host
|
|
190
|
+
self.port = port
|
|
191
|
+
|
|
192
|
+
class ServiceUnavailableError(TransientError):
|
|
193
|
+
"""Exception for service unavailability."""
|
|
194
|
+
|
|
195
|
+
def __init__(self, message: str = "Service temporarily unavailable", service_name: str = None, context: dict = None):
|
|
196
|
+
context = context or {}
|
|
197
|
+
if service_name:
|
|
198
|
+
context['service_name'] = service_name
|
|
199
|
+
message = f"{message}: {service_name}"
|
|
200
|
+
super().__init__(message, context)
|
|
201
|
+
self.service_name = service_name
|
|
202
|
+
|
|
203
|
+
class TemporaryError(TransientError):
|
|
204
|
+
"""Generic temporary error that should retry quickly."""
|
|
205
|
+
pass
|
|
206
|
+
|
|
207
|
+
class TooManyRequestsError(TransientError):
|
|
208
|
+
"""Exception for too many requests (429 HTTP status)."""
|
|
209
|
+
|
|
210
|
+
def __init__(self, message: str = "Too many requests", retry_after: int = None, context: dict = None):
|
|
211
|
+
context = context or {}
|
|
212
|
+
if retry_after:
|
|
213
|
+
context['retry_after'] = retry_after
|
|
214
|
+
super().__init__(message, context)
|
|
215
|
+
self.retry_after = retry_after
|
|
216
|
+
|
|
217
|
+
# ======= Specific Retryable Errors =======
|
|
218
|
+
|
|
219
|
+
class ResourceBusyError(RetryableError):
|
|
220
|
+
"""Exception for busy resources that might become available."""
|
|
221
|
+
|
|
222
|
+
def __init__(self, message: str = "Resource is busy", resource_name: str = None, context: dict = None):
|
|
223
|
+
context = context or {}
|
|
224
|
+
if resource_name:
|
|
225
|
+
context['resource_name'] = resource_name
|
|
226
|
+
message = f"{message}: {resource_name}"
|
|
227
|
+
super().__init__(message, context)
|
|
228
|
+
self.resource_name = resource_name
|
|
229
|
+
|
|
230
|
+
class DataInconsistencyError(RetryableError):
|
|
231
|
+
"""Exception for temporary data inconsistency."""
|
|
232
|
+
|
|
233
|
+
def __init__(self, message: str = "Data inconsistency detected", data_source: str = None, context: dict = None):
|
|
234
|
+
context = context or {}
|
|
235
|
+
if data_source:
|
|
236
|
+
context['data_source'] = data_source
|
|
237
|
+
super().__init__(message, context)
|
|
238
|
+
self.data_source = data_source
|
|
239
|
+
|
|
240
|
+
class ProcessingQueueFullError(RetryableError):
|
|
241
|
+
"""Exception for full processing queues."""
|
|
242
|
+
|
|
243
|
+
def __init__(self, message: str = "Processing queue is full", queue_name: str = None, context: dict = None):
|
|
244
|
+
context = context or {}
|
|
245
|
+
if queue_name:
|
|
246
|
+
context['queue_name'] = queue_name
|
|
247
|
+
super().__init__(message, context)
|
|
248
|
+
self.queue_name = queue_name
|
|
249
|
+
|
|
250
|
+
# ======= Specific Permanent Errors =======
|
|
251
|
+
|
|
252
|
+
class AuthenticationError(PermanentError):
|
|
253
|
+
"""Exception for authentication failures."""
|
|
254
|
+
|
|
255
|
+
def __init__(self, message: str = "Authentication failed", provider: str = None, context: dict = None):
|
|
256
|
+
context = context or {}
|
|
257
|
+
if provider:
|
|
258
|
+
context['provider'] = provider
|
|
259
|
+
super().__init__(message, context)
|
|
260
|
+
self.provider = provider
|
|
261
|
+
|
|
262
|
+
class PermissionError(PermanentError):
|
|
263
|
+
"""Exception for permission/authorization failures."""
|
|
264
|
+
|
|
265
|
+
def __init__(self, message: str = "Permission denied", resource: str = None, action: str = None, context: dict = None):
|
|
266
|
+
context = context or {}
|
|
267
|
+
if resource:
|
|
268
|
+
context['resource'] = resource
|
|
269
|
+
if action:
|
|
270
|
+
context['action'] = action
|
|
271
|
+
super().__init__(message, context)
|
|
272
|
+
self.resource = resource
|
|
273
|
+
self.action = action
|
|
274
|
+
|
|
275
|
+
class ValidationError(PermanentError):
|
|
276
|
+
"""Exception for data validation failures."""
|
|
277
|
+
|
|
278
|
+
def __init__(self, message: str = "Validation failed", field: str = None, value: str = None, context: dict = None):
|
|
279
|
+
context = context or {}
|
|
280
|
+
if field:
|
|
281
|
+
context['field'] = field
|
|
282
|
+
if value:
|
|
283
|
+
context['value'] = str(value)[:100] # Truncate long values
|
|
284
|
+
super().__init__(message, context)
|
|
285
|
+
self.field = field
|
|
286
|
+
self.value = value
|
|
287
|
+
|
|
288
|
+
class InvalidDataError(PermanentError):
|
|
289
|
+
"""Exception for invalid or malformed data."""
|
|
290
|
+
|
|
291
|
+
def __init__(self, message: str = "Invalid data format", data_type: str = None, expected_format: str = None, context: dict = None):
|
|
292
|
+
context = context or {}
|
|
293
|
+
if data_type:
|
|
294
|
+
context['data_type'] = data_type
|
|
295
|
+
if expected_format:
|
|
296
|
+
context['expected_format'] = expected_format
|
|
297
|
+
super().__init__(message, context)
|
|
298
|
+
self.data_type = data_type
|
|
299
|
+
self.expected_format = expected_format
|
|
300
|
+
|
|
301
|
+
class NotFoundError(PermanentError):
|
|
302
|
+
"""Exception for missing resources."""
|
|
303
|
+
|
|
304
|
+
def __init__(self, message: str = "Resource not found", resource_type: str = None, resource_id: str = None, context: dict = None):
|
|
305
|
+
context = context or {}
|
|
306
|
+
if resource_type:
|
|
307
|
+
context['resource_type'] = resource_type
|
|
308
|
+
if resource_id:
|
|
309
|
+
context['resource_id'] = resource_id
|
|
310
|
+
super().__init__(message, context)
|
|
311
|
+
self.resource_type = resource_type
|
|
312
|
+
self.resource_id = resource_id
|
|
313
|
+
|
|
314
|
+
class BadRequestError(PermanentError):
|
|
315
|
+
"""Exception for malformed requests."""
|
|
316
|
+
|
|
317
|
+
def __init__(self, message: str = "Bad request", request_type: str = None, context: dict = None):
|
|
318
|
+
context = context or {}
|
|
319
|
+
if request_type:
|
|
320
|
+
context['request_type'] = request_type
|
|
321
|
+
super().__init__(message, context)
|
|
322
|
+
self.request_type = request_type
|
|
323
|
+
|
|
324
|
+
# ======= Circuit Breaker Specific Errors =======
|
|
325
|
+
|
|
326
|
+
class CircuitBreakerOpenError(PermanentError):
|
|
327
|
+
"""Exception when circuit breaker is open."""
|
|
328
|
+
|
|
329
|
+
def __init__(self, message: str = "Circuit breaker is open", agent_name: str = None, failure_count: int = None, context: dict = None):
|
|
330
|
+
context = context or {}
|
|
331
|
+
if agent_name:
|
|
332
|
+
context['agent_name'] = agent_name
|
|
333
|
+
if failure_count:
|
|
334
|
+
context['failure_count'] = failure_count
|
|
335
|
+
super().__init__(message, context)
|
|
336
|
+
self.agent_name = agent_name
|
|
337
|
+
self.failure_count = failure_count
|
|
338
|
+
|
|
339
|
+
# ======= Reliability Infrastructure Errors =======
|
|
340
|
+
|
|
341
|
+
class BackpressureError(RetryableError):
|
|
342
|
+
"""Exception when backpressure limits are exceeded."""
|
|
343
|
+
|
|
344
|
+
def __init__(self, message: str = "Backpressure limit exceeded", agent_id: str = None, queue_size: int = None, context: dict = None):
|
|
345
|
+
context = context or {}
|
|
346
|
+
if agent_id:
|
|
347
|
+
context['agent_id'] = agent_id
|
|
348
|
+
if queue_size is not None:
|
|
349
|
+
context['queue_size'] = queue_size
|
|
350
|
+
message = f"{message} (queue size: {queue_size})"
|
|
351
|
+
super().__init__(message, context)
|
|
352
|
+
self.agent_id = agent_id
|
|
353
|
+
self.queue_size = queue_size
|
|
354
|
+
|
|
355
|
+
class TaskTimeoutError(TransientError):
|
|
356
|
+
"""Exception when a task times out."""
|
|
357
|
+
|
|
358
|
+
def __init__(self, message: str = "Task execution timed out", task_id: str = None, timeout_duration: float = None, context: dict = None):
|
|
359
|
+
context = context or {}
|
|
360
|
+
if task_id:
|
|
361
|
+
context['task_id'] = task_id
|
|
362
|
+
if timeout_duration:
|
|
363
|
+
context['timeout_duration'] = timeout_duration
|
|
364
|
+
message = f"{message} after {timeout_duration}s"
|
|
365
|
+
super().__init__(message, context)
|
|
366
|
+
self.task_id = task_id
|
|
367
|
+
self.timeout_duration = timeout_duration
|
|
368
|
+
|
|
369
|
+
class AcknowledgmentTimeoutError(TransientError):
|
|
370
|
+
"""Exception when message acknowledgment times out."""
|
|
371
|
+
|
|
372
|
+
def __init__(self, message: str = "Message acknowledgment timed out", message_id: str = None, timeout_duration: float = None, context: dict = None):
|
|
373
|
+
context = context or {}
|
|
374
|
+
if message_id:
|
|
375
|
+
context['message_id'] = message_id
|
|
376
|
+
if timeout_duration:
|
|
377
|
+
context['timeout_duration'] = timeout_duration
|
|
378
|
+
super().__init__(message, context)
|
|
379
|
+
self.message_id = message_id
|
|
380
|
+
self.timeout_duration = timeout_duration
|
|
381
|
+
|
|
382
|
+
class TaskNotFoundError(PermanentError):
|
|
383
|
+
"""Exception when a referenced task cannot be found."""
|
|
384
|
+
|
|
385
|
+
def __init__(self, message: str = "Task not found", task_id: str = None, context: dict = None):
|
|
386
|
+
context = context or {}
|
|
387
|
+
if task_id:
|
|
388
|
+
context['task_id'] = task_id
|
|
389
|
+
message = f"{message}: {task_id}"
|
|
390
|
+
super().__init__(message, context)
|
|
391
|
+
self.task_id = task_id
|
|
392
|
+
|
|
393
|
+
class ReliabilityConfigurationError(PermanentError):
|
|
394
|
+
"""Exception for invalid reliability configuration."""
|
|
395
|
+
|
|
396
|
+
def __init__(self, message: str = "Invalid reliability configuration", config_key: str = None, context: dict = None):
|
|
397
|
+
context = context or {}
|
|
398
|
+
if config_key:
|
|
399
|
+
context['config_key'] = config_key
|
|
400
|
+
message = f"{message}: {config_key}"
|
|
401
|
+
super().__init__(message, context)
|
|
402
|
+
self.config_key = config_key
|
|
403
|
+
|
|
404
|
+
class DeadLetterQueueError(RetryableError):
|
|
405
|
+
"""Exception related to dead letter queue operations."""
|
|
406
|
+
|
|
407
|
+
def __init__(self, message: str = "Dead letter queue operation failed", operation: str = None, context: dict = None):
|
|
408
|
+
context = context or {}
|
|
409
|
+
if operation:
|
|
410
|
+
context['operation'] = operation
|
|
411
|
+
message = f"{message}: {operation}"
|
|
412
|
+
super().__init__(message, context)
|
|
413
|
+
self.operation = operation
|
|
414
|
+
|
|
415
|
+
# ======= Utility Functions =======
|
|
416
|
+
|
|
417
|
+
def classify_exception(exception: Exception) -> str:
|
|
418
|
+
"""
|
|
419
|
+
Classify any exception to determine retry behavior.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
exception: The exception to classify
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
Retry hint: "transient", "retryable", "permanent", or "unknown"
|
|
426
|
+
"""
|
|
427
|
+
# If it's already a Daita exception, use its hint
|
|
428
|
+
if isinstance(exception, DaitaError):
|
|
429
|
+
return exception.retry_hint
|
|
430
|
+
|
|
431
|
+
# Classify standard Python exceptions
|
|
432
|
+
exception_name = exception.__class__.__name__
|
|
433
|
+
|
|
434
|
+
# Transient errors (standard library)
|
|
435
|
+
transient_exceptions = {
|
|
436
|
+
'TimeoutError', 'ConnectionError', 'ConnectionResetError',
|
|
437
|
+
'ConnectionAbortedError', 'ConnectionRefusedError',
|
|
438
|
+
'OSError', 'IOError', 'socket.timeout'
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
# Permanent errors (standard library)
|
|
442
|
+
permanent_exceptions = {
|
|
443
|
+
'ValueError', 'TypeError', 'AttributeError', 'KeyError',
|
|
444
|
+
'IndexError', 'NameError', 'SyntaxError', 'ImportError',
|
|
445
|
+
'FileNotFoundError', 'PermissionError'
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if exception_name in transient_exceptions:
|
|
449
|
+
return "transient"
|
|
450
|
+
elif exception_name in permanent_exceptions:
|
|
451
|
+
return "permanent"
|
|
452
|
+
else:
|
|
453
|
+
return "retryable" # Default to retryable for unknown exceptions
|
|
454
|
+
|
|
455
|
+
def create_contextual_error(
|
|
456
|
+
base_exception: Exception,
|
|
457
|
+
context: dict = None,
|
|
458
|
+
retry_hint: str = None
|
|
459
|
+
) -> DaitaError:
|
|
460
|
+
"""
|
|
461
|
+
Wrap a standard exception in a Daita exception with context.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
base_exception: The original exception
|
|
465
|
+
context: Additional context information
|
|
466
|
+
retry_hint: Override retry hint classification
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
Wrapped DaitaError with context and retry hint
|
|
470
|
+
"""
|
|
471
|
+
message = str(base_exception)
|
|
472
|
+
hint = retry_hint or classify_exception(base_exception)
|
|
473
|
+
|
|
474
|
+
# Choose appropriate Daita exception type
|
|
475
|
+
if hint == "transient":
|
|
476
|
+
return TransientError(message, context)
|
|
477
|
+
elif hint == "permanent":
|
|
478
|
+
return PermanentError(message, context)
|
|
479
|
+
else:
|
|
480
|
+
return RetryableError(message, context)
|