@mseep/csv-editor 1.0.0

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 (106) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +53 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +38 -0
  3. package/.github/workflows/deploy-docs.yml +62 -0
  4. package/.github/workflows/publish-github.yml +52 -0
  5. package/.github/workflows/publish.yml +44 -0
  6. package/.github/workflows/test.yml +32 -0
  7. package/.pre-commit-config.yaml +157 -0
  8. package/ALTERNATIVE_PUBLISHING.md +175 -0
  9. package/ARCHITECTURE.md +1011 -0
  10. package/CHANGELOG.md +99 -0
  11. package/CODE_OF_CONDUCT.md +41 -0
  12. package/CONTRIBUTING.md +427 -0
  13. package/Dockerfile +22 -0
  14. package/LICENSE +21 -0
  15. package/MCP_CONFIG.md +505 -0
  16. package/PUBLISHING.md +210 -0
  17. package/README.md +400 -0
  18. package/SECURITY.md +61 -0
  19. package/docs/README.md +41 -0
  20. package/docs/blog/2019-05-28-first-blog-post.md +12 -0
  21. package/docs/blog/2019-05-29-long-blog-post.md +44 -0
  22. package/docs/blog/2021-08-01-mdx-blog-post.mdx +24 -0
  23. package/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg +0 -0
  24. package/docs/blog/2021-08-26-welcome/index.md +29 -0
  25. package/docs/blog/authors.yml +25 -0
  26. package/docs/blog/tags.yml +19 -0
  27. package/docs/docs/api/overview.md +183 -0
  28. package/docs/docs/installation.md +252 -0
  29. package/docs/docs/intro.md +87 -0
  30. package/docs/docs/tutorial-basics/_category_.json +8 -0
  31. package/docs/docs/tutorial-basics/congratulations.md +23 -0
  32. package/docs/docs/tutorial-basics/create-a-blog-post.md +34 -0
  33. package/docs/docs/tutorial-basics/create-a-document.md +57 -0
  34. package/docs/docs/tutorial-basics/create-a-page.md +43 -0
  35. package/docs/docs/tutorial-basics/deploy-your-site.md +31 -0
  36. package/docs/docs/tutorial-basics/markdown-features.mdx +152 -0
  37. package/docs/docs/tutorial-extras/_category_.json +7 -0
  38. package/docs/docs/tutorial-extras/img/docsVersionDropdown.png +0 -0
  39. package/docs/docs/tutorial-extras/img/localeDropdown.png +0 -0
  40. package/docs/docs/tutorial-extras/manage-docs-versions.md +55 -0
  41. package/docs/docs/tutorial-extras/translate-your-site.md +88 -0
  42. package/docs/docs/tutorials/quickstart.md +365 -0
  43. package/docs/docusaurus.config.ts +163 -0
  44. package/docs/package-lock.json +17493 -0
  45. package/docs/package.json +48 -0
  46. package/docs/sidebars.ts +33 -0
  47. package/docs/src/components/HomepageFeatures/index.tsx +71 -0
  48. package/docs/src/components/HomepageFeatures/styles.module.css +11 -0
  49. package/docs/src/css/custom.css +30 -0
  50. package/docs/src/pages/index.module.css +23 -0
  51. package/docs/src/pages/index.tsx +44 -0
  52. package/docs/src/pages/markdown-page.md +7 -0
  53. package/docs/static/.nojekyll +0 -0
  54. package/docs/static/img/docusaurus-social-card.jpg +0 -0
  55. package/docs/static/img/docusaurus.png +0 -0
  56. package/docs/static/img/favicon.ico +0 -0
  57. package/docs/static/img/logo.svg +1 -0
  58. package/docs/static/img/undraw_docusaurus_mountain.svg +171 -0
  59. package/docs/static/img/undraw_docusaurus_react.svg +170 -0
  60. package/docs/static/img/undraw_docusaurus_tree.svg +40 -0
  61. package/docs/tsconfig.json +8 -0
  62. package/examples/README.md +48 -0
  63. package/examples/auto_save_demo.py +206 -0
  64. package/examples/auto_save_overwrite.py +201 -0
  65. package/examples/basic_usage.py +135 -0
  66. package/examples/demo.py +139 -0
  67. package/examples/history_demo.py +317 -0
  68. package/examples/test_default_autosave.py +124 -0
  69. package/examples/update_consignee_example.py +179 -0
  70. package/package.json +51 -0
  71. package/plans/2026-04-19-fastmcp3-migration-plan.md +1045 -0
  72. package/pyproject.toml +331 -0
  73. package/requirements-dev.txt +30 -0
  74. package/requirements.txt +22 -0
  75. package/scripts/publish.py +67 -0
  76. package/smithery.yaml +15 -0
  77. package/specs/2026-04-19-fastmcp3-migration-design.md +243 -0
  78. package/src/csv_editor/__init__.py +8 -0
  79. package/src/csv_editor/models/__init__.py +39 -0
  80. package/src/csv_editor/models/auto_save.py +246 -0
  81. package/src/csv_editor/models/csv_session.py +468 -0
  82. package/src/csv_editor/models/data_models.py +244 -0
  83. package/src/csv_editor/models/history_manager.py +456 -0
  84. package/src/csv_editor/prompts/__init__.py +0 -0
  85. package/src/csv_editor/prompts/data_prompts.py +13 -0
  86. package/src/csv_editor/resources/__init__.py +0 -0
  87. package/src/csv_editor/resources/csv_resources.py +22 -0
  88. package/src/csv_editor/server.py +640 -0
  89. package/src/csv_editor/tools/__init__.py +5 -0
  90. package/src/csv_editor/tools/analytics.py +700 -0
  91. package/src/csv_editor/tools/auto_save_operations.py +235 -0
  92. package/src/csv_editor/tools/data_operations.py +3 -0
  93. package/src/csv_editor/tools/history_operations.py +315 -0
  94. package/src/csv_editor/tools/io_operations.py +431 -0
  95. package/src/csv_editor/tools/transformations.py +663 -0
  96. package/src/csv_editor/tools/validation.py +822 -0
  97. package/src/csv_editor/utils/__init__.py +0 -0
  98. package/src/csv_editor/utils/validators.py +205 -0
  99. package/tests/README.md +65 -0
  100. package/tests/__init__.py +7 -0
  101. package/tests/conftest.py +50 -0
  102. package/tests/test_auto_save.py +378 -0
  103. package/tests/test_basic.py +103 -0
  104. package/tests/test_integration.py +356 -0
  105. package/tests/test_server_boot.py +50 -0
  106. package/tests/test_settings.py +184 -0
