@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.
- package/.github/ISSUE_TEMPLATE/bug_report.md +53 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +38 -0
- package/.github/workflows/deploy-docs.yml +62 -0
- package/.github/workflows/publish-github.yml +52 -0
- package/.github/workflows/publish.yml +44 -0
- package/.github/workflows/test.yml +32 -0
- package/.pre-commit-config.yaml +157 -0
- package/ALTERNATIVE_PUBLISHING.md +175 -0
- package/ARCHITECTURE.md +1011 -0
- package/CHANGELOG.md +99 -0
- package/CODE_OF_CONDUCT.md +41 -0
- package/CONTRIBUTING.md +427 -0
- package/Dockerfile +22 -0
- package/LICENSE +21 -0
- package/MCP_CONFIG.md +505 -0
- package/PUBLISHING.md +210 -0
- package/README.md +400 -0
- package/SECURITY.md +61 -0
- package/docs/README.md +41 -0
- package/docs/blog/2019-05-28-first-blog-post.md +12 -0
- package/docs/blog/2019-05-29-long-blog-post.md +44 -0
- package/docs/blog/2021-08-01-mdx-blog-post.mdx +24 -0
- package/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg +0 -0
- package/docs/blog/2021-08-26-welcome/index.md +29 -0
- package/docs/blog/authors.yml +25 -0
- package/docs/blog/tags.yml +19 -0
- package/docs/docs/api/overview.md +183 -0
- package/docs/docs/installation.md +252 -0
- package/docs/docs/intro.md +87 -0
- package/docs/docs/tutorial-basics/_category_.json +8 -0
- package/docs/docs/tutorial-basics/congratulations.md +23 -0
- package/docs/docs/tutorial-basics/create-a-blog-post.md +34 -0
- package/docs/docs/tutorial-basics/create-a-document.md +57 -0
- package/docs/docs/tutorial-basics/create-a-page.md +43 -0
- package/docs/docs/tutorial-basics/deploy-your-site.md +31 -0
- package/docs/docs/tutorial-basics/markdown-features.mdx +152 -0
- package/docs/docs/tutorial-extras/_category_.json +7 -0
- package/docs/docs/tutorial-extras/img/docsVersionDropdown.png +0 -0
- package/docs/docs/tutorial-extras/img/localeDropdown.png +0 -0
- package/docs/docs/tutorial-extras/manage-docs-versions.md +55 -0
- package/docs/docs/tutorial-extras/translate-your-site.md +88 -0
- package/docs/docs/tutorials/quickstart.md +365 -0
- package/docs/docusaurus.config.ts +163 -0
- package/docs/package-lock.json +17493 -0
- package/docs/package.json +48 -0
- package/docs/sidebars.ts +33 -0
- package/docs/src/components/HomepageFeatures/index.tsx +71 -0
- package/docs/src/components/HomepageFeatures/styles.module.css +11 -0
- package/docs/src/css/custom.css +30 -0
- package/docs/src/pages/index.module.css +23 -0
- package/docs/src/pages/index.tsx +44 -0
- package/docs/src/pages/markdown-page.md +7 -0
- package/docs/static/.nojekyll +0 -0
- package/docs/static/img/docusaurus-social-card.jpg +0 -0
- package/docs/static/img/docusaurus.png +0 -0
- package/docs/static/img/favicon.ico +0 -0
- package/docs/static/img/logo.svg +1 -0
- package/docs/static/img/undraw_docusaurus_mountain.svg +171 -0
- package/docs/static/img/undraw_docusaurus_react.svg +170 -0
- package/docs/static/img/undraw_docusaurus_tree.svg +40 -0
- package/docs/tsconfig.json +8 -0
- package/examples/README.md +48 -0
- package/examples/auto_save_demo.py +206 -0
- package/examples/auto_save_overwrite.py +201 -0
- package/examples/basic_usage.py +135 -0
- package/examples/demo.py +139 -0
- package/examples/history_demo.py +317 -0
- package/examples/test_default_autosave.py +124 -0
- package/examples/update_consignee_example.py +179 -0
- package/package.json +51 -0
- package/plans/2026-04-19-fastmcp3-migration-plan.md +1045 -0
- package/pyproject.toml +331 -0
- package/requirements-dev.txt +30 -0
- package/requirements.txt +22 -0
- package/scripts/publish.py +67 -0
- package/smithery.yaml +15 -0
- package/specs/2026-04-19-fastmcp3-migration-design.md +243 -0
- package/src/csv_editor/__init__.py +8 -0
- package/src/csv_editor/models/__init__.py +39 -0
- package/src/csv_editor/models/auto_save.py +246 -0
- package/src/csv_editor/models/csv_session.py +468 -0
- package/src/csv_editor/models/data_models.py +244 -0
- package/src/csv_editor/models/history_manager.py +456 -0
- package/src/csv_editor/prompts/__init__.py +0 -0
- package/src/csv_editor/prompts/data_prompts.py +13 -0
- package/src/csv_editor/resources/__init__.py +0 -0
- package/src/csv_editor/resources/csv_resources.py +22 -0
- package/src/csv_editor/server.py +640 -0
- package/src/csv_editor/tools/__init__.py +5 -0
- package/src/csv_editor/tools/analytics.py +700 -0
- package/src/csv_editor/tools/auto_save_operations.py +235 -0
- package/src/csv_editor/tools/data_operations.py +3 -0
- package/src/csv_editor/tools/history_operations.py +315 -0
- package/src/csv_editor/tools/io_operations.py +431 -0
- package/src/csv_editor/tools/transformations.py +663 -0
- package/src/csv_editor/tools/validation.py +822 -0
- package/src/csv_editor/utils/__init__.py +0 -0
- package/src/csv_editor/utils/validators.py +205 -0
- package/tests/README.md +65 -0
- package/tests/__init__.py +7 -0
- package/tests/conftest.py +50 -0
- package/tests/test_auto_save.py +378 -0
- package/tests/test_basic.py +103 -0
- package/tests/test_integration.py +356 -0
- package/tests/test_server_boot.py +50 -0
- package/tests/test_settings.py +184 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Demonstration of history tracking with undo/redo functionality."""
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import tempfile
|
|
6
|
+
import os
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import json
|
|
10
|
+
|
|
11
|
+
# Setup path for imports
|
|
12
|
+
import sys
|
|
13
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
14
|
+
|
|
15
|
+
from src.csv_editor.models.csv_session import CSVSession
|
|
16
|
+
from src.csv_editor.models.history_manager import HistoryStorage
|
|
17
|
+
from src.csv_editor.models.data_models import OperationType
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def demonstrate_history():
|
|
21
|
+
"""Demonstrate history tracking with undo/redo capabilities."""
|
|
22
|
+
|
|
23
|
+
print("=" * 60)
|
|
24
|
+
print("CSV Editor History & Undo/Redo Demonstration")
|
|
25
|
+
print("=" * 60)
|
|
26
|
+
|
|
27
|
+
# Create initial data
|
|
28
|
+
initial_data = pd.DataFrame({
|
|
29
|
+
'product': ['Laptop', 'Mouse', 'Keyboard', 'Monitor'],
|
|
30
|
+
'price': [999.99, 29.99, 79.99, 299.99],
|
|
31
|
+
'stock': [50, 200, 150, 75],
|
|
32
|
+
'category': ['Electronics', 'Accessories', 'Accessories', 'Electronics']
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
# Create session with history enabled (JSON persistence)
|
|
36
|
+
session = CSVSession(
|
|
37
|
+
enable_history=True,
|
|
38
|
+
history_storage=HistoryStorage.JSON
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Load data
|
|
42
|
+
temp_file = os.path.join(tempfile.gettempdir(), "demo_data.csv")
|
|
43
|
+
initial_data.to_csv(temp_file, index=False)
|
|
44
|
+
session.load_data(initial_data, file_path=temp_file)
|
|
45
|
+
|
|
46
|
+
print(f"\n✓ Created session with history tracking")
|
|
47
|
+
print(f"Initial data:\n{session.df}\n")
|
|
48
|
+
|
|
49
|
+
# Perform a series of operations
|
|
50
|
+
print("-" * 40)
|
|
51
|
+
print("Performing operations...")
|
|
52
|
+
print("-" * 40)
|
|
53
|
+
|
|
54
|
+
# Operation 1: Increase prices
|
|
55
|
+
print("\n1. Increasing all prices by 10%")
|
|
56
|
+
session.df['price'] = session.df['price'] * 1.1
|
|
57
|
+
session.record_operation(OperationType.TRANSFORM, {"operation": "price_increase_10%"})
|
|
58
|
+
print(f"Prices after increase:\n{session.df['price'].tolist()}")
|
|
59
|
+
|
|
60
|
+
# Operation 2: Filter low stock
|
|
61
|
+
print("\n2. Filtering items with stock >= 100")
|
|
62
|
+
session.df = session.df[session.df['stock'] >= 100]
|
|
63
|
+
session.record_operation(OperationType.FILTER, {"condition": "stock >= 100"})
|
|
64
|
+
print(f"Products after filter: {session.df['product'].tolist()}")
|
|
65
|
+
|
|
66
|
+
# Operation 3: Add discount column
|
|
67
|
+
print("\n3. Adding discount column")
|
|
68
|
+
session.df['discount'] = 0.15
|
|
69
|
+
session.record_operation(OperationType.ADD_COLUMN, {"column": "discount", "value": 0.15})
|
|
70
|
+
print(f"Columns: {session.df.columns.tolist()}")
|
|
71
|
+
|
|
72
|
+
# Operation 4: Sort by price
|
|
73
|
+
print("\n4. Sorting by price (descending)")
|
|
74
|
+
session.df = session.df.sort_values('price', ascending=False)
|
|
75
|
+
session.record_operation(OperationType.SORT, {"column": "price", "ascending": False})
|
|
76
|
+
print(f"Products after sort: {session.df['product'].tolist()}")
|
|
77
|
+
|
|
78
|
+
# Show current history
|
|
79
|
+
print("\n" + "=" * 60)
|
|
80
|
+
print("Current History")
|
|
81
|
+
print("=" * 60)
|
|
82
|
+
history_result = session.get_history()
|
|
83
|
+
if history_result["success"]:
|
|
84
|
+
print(f"Total operations: {history_result['statistics']['total_operations']}")
|
|
85
|
+
print(f"Current position: {history_result['statistics']['current_position']}")
|
|
86
|
+
print(f"Can undo: {history_result['statistics']['can_undo']}")
|
|
87
|
+
print(f"Can redo: {history_result['statistics']['can_redo']}")
|
|
88
|
+
|
|
89
|
+
print("\nOperations:")
|
|
90
|
+
for op in history_result['history']:
|
|
91
|
+
print(f" [{op['index']}] {op['operation_type']} - {op['timestamp']}")
|
|
92
|
+
if op['is_current']:
|
|
93
|
+
print(f" ^ Current position")
|
|
94
|
+
|
|
95
|
+
# Demonstrate undo
|
|
96
|
+
print("\n" + "=" * 60)
|
|
97
|
+
print("Undo Operations")
|
|
98
|
+
print("=" * 60)
|
|
99
|
+
|
|
100
|
+
print("\n1. Undoing last operation (sort)...")
|
|
101
|
+
undo_result = await session.undo()
|
|
102
|
+
if undo_result["success"]:
|
|
103
|
+
print(f"✓ {undo_result['message']}")
|
|
104
|
+
print(f"Products order: {session.df['product'].tolist() if session.df is not None else 'N/A'}")
|
|
105
|
+
|
|
106
|
+
print("\n2. Undoing again (add discount column)...")
|
|
107
|
+
undo_result = await session.undo()
|
|
108
|
+
if undo_result["success"]:
|
|
109
|
+
print(f"✓ {undo_result['message']}")
|
|
110
|
+
print(f"Columns: {session.df.columns.tolist() if session.df is not None else 'N/A'}")
|
|
111
|
+
|
|
112
|
+
print("\n3. Undoing once more (filter)...")
|
|
113
|
+
undo_result = await session.undo()
|
|
114
|
+
if undo_result["success"]:
|
|
115
|
+
print(f"✓ {undo_result['message']}")
|
|
116
|
+
print(f"Row count: {len(session.df) if session.df is not None else 0}")
|
|
117
|
+
print(f"Products: {session.df['product'].tolist() if session.df is not None else 'N/A'}")
|
|
118
|
+
|
|
119
|
+
# Show history after undo
|
|
120
|
+
history_result = session.get_history()
|
|
121
|
+
print(f"\nCurrent position after undos: {history_result['statistics']['current_position']}")
|
|
122
|
+
print(f"Can redo: {history_result['statistics']['can_redo']}")
|
|
123
|
+
|
|
124
|
+
# Demonstrate redo
|
|
125
|
+
print("\n" + "=" * 60)
|
|
126
|
+
print("Redo Operations")
|
|
127
|
+
print("=" * 60)
|
|
128
|
+
|
|
129
|
+
print("\n1. Redoing (re-apply filter)...")
|
|
130
|
+
redo_result = await session.redo()
|
|
131
|
+
if redo_result["success"]:
|
|
132
|
+
print(f"✓ {redo_result['message']}")
|
|
133
|
+
print(f"Row count: {len(session.df) if session.df is not None else 0}")
|
|
134
|
+
|
|
135
|
+
print("\n2. Redoing again (re-add discount column)...")
|
|
136
|
+
redo_result = await session.redo()
|
|
137
|
+
if redo_result["success"]:
|
|
138
|
+
print(f"✓ {redo_result['message']}")
|
|
139
|
+
print(f"Columns: {session.df.columns.tolist() if session.df is not None else 'N/A'}")
|
|
140
|
+
|
|
141
|
+
# Perform a new operation (clears redo stack)
|
|
142
|
+
print("\n" + "=" * 60)
|
|
143
|
+
print("New Operation (clears redo stack)")
|
|
144
|
+
print("=" * 60)
|
|
145
|
+
|
|
146
|
+
print("\nAdding a new column 'in_stock'...")
|
|
147
|
+
session.df['in_stock'] = True
|
|
148
|
+
session.record_operation(OperationType.ADD_COLUMN, {"column": "in_stock", "value": True})
|
|
149
|
+
print(f"Columns: {session.df.columns.tolist()}")
|
|
150
|
+
|
|
151
|
+
history_result = session.get_history()
|
|
152
|
+
print(f"\nCan still redo? {history_result['statistics']['can_redo']} (should be False)")
|
|
153
|
+
|
|
154
|
+
# Demonstrate restore to specific operation
|
|
155
|
+
print("\n" + "=" * 60)
|
|
156
|
+
print("Restore to Specific Operation")
|
|
157
|
+
print("=" * 60)
|
|
158
|
+
|
|
159
|
+
# Get first operation ID
|
|
160
|
+
history_result = session.get_history()
|
|
161
|
+
if history_result["success"] and history_result["history"]:
|
|
162
|
+
first_op = history_result["history"][0]
|
|
163
|
+
print(f"\nRestoring to first operation: {first_op['operation_type']}")
|
|
164
|
+
|
|
165
|
+
restore_result = await session.restore_to_operation(first_op['operation_id'])
|
|
166
|
+
if restore_result["success"]:
|
|
167
|
+
print(f"✓ {restore_result['message']}")
|
|
168
|
+
print(f"Data shape: {restore_result['shape']}")
|
|
169
|
+
print(f"Current data:\n{session.df}")
|
|
170
|
+
|
|
171
|
+
# Export history
|
|
172
|
+
print("\n" + "=" * 60)
|
|
173
|
+
print("Export History")
|
|
174
|
+
print("=" * 60)
|
|
175
|
+
|
|
176
|
+
# Export as JSON
|
|
177
|
+
history_file = os.path.join(tempfile.gettempdir(), "history_export.json")
|
|
178
|
+
if session.history_manager:
|
|
179
|
+
success = session.history_manager.export_history(history_file, "json")
|
|
180
|
+
if success:
|
|
181
|
+
print(f"✓ History exported to: {history_file}")
|
|
182
|
+
|
|
183
|
+
# Show exported content
|
|
184
|
+
with open(history_file, 'r') as f:
|
|
185
|
+
history_data = json.load(f)
|
|
186
|
+
print(f" Total operations: {history_data['total_operations']}")
|
|
187
|
+
print(f" Exported at: {history_data['exported_at']}")
|
|
188
|
+
|
|
189
|
+
# Show history statistics
|
|
190
|
+
print("\n" + "=" * 60)
|
|
191
|
+
print("History Statistics")
|
|
192
|
+
print("=" * 60)
|
|
193
|
+
|
|
194
|
+
if session.history_manager:
|
|
195
|
+
stats = session.history_manager.get_statistics()
|
|
196
|
+
print(f"Total operations: {stats['total_operations']}")
|
|
197
|
+
print(f"Current position: {stats['current_position']}")
|
|
198
|
+
print(f"Snapshots saved: {stats['snapshots_count']}")
|
|
199
|
+
print(f"Storage type: {stats['storage_type']}")
|
|
200
|
+
print("\nOperation breakdown:")
|
|
201
|
+
for op_type, count in stats['operation_types'].items():
|
|
202
|
+
print(f" {op_type}: {count}")
|
|
203
|
+
|
|
204
|
+
# Show persistence
|
|
205
|
+
print("\n" + "=" * 60)
|
|
206
|
+
print("History Persistence")
|
|
207
|
+
print("=" * 60)
|
|
208
|
+
|
|
209
|
+
history_dir = session.history_manager.history_dir if session.history_manager else None
|
|
210
|
+
if history_dir:
|
|
211
|
+
print(f"History directory: {history_dir}")
|
|
212
|
+
|
|
213
|
+
# List history files
|
|
214
|
+
history_files = list(Path(history_dir).glob(f"*{session.session_id}*"))
|
|
215
|
+
print(f"History files created: {len(history_files)}")
|
|
216
|
+
for hf in history_files:
|
|
217
|
+
print(f" - {hf.name}")
|
|
218
|
+
|
|
219
|
+
# Check snapshot directory
|
|
220
|
+
snapshot_dir = Path(history_dir) / "snapshots" / session.session_id
|
|
221
|
+
if snapshot_dir.exists():
|
|
222
|
+
snapshots = list(snapshot_dir.glob("*.pkl"))
|
|
223
|
+
print(f"Snapshots saved: {len(snapshots)}")
|
|
224
|
+
|
|
225
|
+
print("\n✅ History demonstration completed!")
|
|
226
|
+
|
|
227
|
+
return session
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
async def demonstrate_history_recovery():
|
|
231
|
+
"""Demonstrate recovering history from a previous session."""
|
|
232
|
+
|
|
233
|
+
print("\n" + "=" * 60)
|
|
234
|
+
print("History Recovery from Previous Session")
|
|
235
|
+
print("=" * 60)
|
|
236
|
+
|
|
237
|
+
# Create a session and perform operations
|
|
238
|
+
print("\n1. Creating initial session with operations...")
|
|
239
|
+
session1 = CSVSession(
|
|
240
|
+
session_id="demo-recovery-session",
|
|
241
|
+
enable_history=True,
|
|
242
|
+
history_storage=HistoryStorage.JSON
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
data = pd.DataFrame({
|
|
246
|
+
'item': ['A', 'B', 'C'],
|
|
247
|
+
'value': [10, 20, 30]
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
session1.load_data(data)
|
|
251
|
+
|
|
252
|
+
# Perform operations
|
|
253
|
+
session1.df['value'] = session1.df['value'] * 2
|
|
254
|
+
session1.record_operation(OperationType.TRANSFORM, {"operation": "double_values"})
|
|
255
|
+
|
|
256
|
+
session1.df['status'] = 'active'
|
|
257
|
+
session1.record_operation(OperationType.ADD_COLUMN, {"column": "status"})
|
|
258
|
+
|
|
259
|
+
print(f" Operations performed: 2")
|
|
260
|
+
print(f" Final data:\n{session1.df}")
|
|
261
|
+
|
|
262
|
+
# Simulate session end
|
|
263
|
+
session1_id = session1.session_id
|
|
264
|
+
del session1
|
|
265
|
+
|
|
266
|
+
print("\n2. Session ended. Creating new session with same ID...")
|
|
267
|
+
|
|
268
|
+
# Create new session with same ID - should load history
|
|
269
|
+
session2 = CSVSession(
|
|
270
|
+
session_id=session1_id,
|
|
271
|
+
enable_history=True,
|
|
272
|
+
history_storage=HistoryStorage.JSON
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
# Load the data (in real scenario, would load from file)
|
|
276
|
+
session2.load_data(data)
|
|
277
|
+
|
|
278
|
+
# Check if history was recovered
|
|
279
|
+
history_result = session2.get_history()
|
|
280
|
+
if history_result["success"]:
|
|
281
|
+
print(f"✓ History recovered! Found {history_result['statistics']['total_operations']} operations")
|
|
282
|
+
|
|
283
|
+
# Can we undo operations from previous session?
|
|
284
|
+
print("\n3. Testing undo on recovered history...")
|
|
285
|
+
if history_result['statistics']['can_undo']:
|
|
286
|
+
undo_result = await session2.undo()
|
|
287
|
+
if undo_result["success"]:
|
|
288
|
+
print(f"✓ Successfully undid operation from previous session!")
|
|
289
|
+
print(f" Operation: {undo_result['operation']['operation_type']}")
|
|
290
|
+
|
|
291
|
+
print("\n✅ History recovery demonstration completed!")
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
async def main():
|
|
295
|
+
"""Run all history demonstrations."""
|
|
296
|
+
|
|
297
|
+
# Demo 1: Basic history with undo/redo
|
|
298
|
+
session = await demonstrate_history()
|
|
299
|
+
|
|
300
|
+
# Demo 2: History recovery
|
|
301
|
+
await demonstrate_history_recovery()
|
|
302
|
+
|
|
303
|
+
print("\n" + "=" * 60)
|
|
304
|
+
print("Key Features Demonstrated")
|
|
305
|
+
print("=" * 60)
|
|
306
|
+
print("✓ Operation history tracking with timestamps")
|
|
307
|
+
print("✓ Persistent history storage (JSON/Pickle)")
|
|
308
|
+
print("✓ Undo/Redo functionality")
|
|
309
|
+
print("✓ Restore to any previous operation")
|
|
310
|
+
print("✓ History export (JSON/CSV)")
|
|
311
|
+
print("✓ History recovery across sessions")
|
|
312
|
+
print("✓ Automatic snapshots for data recovery")
|
|
313
|
+
print("✓ History statistics and analysis")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
if __name__ == "__main__":
|
|
317
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Test that auto-save is enabled by default and works correctly."""
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import tempfile
|
|
6
|
+
import os
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
# Setup path for imports
|
|
11
|
+
import sys
|
|
12
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
13
|
+
|
|
14
|
+
from src.csv_editor.models.csv_session import CSVSession
|
|
15
|
+
from src.csv_editor.models.data_models import OperationType
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def test_default_autosave():
|
|
19
|
+
"""Test that auto-save is enabled by default."""
|
|
20
|
+
|
|
21
|
+
print("=" * 60)
|
|
22
|
+
print("Testing Default Auto-Save Configuration")
|
|
23
|
+
print("=" * 60)
|
|
24
|
+
|
|
25
|
+
# Create a temporary CSV file
|
|
26
|
+
temp_dir = tempfile.mkdtemp()
|
|
27
|
+
test_file = os.path.join(temp_dir, "test_data.csv")
|
|
28
|
+
|
|
29
|
+
# Create initial data
|
|
30
|
+
initial_data = pd.DataFrame({
|
|
31
|
+
'id': [1, 2, 3],
|
|
32
|
+
'name': ['Alice', 'Bob', 'Charlie'],
|
|
33
|
+
'value': [100, 200, 300]
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
# Save initial data
|
|
37
|
+
initial_data.to_csv(test_file, index=False)
|
|
38
|
+
print(f"\n✓ Created test file: {test_file}")
|
|
39
|
+
print(f"Initial content:\n{initial_data}")
|
|
40
|
+
|
|
41
|
+
# Create session WITHOUT explicitly configuring auto-save
|
|
42
|
+
session = CSVSession() # Using defaults
|
|
43
|
+
|
|
44
|
+
# Check default auto-save configuration
|
|
45
|
+
print("\n" + "-" * 40)
|
|
46
|
+
print("Default Auto-Save Configuration:")
|
|
47
|
+
print("-" * 40)
|
|
48
|
+
config = session.auto_save_config
|
|
49
|
+
print(f"Enabled: {config.enabled}")
|
|
50
|
+
print(f"Mode: {config.mode.value}")
|
|
51
|
+
print(f"Strategy: {config.strategy.value}")
|
|
52
|
+
|
|
53
|
+
# Verify defaults
|
|
54
|
+
assert config.enabled == True, "Auto-save should be enabled by default"
|
|
55
|
+
assert config.mode.value == "after_operation", "Mode should be 'after_operation' by default"
|
|
56
|
+
assert config.strategy.value == "overwrite", "Strategy should be 'overwrite' by default"
|
|
57
|
+
print("\n✅ All defaults are correct!")
|
|
58
|
+
|
|
59
|
+
# Load the CSV file
|
|
60
|
+
df = pd.read_csv(test_file)
|
|
61
|
+
session.load_data(df, file_path=test_file)
|
|
62
|
+
|
|
63
|
+
print("\n" + "-" * 40)
|
|
64
|
+
print("Testing Auto-Save on Operations:")
|
|
65
|
+
print("-" * 40)
|
|
66
|
+
|
|
67
|
+
# Perform an operation
|
|
68
|
+
print("\n1. Doubling all values...")
|
|
69
|
+
session.df['value'] = session.df['value'] * 2
|
|
70
|
+
session.record_operation(OperationType.TRANSFORM, {"operation": "double_values"})
|
|
71
|
+
|
|
72
|
+
# Trigger auto-save (should happen automatically after operation)
|
|
73
|
+
result = await session.trigger_auto_save_if_needed()
|
|
74
|
+
print(f" Auto-save triggered: {result is not None}")
|
|
75
|
+
|
|
76
|
+
# Read the file to verify it was updated
|
|
77
|
+
saved_df = pd.read_csv(test_file)
|
|
78
|
+
print(f" Values in file after operation: {saved_df['value'].tolist()}")
|
|
79
|
+
assert saved_df['value'].tolist() == [200, 400, 600], "File should be auto-saved with new values"
|
|
80
|
+
print(" ✓ File was automatically updated!")
|
|
81
|
+
|
|
82
|
+
# Perform another operation
|
|
83
|
+
print("\n2. Adding a new column...")
|
|
84
|
+
session.df['status'] = 'active'
|
|
85
|
+
session.record_operation(OperationType.ADD_COLUMN, {"column": "status"})
|
|
86
|
+
|
|
87
|
+
# Trigger auto-save again
|
|
88
|
+
result = await session.trigger_auto_save_if_needed()
|
|
89
|
+
print(f" Auto-save triggered: {result is not None}")
|
|
90
|
+
|
|
91
|
+
# Verify the file has the new column
|
|
92
|
+
saved_df = pd.read_csv(test_file)
|
|
93
|
+
print(f" Columns in file: {saved_df.columns.tolist()}")
|
|
94
|
+
assert 'status' in saved_df.columns, "New column should be in the saved file"
|
|
95
|
+
print(" ✓ New column was automatically saved!")
|
|
96
|
+
|
|
97
|
+
# Show final file content
|
|
98
|
+
print("\n" + "-" * 40)
|
|
99
|
+
print("Final File Content:")
|
|
100
|
+
print("-" * 40)
|
|
101
|
+
print(saved_df)
|
|
102
|
+
|
|
103
|
+
print("\n" + "=" * 60)
|
|
104
|
+
print("✅ Default Auto-Save Test Passed!")
|
|
105
|
+
print("=" * 60)
|
|
106
|
+
print("\nSummary:")
|
|
107
|
+
print("• Auto-save is ENABLED by default")
|
|
108
|
+
print("• Mode is 'after_operation' by default")
|
|
109
|
+
print("• Strategy is 'overwrite' by default")
|
|
110
|
+
print("• Original file is automatically updated after each operation")
|
|
111
|
+
print("• No manual configuration needed!")
|
|
112
|
+
|
|
113
|
+
return test_file
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
async def main():
|
|
117
|
+
"""Run the test."""
|
|
118
|
+
test_file = await test_default_autosave()
|
|
119
|
+
print(f"\nTest file location: {test_file}")
|
|
120
|
+
print("(File has been automatically saved with all changes)")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
if __name__ == "__main__":
|
|
124
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Example showing how to update the consignee name field using the new update_column function."""
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
from mcp_client import MCPClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def update_consignee_example():
|
|
9
|
+
"""Example of updating consignee field to keep only the company name."""
|
|
10
|
+
|
|
11
|
+
# Initialize MCP client
|
|
12
|
+
client = MCPClient()
|
|
13
|
+
await client.connect("csv-editor")
|
|
14
|
+
|
|
15
|
+
# Load the CSV file
|
|
16
|
+
print("Loading CSV file...")
|
|
17
|
+
result = await client.call_tool(
|
|
18
|
+
"load_csv",
|
|
19
|
+
{
|
|
20
|
+
"file_path": "/home/santosh/projects/csv-editor/tests/sample_data/123456/1753698447530_BOL_Lubecon USA LLC_GHY 1.csv"
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
session_id = result["session_id"]
|
|
24
|
+
print(f"Session created: {session_id}")
|
|
25
|
+
print(f"Loaded {result['row_count']} rows with {result['column_count']} columns")
|
|
26
|
+
|
|
27
|
+
# Get current consignee value
|
|
28
|
+
print("\nGetting current consignee value...")
|
|
29
|
+
info_result = await client.call_tool(
|
|
30
|
+
"get_session_info",
|
|
31
|
+
{"session_id": session_id}
|
|
32
|
+
)
|
|
33
|
+
print(f"Columns: {info_result['columns'][:10]}...") # Show first 10 columns
|
|
34
|
+
|
|
35
|
+
# Method 1: Using 'split' operation to extract company name
|
|
36
|
+
print("\n--- Method 1: Using 'split' operation ---")
|
|
37
|
+
print("Extracting company name (everything before the dash)...")
|
|
38
|
+
|
|
39
|
+
result = await client.call_tool(
|
|
40
|
+
"update_column",
|
|
41
|
+
{
|
|
42
|
+
"session_id": session_id,
|
|
43
|
+
"column": "Consignee Name",
|
|
44
|
+
"operation": "split",
|
|
45
|
+
"pattern": " -", # Split on " -" (space and dash)
|
|
46
|
+
"value": 0 # Take the first part (index 0)
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if result["success"]:
|
|
51
|
+
print(f"✓ Column updated successfully!")
|
|
52
|
+
print(f" Original: {result['original_sample'][0] if result['original_sample'] else 'N/A'}")
|
|
53
|
+
print(f" Updated: {result['updated_sample'][0] if result['updated_sample'] else 'N/A'}")
|
|
54
|
+
|
|
55
|
+
# Export the result
|
|
56
|
+
print("\nExporting updated CSV...")
|
|
57
|
+
export_result = await client.call_tool(
|
|
58
|
+
"export_csv",
|
|
59
|
+
{
|
|
60
|
+
"session_id": session_id,
|
|
61
|
+
"file_path": "/tmp/updated_bol_method1.csv",
|
|
62
|
+
"format": "csv"
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
print(f"✓ Exported to: {export_result['file_path']}")
|
|
66
|
+
|
|
67
|
+
# Close session
|
|
68
|
+
await client.call_tool("close_session", {"session_id": session_id})
|
|
69
|
+
|
|
70
|
+
# Method 2: Using 'replace' operation with regex
|
|
71
|
+
print("\n--- Method 2: Using 'replace' with regex ---")
|
|
72
|
+
|
|
73
|
+
# Load the file again
|
|
74
|
+
result = await client.call_tool(
|
|
75
|
+
"load_csv",
|
|
76
|
+
{
|
|
77
|
+
"file_path": "/home/santosh/projects/csv-editor/tests/sample_data/123456/1753698447530_BOL_Lubecon USA LLC_GHY 1.csv"
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
session_id = result["session_id"]
|
|
81
|
+
|
|
82
|
+
print("Removing location information using regex...")
|
|
83
|
+
result = await client.call_tool(
|
|
84
|
+
"update_column",
|
|
85
|
+
{
|
|
86
|
+
"session_id": session_id,
|
|
87
|
+
"column": "Consignee Name",
|
|
88
|
+
"operation": "replace",
|
|
89
|
+
"pattern": r"\s*-.*$", # Match everything from " -" to end of string
|
|
90
|
+
"replacement": "" # Replace with empty string
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if result["success"]:
|
|
95
|
+
print(f"✓ Column updated successfully!")
|
|
96
|
+
print(f" Original: {result['original_sample'][0] if result['original_sample'] else 'N/A'}")
|
|
97
|
+
print(f" Updated: {result['updated_sample'][0] if result['updated_sample'] else 'N/A'}")
|
|
98
|
+
|
|
99
|
+
# Export the result
|
|
100
|
+
export_result = await client.call_tool(
|
|
101
|
+
"export_csv",
|
|
102
|
+
{
|
|
103
|
+
"session_id": session_id,
|
|
104
|
+
"file_path": "/tmp/updated_bol_method2.csv",
|
|
105
|
+
"format": "csv"
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
print(f"✓ Exported to: {export_result['file_path']}")
|
|
109
|
+
|
|
110
|
+
# Close session
|
|
111
|
+
await client.call_tool("close_session", {"session_id": session_id})
|
|
112
|
+
|
|
113
|
+
# Method 3: Using 'extract' operation
|
|
114
|
+
print("\n--- Method 3: Using 'extract' with regex ---")
|
|
115
|
+
|
|
116
|
+
# Load the file again
|
|
117
|
+
result = await client.call_tool(
|
|
118
|
+
"load_csv",
|
|
119
|
+
{
|
|
120
|
+
"file_path": "/home/santosh/projects/csv-editor/tests/sample_data/123456/1753698447530_BOL_Lubecon USA LLC_GHY 1.csv"
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
session_id = result["session_id"]
|
|
124
|
+
|
|
125
|
+
print("Extracting company name using regex...")
|
|
126
|
+
result = await client.call_tool(
|
|
127
|
+
"update_column",
|
|
128
|
+
{
|
|
129
|
+
"session_id": session_id,
|
|
130
|
+
"column": "Consignee Name",
|
|
131
|
+
"operation": "extract",
|
|
132
|
+
"pattern": r"^([^-]+)", # Extract everything before the first dash
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if result["success"]:
|
|
137
|
+
print(f"✓ Column updated successfully!")
|
|
138
|
+
print(f" Original: {result['original_sample'][0] if result['original_sample'] else 'N/A'}")
|
|
139
|
+
print(f" Updated: {result['updated_sample'][0] if result['updated_sample'] else 'N/A'}")
|
|
140
|
+
|
|
141
|
+
# Also clean up any trailing spaces
|
|
142
|
+
print("\nCleaning up whitespace...")
|
|
143
|
+
result = await client.call_tool(
|
|
144
|
+
"update_column",
|
|
145
|
+
{
|
|
146
|
+
"session_id": session_id,
|
|
147
|
+
"column": "Consignee Name",
|
|
148
|
+
"operation": "strip"
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
print(f"✓ Whitespace cleaned")
|
|
152
|
+
|
|
153
|
+
# Export the result
|
|
154
|
+
export_result = await client.call_tool(
|
|
155
|
+
"export_csv",
|
|
156
|
+
{
|
|
157
|
+
"session_id": session_id,
|
|
158
|
+
"file_path": "/tmp/updated_bol_method3.csv",
|
|
159
|
+
"format": "csv"
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
print(f"✓ Exported to: {export_result['file_path']}")
|
|
163
|
+
|
|
164
|
+
# Close session
|
|
165
|
+
await client.call_tool("close_session", {"session_id": session_id})
|
|
166
|
+
|
|
167
|
+
await client.disconnect()
|
|
168
|
+
|
|
169
|
+
print("\n✅ All methods completed successfully!")
|
|
170
|
+
print("\nThe new 'update_column' function makes it simple to:")
|
|
171
|
+
print(" • Extract parts of text (split, extract)")
|
|
172
|
+
print(" • Clean up text (strip, replace)")
|
|
173
|
+
print(" • Transform text (upper, lower)")
|
|
174
|
+
print(" • Fill missing values")
|
|
175
|
+
print("\nCheck the exported files in /tmp/ to see the results.")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
if __name__ == "__main__":
|
|
179
|
+
asyncio.run(update_consignee_example())
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mseep/csv-editor",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for comprehensive CSV file operations",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"mcp-server",
|
|
8
|
+
"csv",
|
|
9
|
+
"excel",
|
|
10
|
+
"data",
|
|
11
|
+
"pandas",
|
|
12
|
+
"model-context-protocol",
|
|
13
|
+
"ai",
|
|
14
|
+
"claude",
|
|
15
|
+
"llm",
|
|
16
|
+
"mseep"
|
|
17
|
+
],
|
|
18
|
+
"homepage": "https://github.com/santoshray02/csv-editor#readme",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/santoshray02/csv-editor/issues"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/santoshray02/csv-editor.git"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"author": {
|
|
28
|
+
"name": "Santosh Ray",
|
|
29
|
+
"email": "rayskumar02@gmail.com"
|
|
30
|
+
},
|
|
31
|
+
"type": "module",
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "echo 'Python package - no JS build needed'",
|
|
34
|
+
"prepare": "echo 'Installing Python dependencies...'"
|
|
35
|
+
},
|
|
36
|
+
"mcp": {
|
|
37
|
+
"serverType": "python",
|
|
38
|
+
"command": "python",
|
|
39
|
+
"args": [
|
|
40
|
+
"-m",
|
|
41
|
+
"csv_editor.server"
|
|
42
|
+
],
|
|
43
|
+
"env": {},
|
|
44
|
+
"capabilities": {
|
|
45
|
+
"tools": true,
|
|
46
|
+
"resources": true,
|
|
47
|
+
"prompts": true
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"publisher": "mseep"
|
|
51
|
+
}
|