iflow-mcp_democratize-technology-chronos-mcp 2.0.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.
- chronos_mcp/__init__.py +5 -0
- chronos_mcp/__main__.py +9 -0
- chronos_mcp/accounts.py +410 -0
- chronos_mcp/bulk.py +946 -0
- chronos_mcp/caldav_utils.py +149 -0
- chronos_mcp/calendars.py +204 -0
- chronos_mcp/config.py +187 -0
- chronos_mcp/credentials.py +190 -0
- chronos_mcp/events.py +515 -0
- chronos_mcp/exceptions.py +477 -0
- chronos_mcp/journals.py +477 -0
- chronos_mcp/logging_config.py +23 -0
- chronos_mcp/models.py +202 -0
- chronos_mcp/py.typed +0 -0
- chronos_mcp/rrule.py +259 -0
- chronos_mcp/search.py +315 -0
- chronos_mcp/server.py +121 -0
- chronos_mcp/tasks.py +518 -0
- chronos_mcp/tools/__init__.py +29 -0
- chronos_mcp/tools/accounts.py +151 -0
- chronos_mcp/tools/base.py +59 -0
- chronos_mcp/tools/bulk.py +557 -0
- chronos_mcp/tools/calendars.py +142 -0
- chronos_mcp/tools/events.py +698 -0
- chronos_mcp/tools/journals.py +310 -0
- chronos_mcp/tools/tasks.py +414 -0
- chronos_mcp/utils.py +163 -0
- chronos_mcp/validation.py +636 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/METADATA +299 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/RECORD +68 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/WHEEL +5 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/entry_points.txt +2 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/licenses/LICENSE +21 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/conftest.py +91 -0
- tests/unit/__init__.py +0 -0
- tests/unit/test_accounts.py +380 -0
- tests/unit/test_accounts_ssrf.py +134 -0
- tests/unit/test_base.py +135 -0
- tests/unit/test_bulk.py +380 -0
- tests/unit/test_bulk_create.py +408 -0
- tests/unit/test_bulk_delete.py +341 -0
- tests/unit/test_bulk_resource_limits.py +74 -0
- tests/unit/test_caldav_utils.py +300 -0
- tests/unit/test_calendars.py +286 -0
- tests/unit/test_config.py +111 -0
- tests/unit/test_config_validation.py +128 -0
- tests/unit/test_credentials_security.py +189 -0
- tests/unit/test_cryptography_security.py +178 -0
- tests/unit/test_events.py +536 -0
- tests/unit/test_exceptions.py +58 -0
- tests/unit/test_journals.py +1097 -0
- tests/unit/test_models.py +95 -0
- tests/unit/test_race_conditions.py +202 -0
- tests/unit/test_recurring_events.py +156 -0
- tests/unit/test_rrule.py +217 -0
- tests/unit/test_search.py +372 -0
- tests/unit/test_search_advanced.py +333 -0
- tests/unit/test_server_input_validation.py +219 -0
- tests/unit/test_ssrf_protection.py +505 -0
- tests/unit/test_tasks.py +918 -0
- tests/unit/test_thread_safety.py +301 -0
- tests/unit/test_tools_journals.py +617 -0
- tests/unit/test_tools_tasks.py +968 -0
- tests/unit/test_url_validation_security.py +234 -0
- tests/unit/test_utils.py +180 -0
- tests/unit/test_validation.py +983 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Chronos MCP Exception Hierarchy
|
|
3
|
+
|
|
4
|
+
This module provides a comprehensive error handling framework for Chronos MCP,
|
|
5
|
+
with custom exceptions for different error scenarios and utilities for
|
|
6
|
+
consistent error handling across the application.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import functools
|
|
10
|
+
import logging
|
|
11
|
+
import re
|
|
12
|
+
import traceback
|
|
13
|
+
import uuid
|
|
14
|
+
from contextlib import contextmanager
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from typing import Any, Callable, Dict, Optional, TypeVar, Union
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Base Exception
|
|
22
|
+
class ChronosError(Exception):
|
|
23
|
+
"""
|
|
24
|
+
Base exception for all Chronos errors.
|
|
25
|
+
|
|
26
|
+
Provides structured error information including:
|
|
27
|
+
- Unique request ID for tracing
|
|
28
|
+
- Error code for categorization
|
|
29
|
+
- Detailed context information
|
|
30
|
+
- Timestamp for debugging
|
|
31
|
+
- Full traceback capture
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
message: str,
|
|
37
|
+
error_code: Optional[str] = None,
|
|
38
|
+
details: Optional[Dict[str, Any]] = None,
|
|
39
|
+
request_id: Optional[str] = None,
|
|
40
|
+
):
|
|
41
|
+
super().__init__(message)
|
|
42
|
+
self.message = message
|
|
43
|
+
self.error_code = error_code or self.__class__.__name__
|
|
44
|
+
self.details = details or {}
|
|
45
|
+
self.request_id = request_id or str(uuid.uuid4())
|
|
46
|
+
self.timestamp = datetime.now(timezone.utc).isoformat()
|
|
47
|
+
self.traceback = traceback.format_exc()
|
|
48
|
+
|
|
49
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
50
|
+
"""Convert to dictionary for logging/API responses"""
|
|
51
|
+
return {
|
|
52
|
+
"error": self.error_code,
|
|
53
|
+
"message": self.message,
|
|
54
|
+
"details": self.details,
|
|
55
|
+
"request_id": self.request_id,
|
|
56
|
+
"timestamp": self.timestamp,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
def __str__(self):
|
|
60
|
+
return f"{self.error_code}: {self.message} (request_id={self.request_id})"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# Configuration Errors
|
|
64
|
+
class ConfigurationError(ChronosError):
|
|
65
|
+
"""Raised when configuration is invalid or missing"""
|
|
66
|
+
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class AccountNotFoundError(ConfigurationError):
|
|
71
|
+
"""Raised when an account is not found in configuration"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, alias: str, **kwargs):
|
|
74
|
+
super().__init__(
|
|
75
|
+
f"Account '{alias}' not found in configuration",
|
|
76
|
+
details={"alias": alias},
|
|
77
|
+
**kwargs,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class InvalidConfigError(ConfigurationError):
|
|
82
|
+
"""Raised when configuration file is invalid"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, reason: str, config_path: Optional[str] = None, **kwargs):
|
|
85
|
+
details = {"reason": reason}
|
|
86
|
+
if config_path:
|
|
87
|
+
details["config_path"] = config_path
|
|
88
|
+
|
|
89
|
+
super().__init__(f"Invalid configuration: {reason}", details=details, **kwargs)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# Account Management Errors
|
|
93
|
+
class AccountError(ChronosError):
|
|
94
|
+
"""Base class for account-related errors"""
|
|
95
|
+
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class AccountConnectionError(AccountError):
|
|
100
|
+
"""Raised when connection to CalDAV account fails"""
|
|
101
|
+
|
|
102
|
+
def __init__(
|
|
103
|
+
self, alias: str, original_error: Optional[Exception] = None, **kwargs
|
|
104
|
+
):
|
|
105
|
+
details = {"alias": alias}
|
|
106
|
+
if original_error:
|
|
107
|
+
details["original_error"] = str(original_error)
|
|
108
|
+
details["original_type"] = type(original_error).__name__
|
|
109
|
+
|
|
110
|
+
super().__init__(
|
|
111
|
+
f"Failed to connect to account '{alias}'", details=details, **kwargs
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class AccountAuthenticationError(AccountError):
|
|
116
|
+
"""Raised when authentication fails"""
|
|
117
|
+
|
|
118
|
+
def __init__(self, alias: str, **kwargs):
|
|
119
|
+
super().__init__(
|
|
120
|
+
f"Authentication failed for account '{alias}'",
|
|
121
|
+
error_code="AUTH_FAILED",
|
|
122
|
+
details={"alias": alias},
|
|
123
|
+
**kwargs,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class AccountAlreadyExistsError(AccountError):
|
|
128
|
+
"""Raised when trying to add an account that already exists"""
|
|
129
|
+
|
|
130
|
+
def __init__(self, alias: str, **kwargs):
|
|
131
|
+
super().__init__(
|
|
132
|
+
f"Account '{alias}' already exists",
|
|
133
|
+
error_code="ACCOUNT_EXISTS",
|
|
134
|
+
details={"alias": alias},
|
|
135
|
+
**kwargs,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# CalDAV Operation Errors
|
|
140
|
+
class CalDAVError(ChronosError):
|
|
141
|
+
"""Base class for CalDAV operation errors"""
|
|
142
|
+
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class CalendarNotFoundError(CalDAVError):
|
|
147
|
+
"""Raised when a calendar is not found"""
|
|
148
|
+
|
|
149
|
+
def __init__(self, calendar_uid: str, account: Optional[str] = None, **kwargs):
|
|
150
|
+
details = {"calendar_uid": calendar_uid}
|
|
151
|
+
if account:
|
|
152
|
+
details["account"] = account
|
|
153
|
+
|
|
154
|
+
super().__init__(
|
|
155
|
+
f"Calendar '{calendar_uid}' not found", details=details, **kwargs
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class CalendarCreationError(CalDAVError):
|
|
160
|
+
"""Raised when calendar creation fails"""
|
|
161
|
+
|
|
162
|
+
def __init__(self, name: str, reason: Optional[str] = None, **kwargs):
|
|
163
|
+
message = f"Failed to create calendar '{name}'"
|
|
164
|
+
if reason:
|
|
165
|
+
message += f": {reason}"
|
|
166
|
+
|
|
167
|
+
super().__init__(
|
|
168
|
+
message, details={"calendar_name": name, "reason": reason}, **kwargs
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class CalendarDeletionError(CalDAVError):
|
|
173
|
+
"""Raised when calendar deletion fails"""
|
|
174
|
+
|
|
175
|
+
def __init__(self, calendar_uid: str, reason: Optional[str] = None, **kwargs):
|
|
176
|
+
message = f"Failed to delete calendar '{calendar_uid}'"
|
|
177
|
+
if reason:
|
|
178
|
+
message += f": {reason}"
|
|
179
|
+
|
|
180
|
+
super().__init__(
|
|
181
|
+
message, details={"calendar_uid": calendar_uid, "reason": reason}, **kwargs
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class EventNotFoundError(CalDAVError):
|
|
186
|
+
"""Raised when an event is not found"""
|
|
187
|
+
|
|
188
|
+
def __init__(self, event_uid: str, calendar_uid: str, **kwargs):
|
|
189
|
+
super().__init__(
|
|
190
|
+
f"Event '{event_uid}' not found in calendar '{calendar_uid}'",
|
|
191
|
+
details={"event_uid": event_uid, "calendar_uid": calendar_uid},
|
|
192
|
+
**kwargs,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class TaskNotFoundError(CalDAVError):
|
|
197
|
+
"""Raised when a task is not found"""
|
|
198
|
+
|
|
199
|
+
def __init__(self, task_uid: str, calendar_uid: str, **kwargs):
|
|
200
|
+
super().__init__(
|
|
201
|
+
f"Task '{task_uid}' not found in calendar '{calendar_uid}'",
|
|
202
|
+
details={"task_uid": task_uid, "calendar_uid": calendar_uid},
|
|
203
|
+
**kwargs,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class JournalNotFoundError(CalDAVError):
|
|
208
|
+
"""Raised when a journal entry is not found"""
|
|
209
|
+
|
|
210
|
+
def __init__(self, journal_uid: str, calendar_uid: str, **kwargs):
|
|
211
|
+
super().__init__(
|
|
212
|
+
f"Journal '{journal_uid}' not found in calendar '{calendar_uid}'",
|
|
213
|
+
details={"journal_uid": journal_uid, "calendar_uid": calendar_uid},
|
|
214
|
+
**kwargs,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class EventCreationError(CalDAVError):
|
|
219
|
+
"""Raised when event creation fails"""
|
|
220
|
+
|
|
221
|
+
def __init__(self, summary: str, reason: Optional[str] = None, **kwargs):
|
|
222
|
+
message = f"Failed to create event '{summary}'"
|
|
223
|
+
if reason:
|
|
224
|
+
message += f": {reason}"
|
|
225
|
+
|
|
226
|
+
super().__init__(
|
|
227
|
+
message, details={"event_summary": summary, "reason": reason}, **kwargs
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class EventDeletionError(CalDAVError):
|
|
232
|
+
"""Raised when event deletion fails"""
|
|
233
|
+
|
|
234
|
+
def __init__(self, event_uid: str, reason: Optional[str] = None, **kwargs):
|
|
235
|
+
message = f"Failed to delete event '{event_uid}'"
|
|
236
|
+
if reason:
|
|
237
|
+
message += f": {reason}"
|
|
238
|
+
|
|
239
|
+
super().__init__(
|
|
240
|
+
message, details={"event_uid": event_uid, "reason": reason}, **kwargs
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# Validation Errors
|
|
245
|
+
class ValidationError(ChronosError):
|
|
246
|
+
"""Base class for validation errors"""
|
|
247
|
+
|
|
248
|
+
pass
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class DateTimeValidationError(ValidationError):
|
|
252
|
+
"""Raised when datetime parsing/validation fails"""
|
|
253
|
+
|
|
254
|
+
def __init__(self, value: str, expected_format: Optional[str] = None, **kwargs):
|
|
255
|
+
message = f"Invalid datetime format: '{value}'"
|
|
256
|
+
if expected_format:
|
|
257
|
+
message += f" (expected: {expected_format})"
|
|
258
|
+
|
|
259
|
+
super().__init__(
|
|
260
|
+
message,
|
|
261
|
+
error_code="INVALID_DATETIME",
|
|
262
|
+
details={"value": value, "expected_format": expected_format},
|
|
263
|
+
**kwargs,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class RecurrenceRuleValidationError(ValidationError):
|
|
268
|
+
"""Raised when RRULE validation fails"""
|
|
269
|
+
|
|
270
|
+
def __init__(self, rrule: str, reason: str, **kwargs):
|
|
271
|
+
super().__init__(
|
|
272
|
+
f"Invalid recurrence rule: {reason}",
|
|
273
|
+
error_code="INVALID_RRULE",
|
|
274
|
+
details={"rrule": rrule, "reason": reason},
|
|
275
|
+
**kwargs,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class AttendeeValidationError(ValidationError):
|
|
280
|
+
"""Raised when attendee data validation fails"""
|
|
281
|
+
|
|
282
|
+
def __init__(self, attendee_data: Any, reason: str, **kwargs):
|
|
283
|
+
super().__init__(
|
|
284
|
+
f"Invalid attendee data: {reason}",
|
|
285
|
+
error_code="INVALID_ATTENDEE",
|
|
286
|
+
details={"attendee_data": str(attendee_data), "reason": reason},
|
|
287
|
+
**kwargs,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# Error Handling Utilities
|
|
292
|
+
class ErrorHandler:
|
|
293
|
+
"""Utility class for consistent error handling"""
|
|
294
|
+
|
|
295
|
+
@staticmethod
|
|
296
|
+
def safe_operation(
|
|
297
|
+
logger: logging.Logger,
|
|
298
|
+
default_return: Any = None,
|
|
299
|
+
error_message: Optional[str] = None,
|
|
300
|
+
raise_on_error: bool = False,
|
|
301
|
+
):
|
|
302
|
+
"""
|
|
303
|
+
Decorator for safe operations that follow the None/False pattern.
|
|
304
|
+
|
|
305
|
+
This decorator catches exceptions and handles them according to
|
|
306
|
+
the Chronos error handling strategy:
|
|
307
|
+
- Log detailed error information
|
|
308
|
+
- Return a default value (None/False)
|
|
309
|
+
- Optionally re-raise for specific scenarios
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
logger: Logger instance for error logging
|
|
313
|
+
default_return: Value to return on error (default: None)
|
|
314
|
+
error_message: Custom error message format
|
|
315
|
+
raise_on_error: Whether to re-raise exceptions
|
|
316
|
+
|
|
317
|
+
Usage:
|
|
318
|
+
@ErrorHandler.safe_operation(logger, default_return=False)
|
|
319
|
+
def connect_account(self, alias: str) -> bool:
|
|
320
|
+
# implementation that may raise exceptions
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
def decorator(func: Callable[..., T]) -> Callable[..., Union[T, Any]]:
|
|
324
|
+
@functools.wraps(func)
|
|
325
|
+
def wrapper(*args, **kwargs):
|
|
326
|
+
request_id = kwargs.get("request_id", str(uuid.uuid4()))
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
# Only add request_id if the function accepts it
|
|
330
|
+
import inspect
|
|
331
|
+
|
|
332
|
+
sig = inspect.signature(func)
|
|
333
|
+
if "request_id" in sig.parameters and "request_id" not in kwargs:
|
|
334
|
+
kwargs["request_id"] = request_id
|
|
335
|
+
|
|
336
|
+
return func(*args, **kwargs)
|
|
337
|
+
|
|
338
|
+
except ChronosError as e:
|
|
339
|
+
# Already a Chronos error, just log and handle
|
|
340
|
+
e.request_id = request_id
|
|
341
|
+
logger.error(f"{e} | Details: {e.details}")
|
|
342
|
+
|
|
343
|
+
if raise_on_error:
|
|
344
|
+
raise
|
|
345
|
+
return default_return
|
|
346
|
+
|
|
347
|
+
except Exception as e:
|
|
348
|
+
# Wrap in ChronosError
|
|
349
|
+
chronos_error = ChronosError(
|
|
350
|
+
message=error_message or f"Operation failed: {str(e)}",
|
|
351
|
+
details={
|
|
352
|
+
"function": func.__name__,
|
|
353
|
+
"original_error": str(e),
|
|
354
|
+
"original_type": type(e).__name__,
|
|
355
|
+
},
|
|
356
|
+
request_id=request_id,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
logger.error(f"{chronos_error} | Stack: {chronos_error.traceback}")
|
|
360
|
+
|
|
361
|
+
if raise_on_error:
|
|
362
|
+
raise chronos_error
|
|
363
|
+
return default_return
|
|
364
|
+
|
|
365
|
+
return wrapper
|
|
366
|
+
|
|
367
|
+
return decorator
|
|
368
|
+
|
|
369
|
+
@staticmethod
|
|
370
|
+
@contextmanager
|
|
371
|
+
def error_context(
|
|
372
|
+
logger: logging.Logger,
|
|
373
|
+
operation: str,
|
|
374
|
+
request_id: Optional[str] = None,
|
|
375
|
+
raise_on_error: bool = False,
|
|
376
|
+
):
|
|
377
|
+
"""
|
|
378
|
+
Context manager for error handling.
|
|
379
|
+
|
|
380
|
+
Provides consistent error handling and logging for code blocks.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
logger: Logger instance
|
|
384
|
+
operation: Description of the operation
|
|
385
|
+
request_id: Optional request ID for tracing
|
|
386
|
+
raise_on_error: Whether to re-raise exceptions
|
|
387
|
+
|
|
388
|
+
Usage:
|
|
389
|
+
with ErrorHandler.error_context(logger, "connect_account"):
|
|
390
|
+
# operation code that may raise exceptions
|
|
391
|
+
"""
|
|
392
|
+
request_id = request_id or str(uuid.uuid4())
|
|
393
|
+
logger.debug(f"Starting {operation} (request_id={request_id})")
|
|
394
|
+
|
|
395
|
+
try:
|
|
396
|
+
yield request_id
|
|
397
|
+
logger.debug(f"Completed {operation} (request_id={request_id})")
|
|
398
|
+
|
|
399
|
+
except ChronosError as e:
|
|
400
|
+
e.request_id = request_id
|
|
401
|
+
logger.error(f"{operation} failed: {e}")
|
|
402
|
+
if raise_on_error:
|
|
403
|
+
raise
|
|
404
|
+
|
|
405
|
+
except Exception as e:
|
|
406
|
+
chronos_error = ChronosError(
|
|
407
|
+
message=f"{operation} failed: {str(e)}",
|
|
408
|
+
details={
|
|
409
|
+
"operation": operation,
|
|
410
|
+
"original_error": str(e),
|
|
411
|
+
"original_type": type(e).__name__,
|
|
412
|
+
},
|
|
413
|
+
request_id=request_id,
|
|
414
|
+
)
|
|
415
|
+
logger.error(f"{chronos_error}")
|
|
416
|
+
if raise_on_error:
|
|
417
|
+
raise chronos_error
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
class ErrorSanitizer:
|
|
421
|
+
"""Sanitize error messages for user consumption"""
|
|
422
|
+
|
|
423
|
+
# Patterns to redact sensitive information
|
|
424
|
+
SENSITIVE_PATTERNS = [
|
|
425
|
+
(r'password\s*[=:]\s*["\']?[\w\-\.@#$%^&*!]+["\']?', "password=***"),
|
|
426
|
+
(r'token\s*[=:]\s*["\']?[\w\-\.]+["\']?', "token=***"),
|
|
427
|
+
(r"https?://[^:]+:[^@]+@", "https://***:***@"),
|
|
428
|
+
(r"Authorization:\s*[\w]+\s+[\w\-\.=]+", "Authorization: ***"),
|
|
429
|
+
(r"Bearer\s+[\w\-\.=]+", "Bearer ***"),
|
|
430
|
+
(r'api[_-]?key\s*[=:]\s*["\']?[\w\-\.]+["\']?', "api_key=***"),
|
|
431
|
+
(r'secret\s*[=:]\s*["\']?[\w\-\.]+["\']?', "secret=***"),
|
|
432
|
+
]
|
|
433
|
+
|
|
434
|
+
@classmethod
|
|
435
|
+
def sanitize_message(cls, message: str) -> str:
|
|
436
|
+
"""Remove sensitive information from error messages"""
|
|
437
|
+
sanitized = message
|
|
438
|
+
for pattern, replacement in cls.SENSITIVE_PATTERNS:
|
|
439
|
+
sanitized = re.sub(pattern, replacement, sanitized, flags=re.IGNORECASE)
|
|
440
|
+
|
|
441
|
+
return sanitized
|
|
442
|
+
|
|
443
|
+
@classmethod
|
|
444
|
+
def sanitize_error(cls, error: ChronosError) -> Dict[str, Any]:
|
|
445
|
+
"""Create sanitized error dict for API responses"""
|
|
446
|
+
return {
|
|
447
|
+
"error": error.error_code,
|
|
448
|
+
"message": cls.sanitize_message(error.message),
|
|
449
|
+
"request_id": error.request_id,
|
|
450
|
+
# Don't include details or traceback in user responses
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
@classmethod
|
|
454
|
+
def get_user_friendly_message(cls, error: ChronosError) -> str:
|
|
455
|
+
"""Get user-friendly error message"""
|
|
456
|
+
# Map error codes to friendly messages
|
|
457
|
+
friendly_messages = {
|
|
458
|
+
"AUTH_FAILED": "Authentication failed. Please check your credentials.",
|
|
459
|
+
"INVALID_DATETIME": "Invalid date/time format. Please use ISO format (YYYY-MM-DD HH:MM:SS).",
|
|
460
|
+
"INVALID_RRULE": "Invalid recurrence rule format.",
|
|
461
|
+
"INVALID_ATTENDEE": "Invalid attendee information provided.",
|
|
462
|
+
"ACCOUNT_EXISTS": "An account with this name already exists.",
|
|
463
|
+
"AccountNotFoundError": "The specified account was not found.",
|
|
464
|
+
"CalendarNotFoundError": "The specified calendar was not found.",
|
|
465
|
+
"EventNotFoundError": "The specified event was not found.",
|
|
466
|
+
"TaskNotFoundError": "The specified task was not found.",
|
|
467
|
+
"JournalNotFoundError": "The specified journal entry was not found.",
|
|
468
|
+
"AccountConnectionError": "Could not connect to the calendar server. Please check the server URL.",
|
|
469
|
+
"CalendarCreationError": "Could not create the calendar. It may already exist.",
|
|
470
|
+
"EventCreationError": "Could not create the event. Please check all required fields.",
|
|
471
|
+
"InvalidConfigError": "The configuration file is invalid or corrupted.",
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return friendly_messages.get(
|
|
475
|
+
error.error_code,
|
|
476
|
+
f"An error occurred: {cls.sanitize_message(error.message)}",
|
|
477
|
+
)
|