@@ -0,0 +1,235 @@
1
+ """Auto-save operations for CSV sessions."""
2
+
3
+ import logging
4
+ from typing import Any
5
+
6
+ from fastmcp import Context
7
+
8
+ from ..models.csv_session import get_session_manager
9
+ from ..models.data_models import OperationResult
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ async def configure_auto_save(
15
+ session_id: str,
16
+ enabled: bool = True,
17
+ mode: str = "after_operation",
18
+ strategy: str = "backup",
19
+ interval_seconds: int | None = None,
20
+ max_backups: int | None = None,
21
+ backup_dir: str | None = None,
22
+ custom_path: str | None = None,
23
+ format: str = "csv",
24
+ encoding: str = "utf-8",
25
+ ctx: Context = None,
26
+ ) -> dict[str, Any]:
27
+ """
28
+ Configure auto-save settings for a session.
29
+
30
+ Args:
31
+ session_id: Session identifier
32
+ enabled: Whether auto-save is enabled
33
+ mode: Auto-save mode ('disabled', 'after_operation', 'periodic', 'hybrid')
34
+ strategy: Save strategy ('overwrite', 'backup', 'versioned', 'custom')
35
+ interval_seconds: Interval for periodic saves (default 300)
36
+ max_backups: Maximum number of backup files to keep (default 10)
37
+ backup_dir: Directory for backup files
38
+ custom_path: Custom path for saves (when strategy='custom')
39
+ format: Export format ('csv', 'tsv', 'json', 'excel', 'parquet')
40
+ encoding: File encoding (default 'utf-8')
41
+ ctx: FastMCP context
42
+
43
+ Returns:
44
+ Dict with success status and configuration
45
+ """
46
+ try:
47
+ manager = get_session_manager()
48
+ session = manager.get_session(session_id)
49
+
50
+ if not session:
51
+ return OperationResult(
52
+ success=False,
53
+ message="Session not found",
54
+ error=f"No session with ID: {session_id}",
55
+ ).model_dump()
56
+
57
+ if ctx:
58
+ await ctx.info(f"Configuring auto-save for session {session_id}")
59
+
60
+ # Build configuration
61
+ config = {
62
+ "enabled": enabled,
63
+ "mode": mode,
64
+ "strategy": strategy,
65
+ "format": format,
66
+ "encoding": encoding,
67
+ }
68
+
69
+ if interval_seconds is not None:
70
+ config["interval_seconds"] = interval_seconds
71
+ if max_backups is not None:
72
+ config["max_backups"] = max_backups
73
+ if backup_dir is not None:
74
+ config["backup_dir"] = backup_dir
75
+ if custom_path is not None:
76
+ config["custom_path"] = custom_path
77
+
78
+ # Apply configuration
79
+ result = await session.enable_auto_save(config)
80
+
81
+ if result["success"]:
82
+ if ctx:
83
+ await ctx.info(f"Auto-save configured: {mode} mode, {strategy} strategy")
84
+
85
+ return OperationResult(
86
+ success=True,
87
+ message="Auto-save configured successfully",
88
+ session_id=session_id,
89
+ data=result["config"],
90
+ ).model_dump()
91
+ else:
92
+ return OperationResult(
93
+ success=False, message="Failed to configure auto-save", error=result.get("error")
94
+ ).model_dump()
95
+
96
+ except Exception as e:
97
+ logger.error(f"Error configuring auto-save: {e!s}")
98
+ if ctx:
99
+ await ctx.error(f"Failed to configure auto-save: {e!s}")
100
+ return OperationResult(
101
+ success=False, message="Failed to configure auto-save", error=str(e)
102
+ ).model_dump()
103
+
104
+
105
+ async def disable_auto_save(session_id: str, ctx: Context = None) -> dict[str, Any]:
106
+ """
107
+ Disable auto-save for a session.
108
+
109
+ Args:
110
+ session_id: Session identifier
111
+ ctx: FastMCP context
112
+
113
+ Returns:
114
+ Dict with success status
115
+ """
116
+ try:
117
+ manager = get_session_manager()
118
+ session = manager.get_session(session_id)
119
+
120
+ if not session:
121
+ return OperationResult(
122
+ success=False,
123
+ message="Session not found",
124
+ error=f"No session with ID: {session_id}",
125
+ ).model_dump()
126
+
127
+ result = await session.disable_auto_save()
128
+
129
+ if result["success"]:
130
+ if ctx:
131
+ await ctx.info(f"Auto-save disabled for session {session_id}")
132
+
133
+ return OperationResult(
134
+ success=True, message="Auto-save disabled", session_id=session_id
135
+ ).model_dump()
136
+ else:
137
+ return OperationResult(
138
+ success=False, message="Failed to disable auto-save", error=result.get("error")
139
+ ).model_dump()
140
+
141
+ except Exception as e:
142
+ logger.error(f"Error disabling auto-save: {e!s}")
143
+ if ctx:
144
+ await ctx.error(f"Failed to disable auto-save: {e!s}")
145
+ return OperationResult(
146
+ success=False, message="Failed to disable auto-save", error=str(e)
147
+ ).model_dump()
148
+
149
+
150
+ async def get_auto_save_status(session_id: str, ctx: Context = None) -> dict[str, Any]:
151
+ """
152
+ Get auto-save status for a session.
153
+
154
+ Args:
155
+ session_id: Session identifier
156
+ ctx: FastMCP context
157
+
158
+ Returns:
159
+ Dict with auto-save status
160
+ """
161
+ try:
162
+ manager = get_session_manager()
163
+ session = manager.get_session(session_id)
164
+
165
+ if not session:
166
+ return OperationResult(
167
+ success=False,
168
+ message="Session not found",
169
+ error=f"No session with ID: {session_id}",
170
+ ).model_dump()
171
+
172
+ status = session.get_auto_save_status()
173
+
174
+ if ctx:
175
+ await ctx.info(f"Auto-save status retrieved for session {session_id}")
176
+
177
+ return OperationResult(
178
+ success=True, message="Auto-save status retrieved", session_id=session_id, data=status
179
+ ).model_dump()
180
+
181
+ except Exception as e:
182
+ logger.error(f"Error getting auto-save status: {e!s}")
183
+ if ctx:
184
+ await ctx.error(f"Failed to get auto-save status: {e!s}")
185
+ return OperationResult(
186
+ success=False, message="Failed to get auto-save status", error=str(e)
187
+ ).model_dump()
188
+
189
+
190
+ async def trigger_manual_save(session_id: str, ctx: Context = None) -> dict[str, Any]:
191
+ """
192
+ Manually trigger a save for a session.
193
+
194
+ Args:
195
+ session_id: Session identifier
196
+ ctx: FastMCP context
197
+
198
+ Returns:
199
+ Dict with save result
200
+ """
201
+ try:
202
+ manager = get_session_manager()
203
+ session = manager.get_session(session_id)
204
+
205
+ if not session:
206
+ return OperationResult(
207
+ success=False,
208
+ message="Session not found",
209
+ error=f"No session with ID: {session_id}",
210
+ ).model_dump()
211
+
212
+ if ctx:
213
+ await ctx.info(f"Triggering manual save for session {session_id}")
214
+
215
+ result = await session.manual_save()
216
+
217
+ if result["success"]:
218
+ if ctx:
219
+ await ctx.info(f"Manual save completed: {result.get('save_path')}")
220
+
221
+ return OperationResult(
222
+ success=True, message="Manual save completed", session_id=session_id, data=result
223
+ ).model_dump()
224
+ else:
225
+ return OperationResult(
226
+ success=False, message="Manual save failed", error=result.get("error")
227
+ ).model_dump()
228
+
229
+ except Exception as e:
230
+ logger.error(f"Error in manual save: {e!s}")
231
+ if ctx:
232
+ await ctx.error(f"Failed to trigger manual save: {e!s}")
233
+ return OperationResult(
234
+ success=False, message="Failed to trigger manual save", error=str(e)
235
+ ).model_dump()
@@ -0,0 +1,3 @@
1
+ """Data operations - imports from io_operations for compatibility."""
2
+
3
+ # Re-export io_operations functions as data_operations
@@ -0,0 +1,315 @@
1
+ """History operations for CSV sessions."""
2
+
3
+ import logging
4
+ from typing import Any
5
+
6
+ from fastmcp import Context
7
+
8
+ from ..models.csv_session import get_session_manager
9
+ from ..models.data_models import OperationResult
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ async def undo_operation(session_id: str, ctx: Context = None) -> dict[str, Any]:
15
+ """
16
+ Undo the last operation in a session.
17
+
18
+ Args:
19
+ session_id: Session identifier
20
+ ctx: FastMCP context
21
+
22
+ Returns:
23
+ Dict with success status and undo result
24
+ """
25
+ try:
26
+ manager = get_session_manager()
27
+ session = manager.get_session(session_id)
28
+
29
+ if not session:
30
+ return OperationResult(
31
+ success=False,
32
+ message="Session not found",
33
+ error=f"No session with ID: {session_id}",
34
+ ).model_dump()
35
+
36
+ if ctx:
37
+ await ctx.info(f"Undoing last operation for session {session_id}")
38
+
39
+ result = await session.undo()
40
+
41
+ if result["success"]:
42
+ if ctx:
43
+ await ctx.info(f"Successfully undid operation: {result.get('message')}")
44
+
45
+ return OperationResult(
46
+ success=True, message=result["message"], session_id=session_id, data=result
47
+ ).model_dump()
48
+ else:
49
+ return OperationResult(
50
+ success=False, message="Failed to undo operation", error=result.get("error")
51
+ ).model_dump()
52
+
53
+ except Exception as e:
54
+ logger.error(f"Error undoing operation: {e!s}")
55
+ if ctx:
56
+ await ctx.error(f"Failed to undo operation: {e!s}")
57
+ return OperationResult(
58
+ success=False, message="Failed to undo operation", error=str(e)
59
+ ).model_dump()
60
+
61
+
62
+ async def redo_operation(session_id: str, ctx: Context = None) -> dict[str, Any]:
63
+ """
64
+ Redo a previously undone operation.
65
+
66
+ Args:
67
+ session_id: Session identifier
68
+ ctx: FastMCP context
69
+
70
+ Returns:
71
+ Dict with success status and redo result
72
+ """
73
+ try:
74
+ manager = get_session_manager()
75
+ session = manager.get_session(session_id)
76
+
77
+ if not session:
78
+ return OperationResult(
79
+ success=False,
80
+ message="Session not found",
81
+ error=f"No session with ID: {session_id}",
82
+ ).model_dump()
83
+
84
+ if ctx:
85
+ await ctx.info(f"Redoing operation for session {session_id}")
86
+
87
+ result = await session.redo()
88
+
89
+ if result["success"]:
90
+ if ctx:
91
+ await ctx.info(f"Successfully redid operation: {result.get('message')}")
92
+
93
+ return OperationResult(
94
+ success=True, message=result["message"], session_id=session_id, data=result
95
+ ).model_dump()
96
+ else:
97
+ return OperationResult(
98
+ success=False, message="Failed to redo operation", error=result.get("error")
99
+ ).model_dump()
100
+
101
+ except Exception as e:
102
+ logger.error(f"Error redoing operation: {e!s}")
103
+ if ctx:
104
+ await ctx.error(f"Failed to redo operation: {e!s}")
105
+ return OperationResult(
106
+ success=False, message="Failed to redo operation", error=str(e)
107
+ ).model_dump()
108
+
109
+
110
+ async def get_operation_history(
111
+ session_id: str, limit: int | None = None, ctx: Context = None
112
+ ) -> dict[str, Any]:
113
+ """
114
+ Get operation history for a session.
115
+
116
+ Args:
117
+ session_id: Session identifier
118
+ limit: Maximum number of operations to return
119
+ ctx: FastMCP context
120
+
121
+ Returns:
122
+ Dict with history and statistics
123
+ """
124
+ try:
125
+ manager = get_session_manager()
126
+ session = manager.get_session(session_id)
127
+
128
+ if not session:
129
+ return OperationResult(
130
+ success=False,
131
+ message="Session not found",
132
+ error=f"No session with ID: {session_id}",
133
+ ).model_dump()
134
+
135
+ if ctx:
136
+ await ctx.info(f"Getting operation history for session {session_id}")
137
+
138
+ result = session.get_history(limit)
139
+
140
+ if result["success"]:
141
+ return OperationResult(
142
+ success=True,
143
+ message="History retrieved successfully",
144
+ session_id=session_id,
145
+ data=result,
146
+ ).model_dump()
147
+ else:
148
+ return OperationResult(
149
+ success=False, message="Failed to get history", error=result.get("error")
150
+ ).model_dump()
151
+
152
+ except Exception as e:
153
+ logger.error(f"Error getting history: {e!s}")
154
+ if ctx:
155
+ await ctx.error(f"Failed to get history: {e!s}")
156
+ return OperationResult(
157
+ success=False, message="Failed to get history", error=str(e)
158
+ ).model_dump()
159
+
160
+
161
+ async def restore_to_operation(
162
+ session_id: str, operation_id: str, ctx: Context = None
163
+ ) -> dict[str, Any]:
164
+ """
165
+ Restore session data to a specific operation point.
166
+
167
+ Args:
168
+ session_id: Session identifier
169
+ operation_id: Operation ID to restore to
170
+ ctx: FastMCP context
171
+
172
+ Returns:
173
+ Dict with success status and restore result
174
+ """
175
+ try:
176
+ manager = get_session_manager()
177
+ session = manager.get_session(session_id)
178
+
179
+ if not session:
180
+ return OperationResult(
181
+ success=False,
182
+ message="Session not found",
183
+ error=f"No session with ID: {session_id}",
184
+ ).model_dump()
185
+
186
+ if ctx:
187
+ await ctx.info(f"Restoring session {session_id} to operation {operation_id}")
188
+
189
+ result = await session.restore_to_operation(operation_id)
190
+
191
+ if result["success"]:
192
+ if ctx:
193
+ await ctx.info(f"Successfully restored to operation {operation_id}")
194
+
195
+ return OperationResult(
196
+ success=True, message=result["message"], session_id=session_id, data=result
197
+ ).model_dump()
198
+ else:
199
+ return OperationResult(
200
+ success=False, message="Failed to restore to operation", error=result.get("error")
201
+ ).model_dump()
202
+
203
+ except Exception as e:
204
+ logger.error(f"Error restoring to operation: {e!s}")
205
+ if ctx:
206
+ await ctx.error(f"Failed to restore to operation: {e!s}")
207
+ return OperationResult(
208
+ success=False, message="Failed to restore to operation", error=str(e)
209
+ ).model_dump()
210
+
211
+
212
+ async def clear_history(session_id: str, ctx: Context = None) -> dict[str, Any]:
213
+ """
214
+ Clear all operation history for a session.
215
+
216
+ Args:
217
+ session_id: Session identifier
218
+ ctx: FastMCP context
219
+
220
+ Returns:
221
+ Dict with success status
222
+ """
223
+ try:
224
+ manager = get_session_manager()
225
+ session = manager.get_session(session_id)
226
+
227
+ if not session:
228
+ return OperationResult(
229
+ success=False,
230
+ message="Session not found",
231
+ error=f"No session with ID: {session_id}",
232
+ ).model_dump()
233
+
234
+ if not session.history_manager:
235
+ return OperationResult(
236
+ success=False,
237
+ message="History is not enabled for this session",
238
+ error="History management is disabled",
239
+ ).model_dump()
240
+
241
+ if ctx:
242
+ await ctx.info(f"Clearing history for session {session_id}")
243
+
244
+ session.history_manager.clear_history()
245
+
246
+ return OperationResult(
247
+ success=True, message="History cleared successfully", session_id=session_id
248
+ ).model_dump()
249
+
250
+ except Exception as e:
251
+ logger.error(f"Error clearing history: {e!s}")
252
+ if ctx:
253
+ await ctx.error(f"Failed to clear history: {e!s}")
254
+ return OperationResult(
255
+ success=False, message="Failed to clear history", error=str(e)
256
+ ).model_dump()
257
+
258
+
259
+ async def export_history(
260
+ session_id: str, file_path: str, format: str = "json", ctx: Context = None
261
+ ) -> dict[str, Any]:
262
+ """
263
+ Export operation history to a file.
264
+
265
+ Args:
266
+ session_id: Session identifier
267
+ file_path: Path to export history to
268
+ format: Export format ('json' or 'csv')
269
+ ctx: FastMCP context
270
+
271
+ Returns:
272
+ Dict with success status
273
+ """
274
+ try:
275
+ manager = get_session_manager()
276
+ session = manager.get_session(session_id)
277
+
278
+ if not session:
279
+ return OperationResult(
280
+ success=False,
281
+ message="Session not found",
282
+ error=f"No session with ID: {session_id}",
283
+ ).model_dump()
284
+
285
+ if not session.history_manager:
286
+ return OperationResult(
287
+ success=False,
288
+ message="History is not enabled for this session",
289
+ error="History management is disabled",
290
+ ).model_dump()
291
+
292
+ if ctx:
293
+ await ctx.info(f"Exporting history for session {session_id} to {file_path}")
294
+
295
+ success = session.history_manager.export_history(file_path, format)
296
+
297
+ if success:
298
+ return OperationResult(
299
+ success=True,
300
+ message=f"History exported to {file_path}",
301
+ session_id=session_id,
302
+ data={"file_path": file_path, "format": format},
303
+ ).model_dump()
304
+ else:
305
+ return OperationResult(
306
+ success=False, message="Failed to export history", error="Export operation failed"
307
+ ).model_dump()
308
+
309
+ except Exception as e:
310
+ logger.error(f"Error exporting history: {e!s}")
311
+ if ctx:
312
+ await ctx.error(f"Failed to export history: {e!s}")
313
+ return OperationResult(
314
+ success=False, message="Failed to export history", error=str(e)
315
+ ).model_dump()