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,310 @@
1
+ """
2
+ Journal management tools for Chronos MCP
3
+ """
4
+
5
+ import uuid
6
+ from typing import Any, Dict, List, Optional, Union
7
+
8
+ from pydantic import Field
9
+
10
+ from ..exceptions import (
11
+ CalendarNotFoundError,
12
+ ChronosError,
13
+ ErrorSanitizer,
14
+ EventNotFoundError,
15
+ ValidationError,
16
+ )
17
+ from ..logging_config import setup_logging
18
+ from ..utils import parse_datetime
19
+ from ..validation import InputValidator
20
+ from .base import create_success_response, handle_tool_errors
21
+
22
+ logger = setup_logging()
23
+
24
+ # Module-level managers dictionary for dependency injection
25
+ _managers = {}
26
+
27
+
28
+ # Journal tool functions - defined as standalone functions for importability
29
+ async def create_journal(
30
+ calendar_uid: str = Field(..., description="Calendar UID"),
31
+ summary: str = Field(..., description="Journal entry title/summary"),
32
+ description: Optional[str] = Field(None, description="Journal entry content"),
33
+ entry_date: Optional[str] = Field(
34
+ None, description="Journal entry date (ISO format)"
35
+ ),
36
+ related_to: Optional[List[str]] = Field(
37
+ None, description="List of related component UIDs"
38
+ ),
39
+ account: Optional[str] = Field(None, description="Account alias"),
40
+ ) -> Dict[str, Any]:
41
+ """Create a new journal entry"""
42
+ request_id = str(uuid.uuid4())
43
+
44
+ try:
45
+ # Validate and sanitize text inputs
46
+ try:
47
+ summary = InputValidator.validate_text_field(
48
+ summary, "summary", required=True
49
+ )
50
+ if description:
51
+ description = InputValidator.validate_text_field(
52
+ description, "description"
53
+ )
54
+ except ValidationError as e:
55
+ return {
56
+ "success": False,
57
+ "error": str(e),
58
+ "error_code": "VALIDATION_ERROR",
59
+ "request_id": request_id,
60
+ }
61
+
62
+ # Parse entry date if provided
63
+ entry_dt = None
64
+ if entry_date:
65
+ entry_dt = parse_datetime(entry_date)
66
+
67
+ journal = _managers["journal_manager"].create_journal(
68
+ calendar_uid=calendar_uid,
69
+ summary=summary,
70
+ description=description,
71
+ dtstart=entry_dt,
72
+ related_to=related_to,
73
+ account_alias=account,
74
+ request_id=request_id,
75
+ )
76
+
77
+ return {
78
+ "success": True,
79
+ "journal": {
80
+ "uid": journal.uid,
81
+ "summary": journal.summary,
82
+ "description": journal.description,
83
+ "entry_date": (
84
+ journal.dtstart.isoformat() if journal.dtstart else None
85
+ ),
86
+ "related_to": journal.related_to,
87
+ },
88
+ "request_id": request_id,
89
+ }
90
+
91
+ except ChronosError as e:
92
+ e.request_id = request_id
93
+ logger.error(f"Create journal failed: {e}")
94
+ return {
95
+ "success": False,
96
+ "error": ErrorSanitizer.get_user_friendly_message(e),
97
+ "error_code": e.error_code,
98
+ "request_id": request_id,
99
+ }
100
+
101
+ except Exception as e:
102
+ chronos_error = ChronosError(
103
+ message=f"Failed to create journal: {str(e)}",
104
+ details={
105
+ "tool": "create_journal",
106
+ "summary": summary,
107
+ "calendar_uid": calendar_uid,
108
+ "original_error": str(e),
109
+ "original_type": type(e).__name__,
110
+ },
111
+ request_id=request_id,
112
+ )
113
+ logger.error(f"Unexpected error in create_journal: {chronos_error}")
114
+ return {
115
+ "success": False,
116
+ "error": ErrorSanitizer.get_user_friendly_message(chronos_error),
117
+ "error_code": chronos_error.error_code,
118
+ "request_id": request_id,
119
+ }
120
+
121
+
122
+ async def list_journals(
123
+ calendar_uid: str = Field(..., description="Calendar UID"),
124
+ account: Optional[str] = Field(None, description="Account alias"),
125
+ limit: Optional[Union[int, str]] = Field(
126
+ 50, description="Maximum number of journals to return"
127
+ ),
128
+ ) -> Dict[str, Any]:
129
+ """List journal entries in a calendar"""
130
+ request_id = str(uuid.uuid4())
131
+
132
+ # Handle type conversion for limit parameter
133
+ if limit is not None:
134
+ try:
135
+ limit = int(limit)
136
+ except (ValueError, TypeError):
137
+ return {
138
+ "journals": [],
139
+ "total": 0,
140
+ "error": f"Invalid limit value: {limit}. Must be an integer",
141
+ "error_code": "VALIDATION_ERROR",
142
+ "request_id": request_id,
143
+ }
144
+
145
+ try:
146
+ journals = _managers["journal_manager"].list_journals(
147
+ calendar_uid=calendar_uid,
148
+ limit=limit,
149
+ account_alias=account,
150
+ )
151
+
152
+ return {
153
+ "journals": [
154
+ {
155
+ "uid": journal.uid,
156
+ "summary": journal.summary,
157
+ "description": journal.description,
158
+ "entry_date": (
159
+ journal.dtstart.isoformat() if journal.dtstart else None
160
+ ),
161
+ "related_to": journal.related_to,
162
+ }
163
+ for journal in journals
164
+ ],
165
+ "total": len(journals),
166
+ "calendar_uid": calendar_uid,
167
+ "request_id": request_id,
168
+ }
169
+
170
+ except CalendarNotFoundError as e:
171
+ e.request_id = request_id
172
+ logger.error(f"Calendar not found for journal listing: {e}")
173
+ return {
174
+ "journals": [],
175
+ "total": 0,
176
+ "error": ErrorSanitizer.get_user_friendly_message(e),
177
+ "error_code": e.error_code,
178
+ "request_id": request_id,
179
+ }
180
+
181
+ except ChronosError as e:
182
+ e.request_id = request_id
183
+ logger.error(f"List journals failed: {e}")
184
+ return {
185
+ "journals": [],
186
+ "total": 0,
187
+ "error": ErrorSanitizer.get_user_friendly_message(e),
188
+ "error_code": e.error_code,
189
+ "request_id": request_id,
190
+ }
191
+
192
+ except Exception as e:
193
+ chronos_error = ChronosError(
194
+ message=f"Failed to list journals: {str(e)}",
195
+ details={
196
+ "tool": "list_journals",
197
+ "calendar_uid": calendar_uid,
198
+ "original_error": str(e),
199
+ "original_type": type(e).__name__,
200
+ },
201
+ request_id=request_id,
202
+ )
203
+ logger.error(f"Unexpected error in list_journals: {chronos_error}")
204
+ return {
205
+ "journals": [],
206
+ "total": 0,
207
+ "error": ErrorSanitizer.get_user_friendly_message(chronos_error),
208
+ "error_code": chronos_error.error_code,
209
+ "request_id": request_id,
210
+ }
211
+
212
+
213
+ @handle_tool_errors
214
+ async def update_journal(
215
+ calendar_uid: str = Field(..., description="Calendar UID"),
216
+ journal_uid: str = Field(..., description="Journal UID to update"),
217
+ summary: Optional[str] = Field(None, description="Journal entry title/summary"),
218
+ description: Optional[str] = Field(None, description="Journal entry content"),
219
+ entry_date: Optional[str] = Field(
220
+ None, description="Journal entry date (ISO format)"
221
+ ),
222
+ account: Optional[str] = Field(None, description="Account alias"),
223
+ request_id: str = None,
224
+ ) -> Dict[str, Any]:
225
+ """Update an existing journal entry. Only provided fields will be updated."""
226
+ # Validate inputs
227
+ if summary is not None:
228
+ summary = InputValidator.validate_text_field(summary, "summary", required=True)
229
+ if description is not None:
230
+ description = InputValidator.validate_text_field(description, "description")
231
+
232
+ # Parse entry date if provided
233
+ entry_dt = None
234
+ if entry_date is not None:
235
+ entry_dt = parse_datetime(entry_date)
236
+
237
+ updated_journal = _managers["journal_manager"].update_journal(
238
+ calendar_uid=calendar_uid,
239
+ journal_uid=journal_uid,
240
+ summary=summary,
241
+ description=description,
242
+ dtstart=entry_dt,
243
+ account_alias=account,
244
+ request_id=request_id,
245
+ )
246
+
247
+ return create_success_response(
248
+ message=f"Journal '{journal_uid}' updated successfully",
249
+ request_id=request_id,
250
+ journal={
251
+ "uid": updated_journal.uid,
252
+ "summary": updated_journal.summary,
253
+ "description": updated_journal.description,
254
+ "entry_date": (
255
+ updated_journal.dtstart.isoformat() if updated_journal.dtstart else None
256
+ ),
257
+ "related_to": updated_journal.related_to,
258
+ },
259
+ )
260
+
261
+
262
+ @handle_tool_errors
263
+ async def delete_journal(
264
+ calendar_uid: str = Field(..., description="Calendar UID"),
265
+ journal_uid: str = Field(..., description="Journal UID to delete"),
266
+ account: Optional[str] = Field(None, description="Account alias"),
267
+ request_id: str = None,
268
+ ) -> Dict[str, Any]:
269
+ """Delete a journal entry"""
270
+ _managers["journal_manager"].delete_journal(
271
+ calendar_uid=calendar_uid,
272
+ journal_uid=journal_uid,
273
+ account_alias=account,
274
+ request_id=request_id,
275
+ )
276
+
277
+ return create_success_response(
278
+ message=f"Journal '{journal_uid}' deleted successfully",
279
+ request_id=request_id,
280
+ )
281
+
282
+
283
+ def register_journal_tools(mcp, managers):
284
+ """Register journal management tools with the MCP server"""
285
+
286
+ # Update module-level managers for dependency injection
287
+ _managers.update(managers)
288
+
289
+ # Register all journal tools with the MCP server
290
+ mcp.tool(create_journal)
291
+ mcp.tool(list_journals)
292
+ mcp.tool(update_journal)
293
+ mcp.tool(delete_journal)
294
+
295
+
296
+ # Add .fn attribute to each function for backwards compatibility with tests
297
+ create_journal.fn = create_journal
298
+ list_journals.fn = list_journals
299
+ update_journal.fn = update_journal
300
+ delete_journal.fn = delete_journal
301
+
302
+
303
+ # Export all tools for backwards compatibility
304
+ __all__ = [
305
+ "create_journal",
306
+ "list_journals",
307
+ "update_journal",
308
+ "delete_journal",
309
+ "register_journal_tools",
310
+ ]