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.
Files changed (68) hide show
  1. chronos_mcp/__init__.py +5 -0
  2. chronos_mcp/__main__.py +9 -0
  3. chronos_mcp/accounts.py +410 -0
  4. chronos_mcp/bulk.py +946 -0
  5. chronos_mcp/caldav_utils.py +149 -0
  6. chronos_mcp/calendars.py +204 -0
  7. chronos_mcp/config.py +187 -0
  8. chronos_mcp/credentials.py +190 -0
  9. chronos_mcp/events.py +515 -0
  10. chronos_mcp/exceptions.py +477 -0
  11. chronos_mcp/journals.py +477 -0
  12. chronos_mcp/logging_config.py +23 -0
  13. chronos_mcp/models.py +202 -0
  14. chronos_mcp/py.typed +0 -0
  15. chronos_mcp/rrule.py +259 -0
  16. chronos_mcp/search.py +315 -0
  17. chronos_mcp/server.py +121 -0
  18. chronos_mcp/tasks.py +518 -0
  19. chronos_mcp/tools/__init__.py +29 -0
  20. chronos_mcp/tools/accounts.py +151 -0
  21. chronos_mcp/tools/base.py +59 -0
  22. chronos_mcp/tools/bulk.py +557 -0
  23. chronos_mcp/tools/calendars.py +142 -0
  24. chronos_mcp/tools/events.py +698 -0
  25. chronos_mcp/tools/journals.py +310 -0
  26. chronos_mcp/tools/tasks.py +414 -0
  27. chronos_mcp/utils.py +163 -0
  28. chronos_mcp/validation.py +636 -0
  29. iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/METADATA +299 -0
  30. iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/RECORD +68 -0
  31. iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/WHEEL +5 -0
  32. iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/entry_points.txt +2 -0
  33. iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/licenses/LICENSE +21 -0
  34. iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/top_level.txt +2 -0
  35. tests/__init__.py +0 -0
  36. tests/conftest.py +91 -0
  37. tests/unit/__init__.py +0 -0
  38. tests/unit/test_accounts.py +380 -0
  39. tests/unit/test_accounts_ssrf.py +134 -0
  40. tests/unit/test_base.py +135 -0
  41. tests/unit/test_bulk.py +380 -0
  42. tests/unit/test_bulk_create.py +408 -0
  43. tests/unit/test_bulk_delete.py +341 -0
  44. tests/unit/test_bulk_resource_limits.py +74 -0
  45. tests/unit/test_caldav_utils.py +300 -0
  46. tests/unit/test_calendars.py +286 -0
  47. tests/unit/test_config.py +111 -0
  48. tests/unit/test_config_validation.py +128 -0
  49. tests/unit/test_credentials_security.py +189 -0
  50. tests/unit/test_cryptography_security.py +178 -0
  51. tests/unit/test_events.py +536 -0
  52. tests/unit/test_exceptions.py +58 -0
  53. tests/unit/test_journals.py +1097 -0
  54. tests/unit/test_models.py +95 -0
  55. tests/unit/test_race_conditions.py +202 -0
  56. tests/unit/test_recurring_events.py +156 -0
  57. tests/unit/test_rrule.py +217 -0
  58. tests/unit/test_search.py +372 -0
  59. tests/unit/test_search_advanced.py +333 -0
  60. tests/unit/test_server_input_validation.py +219 -0
  61. tests/unit/test_ssrf_protection.py +505 -0
  62. tests/unit/test_tasks.py +918 -0
  63. tests/unit/test_thread_safety.py +301 -0
  64. tests/unit/test_tools_journals.py +617 -0
  65. tests/unit/test_tools_tasks.py +968 -0
  66. tests/unit/test_url_validation_security.py +234 -0
  67. tests/unit/test_utils.py +180 -0
  68. 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
+ )