variable-explorer 0.1.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 (49) hide show
  1. variable_explorer/__init__.py +7 -0
  2. variable_explorer/_version.py +4 -0
  3. variable_explorer/kernel/__init__.py +5 -0
  4. variable_explorer/kernel/comm_handler.py +255 -0
  5. variable_explorer/kernel/data_provider.py +235 -0
  6. variable_explorer/kernel/editor.py +88 -0
  7. variable_explorer/kernel/introspection.py +186 -0
  8. variable_explorer/kernel/serialization.py +73 -0
  9. variable_explorer/kernel/sorter.py +34 -0
  10. variable_explorer/kernel/statistics.py +101 -0
  11. variable_explorer/labextension/build_log.json +726 -0
  12. variable_explorer/labextension/package.json +96 -0
  13. variable_explorer/labextension/schemas/variable-explorer/package.json.orig +91 -0
  14. variable_explorer/labextension/schemas/variable-explorer/plugin.json +75 -0
  15. variable_explorer/labextension/static/lib_index_js.88a2cd3be0f2bf49f0eb.js +1417 -0
  16. variable_explorer/labextension/static/lib_index_js.88a2cd3be0f2bf49f0eb.js.map +1 -0
  17. variable_explorer/labextension/static/remoteEntry.a8ed3dcc7548f0b68f93.js +576 -0
  18. variable_explorer/labextension/static/remoteEntry.a8ed3dcc7548f0b68f93.js.map +1 -0
  19. variable_explorer/labextension/static/style.js +4 -0
  20. variable_explorer/labextension/static/style_index_js-data_font_woff2_charset_utf-8_base64_d09GMgABAAAAABmsAAsAAAAANbQAABlcAAEAAAAAA-5c9677.c69a59632d259bde8f84.js +785 -0
  21. variable_explorer/labextension/static/style_index_js-data_font_woff2_charset_utf-8_base64_d09GMgABAAAAABmsAAsAAAAANbQAABlcAAEAAAAAA-5c9677.c69a59632d259bde8f84.js.map +1 -0
  22. variable_explorer/labextension/static/vendors-node_modules_ag-grid-community_dist_package_main_esm_mjs.c38425b170e91e5db052.js +50347 -0
  23. variable_explorer/labextension/static/vendors-node_modules_ag-grid-community_dist_package_main_esm_mjs.c38425b170e91e5db052.js.map +1 -0
  24. variable_explorer/labextension/static/vendors-node_modules_ag-grid-community_styles_ag-grid_css-node_modules_ag-grid-community_styl-7d25f0.7424d30423d9f1c112f6.js +8124 -0
  25. variable_explorer/labextension/static/vendors-node_modules_ag-grid-community_styles_ag-grid_css-node_modules_ag-grid-community_styl-7d25f0.7424d30423d9f1c112f6.js.map +1 -0
  26. variable_explorer/labextension/static/vendors-node_modules_ag-grid-react_dist_package_index_esm_mjs.ca52d36c364e6562240a.js +2917 -0
  27. variable_explorer/labextension/static/vendors-node_modules_ag-grid-react_dist_package_index_esm_mjs.ca52d36c364e6562240a.js.map +1 -0
  28. variable_explorer-0.1.0.data/data/share/jupyter/labextensions/variable-explorer/build_log.json +726 -0
  29. variable_explorer-0.1.0.data/data/share/jupyter/labextensions/variable-explorer/install.json +5 -0
  30. variable_explorer-0.1.0.data/data/share/jupyter/labextensions/variable-explorer/package.json +96 -0
  31. variable_explorer-0.1.0.data/data/share/jupyter/labextensions/variable-explorer/schemas/variable-explorer/package.json.orig +91 -0
  32. variable_explorer-0.1.0.data/data/share/jupyter/labextensions/variable-explorer/schemas/variable-explorer/plugin.json +75 -0
  33. variable_explorer-0.1.0.data/data/share/jupyter/labextensions/variable-explorer/static/lib_index_js.88a2cd3be0f2bf49f0eb.js +1417 -0
  34. variable_explorer-0.1.0.data/data/share/jupyter/labextensions/variable-explorer/static/lib_index_js.88a2cd3be0f2bf49f0eb.js.map +1 -0
  35. variable_explorer-0.1.0.data/data/share/jupyter/labextensions/variable-explorer/static/remoteEntry.a8ed3dcc7548f0b68f93.js +576 -0
  36. variable_explorer-0.1.0.data/data/share/jupyter/labextensions/variable-explorer/static/remoteEntry.a8ed3dcc7548f0b68f93.js.map +1 -0
  37. variable_explorer-0.1.0.data/data/share/jupyter/labextensions/variable-explorer/static/style.js +4 -0
  38. variable_explorer-0.1.0.data/data/share/jupyter/labextensions/variable-explorer/static/style_index_js-data_font_woff2_charset_utf-8_base64_d09GMgABAAAAABmsAAsAAAAANbQAABlcAAEAAAAAA-5c9677.c69a59632d259bde8f84.js +785 -0
  39. variable_explorer-0.1.0.data/data/share/jupyter/labextensions/variable-explorer/static/style_index_js-data_font_woff2_charset_utf-8_base64_d09GMgABAAAAABmsAAsAAAAANbQAABlcAAEAAAAAA-5c9677.c69a59632d259bde8f84.js.map +1 -0
  40. variable_explorer-0.1.0.data/data/share/jupyter/labextensions/variable-explorer/static/vendors-node_modules_ag-grid-community_dist_package_main_esm_mjs.c38425b170e91e5db052.js +50347 -0
  41. variable_explorer-0.1.0.data/data/share/jupyter/labextensions/variable-explorer/static/vendors-node_modules_ag-grid-community_dist_package_main_esm_mjs.c38425b170e91e5db052.js.map +1 -0
  42. variable_explorer-0.1.0.data/data/share/jupyter/labextensions/variable-explorer/static/vendors-node_modules_ag-grid-community_styles_ag-grid_css-node_modules_ag-grid-community_styl-7d25f0.7424d30423d9f1c112f6.js +8124 -0
  43. variable_explorer-0.1.0.data/data/share/jupyter/labextensions/variable-explorer/static/vendors-node_modules_ag-grid-community_styles_ag-grid_css-node_modules_ag-grid-community_styl-7d25f0.7424d30423d9f1c112f6.js.map +1 -0
  44. variable_explorer-0.1.0.data/data/share/jupyter/labextensions/variable-explorer/static/vendors-node_modules_ag-grid-react_dist_package_index_esm_mjs.ca52d36c364e6562240a.js +2917 -0
  45. variable_explorer-0.1.0.data/data/share/jupyter/labextensions/variable-explorer/static/vendors-node_modules_ag-grid-react_dist_package_index_esm_mjs.ca52d36c364e6562240a.js.map +1 -0
  46. variable_explorer-0.1.0.dist-info/METADATA +80 -0
  47. variable_explorer-0.1.0.dist-info/RECORD +49 -0
  48. variable_explorer-0.1.0.dist-info/WHEEL +4 -0
  49. variable_explorer-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,7 @@
1
+ """Variable Explorer - A Spyder/Stata-grade variable explorer for JupyterLab."""
2
+
3
+ from ._version import __version__
4
+
5
+
6
+ def _jupyter_labextension_paths():
7
+ return [{"src": "labextension", "dest": "variable-explorer"}]
@@ -0,0 +1,4 @@
1
+ # This file is auto-generated by Hatchling. As such, do not:
2
+ # - modify
3
+ # - track in version control e.g. be sure to add to .gitignore
4
+ __version__ = VERSION = '0.1.0'
@@ -0,0 +1,5 @@
1
+ """Kernel-side handlers for variable exploration."""
2
+
3
+ from .comm_handler import init_comm
4
+
5
+ __all__ = ["init_comm"]
@@ -0,0 +1,255 @@
1
+ """Comm target handler for variable exploration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import traceback
7
+ from typing import Any
8
+
9
+ from .introspection import get_variable_list
10
+ from .data_provider import get_data_page
11
+ from .statistics import compute_column_stats
12
+ from .editor import edit_cell
13
+ from .serialization import safe_serialize
14
+
15
+
16
+ _active_comm = None
17
+ _initialized = False
18
+
19
+
20
+ def init_comm():
21
+ """Register the comm target and post_execute hook. Called from frontend."""
22
+ global _initialized
23
+
24
+ try:
25
+ from ipykernel.comm import Comm
26
+ ip = _get_ipython()
27
+ if ip is None:
28
+ return
29
+
30
+ # Register comm target
31
+ ip.kernel.comm_manager.register_target(
32
+ 'variable_explorer', _on_comm_open
33
+ )
34
+
35
+ # Register post_execute hook for auto-refresh
36
+ if not _initialized:
37
+ ip.events.register('post_execute', _on_post_execute)
38
+ _initialized = True
39
+
40
+ except Exception as e:
41
+ print(f"Variable Explorer: init_comm error: {e}")
42
+
43
+
44
+ def _get_ipython():
45
+ """Get the current IPython instance."""
46
+ try:
47
+ from IPython import get_ipython
48
+ return get_ipython()
49
+ except ImportError:
50
+ return None
51
+
52
+
53
+ def _get_user_ns() -> dict:
54
+ """Get the user namespace."""
55
+ ip = _get_ipython()
56
+ if ip is None:
57
+ return {}
58
+ return ip.user_ns
59
+
60
+
61
+ def _on_comm_open(comm, open_msg):
62
+ """Handle comm open from frontend."""
63
+ global _active_comm
64
+ _active_comm = comm
65
+ comm.on_msg(_on_comm_msg)
66
+ comm.on_close(_on_comm_close)
67
+
68
+ # Send initial variable list
69
+ _send_variable_list(comm)
70
+
71
+
72
+ def _on_comm_close(msg):
73
+ """Handle comm close."""
74
+ global _active_comm
75
+ _active_comm = None
76
+
77
+
78
+ def _on_comm_msg(msg):
79
+ """Route incoming messages from frontend."""
80
+ global _active_comm
81
+ if _active_comm is None:
82
+ return
83
+
84
+ try:
85
+ data = msg['content']['data']
86
+ msg_type = data.get('type', '')
87
+
88
+ handlers = {
89
+ 'refresh': _handle_refresh,
90
+ 'get_data': _handle_get_data,
91
+ 'get_stats': _handle_get_stats,
92
+ 'get_properties': _handle_get_properties,
93
+ 'edit_cell': _handle_edit_cell,
94
+ }
95
+
96
+ handler = handlers.get(msg_type)
97
+ if handler:
98
+ handler(data)
99
+ else:
100
+ _send_error(f"Unknown message type: {msg_type}", msg_type)
101
+
102
+ except Exception as e:
103
+ _send_error(f"Handler error: {e}\n{traceback.format_exc()}", 'handler')
104
+
105
+
106
+ def _handle_refresh(data: dict):
107
+ """Refresh the variable list."""
108
+ _send_variable_list(_active_comm)
109
+
110
+
111
+ def _handle_get_data(data: dict):
112
+ """Get paginated data for a variable."""
113
+ user_ns = _get_user_ns()
114
+ var_name = data.get('variable', '')
115
+ start_row = data.get('startRow', 0)
116
+ end_row = data.get('endRow', 1000)
117
+ sort_model = data.get('sortModel')
118
+
119
+ if var_name not in user_ns:
120
+ _send_error(f"Variable '{var_name}' not found", 'get_data')
121
+ return
122
+
123
+ child_key = data.get('childKey')
124
+ result = get_data_page(var_name, user_ns[var_name], start_row, end_row, sort_model, child_key)
125
+ _send(_active_comm, result)
126
+
127
+
128
+ def _handle_get_stats(data: dict):
129
+ """Get column statistics for a variable."""
130
+ user_ns = _get_user_ns()
131
+ var_name = data.get('variable', '')
132
+
133
+ if var_name not in user_ns:
134
+ _send_error(f"Variable '{var_name}' not found", 'get_stats')
135
+ return
136
+
137
+ result = compute_column_stats(var_name, user_ns[var_name])
138
+ _send(_active_comm, result)
139
+
140
+
141
+ def _handle_get_properties(data: dict):
142
+ """Get dataset-level properties."""
143
+ user_ns = _get_user_ns()
144
+ var_name = data.get('variable', '')
145
+
146
+ if var_name not in user_ns:
147
+ _send_error(f"Variable '{var_name}' not found", 'get_properties')
148
+ return
149
+
150
+ obj = user_ns[var_name]
151
+ result = _get_properties(var_name, obj)
152
+ _send(_active_comm, result)
153
+
154
+
155
+ def _handle_edit_cell(data: dict):
156
+ """Edit a cell value."""
157
+ user_ns = _get_user_ns()
158
+ var_name = data.get('variable', '')
159
+ row_index = data.get('rowIndex', 0)
160
+ column = data.get('column', '')
161
+ new_value = data.get('newValue', '')
162
+
163
+ if var_name not in user_ns:
164
+ _send_error(f"Variable '{var_name}' not found", 'edit_cell')
165
+ return
166
+
167
+ result = edit_cell(user_ns[var_name], row_index, column, new_value)
168
+ result['variable'] = var_name
169
+ _send(_active_comm, result)
170
+
171
+
172
+ def _get_properties(var_name: str, obj: Any) -> dict:
173
+ """Extract dataset-level properties."""
174
+ import sys
175
+
176
+ result: dict[str, Any] = {
177
+ 'type': 'properties',
178
+ 'variable': var_name,
179
+ 'shape': [],
180
+ 'memoryBytes': 0,
181
+ 'dtypes': {},
182
+ 'indexName': '',
183
+ 'indexDtype': '',
184
+ 'sourceFile': None,
185
+ }
186
+
187
+ try:
188
+ import pandas as pd
189
+ if isinstance(obj, pd.DataFrame):
190
+ result['shape'] = list(obj.shape)
191
+ result['memoryBytes'] = int(obj.memory_usage(deep=True).sum())
192
+ result['dtypes'] = {col: str(dtype) for col, dtype in obj.dtypes.items()}
193
+ result['indexName'] = str(obj.index.name or 'RangeIndex')
194
+ result['indexDtype'] = str(obj.index.dtype)
195
+ result['sourceFile'] = obj.attrs.get('source_file')
196
+ return result
197
+ except ImportError:
198
+ pass
199
+
200
+ # Generic fallback
201
+ if hasattr(obj, 'shape'):
202
+ result['shape'] = list(obj.shape)
203
+ if hasattr(obj, '__len__'):
204
+ if not result['shape']:
205
+ result['shape'] = [len(obj)]
206
+
207
+ result['memoryBytes'] = sys.getsizeof(obj)
208
+ return result
209
+
210
+
211
+ def _send_variable_list(comm):
212
+ """Send the current variable list via comm."""
213
+ if comm is None:
214
+ return
215
+ user_ns = _get_user_ns()
216
+ variables = get_variable_list(user_ns)
217
+ _send(comm, {
218
+ 'type': 'variable_list',
219
+ 'variables': variables
220
+ })
221
+
222
+
223
+ def _send_error(message: str, request_type: str = ''):
224
+ """Send an error message to frontend."""
225
+ if _active_comm is None:
226
+ return
227
+ _send(_active_comm, {
228
+ 'type': 'error',
229
+ 'message': message,
230
+ 'requestType': request_type
231
+ })
232
+
233
+
234
+ def _send(comm, data: dict):
235
+ """Send data through comm with safe serialization."""
236
+ try:
237
+ serialized = safe_serialize(data)
238
+ comm.send(serialized)
239
+ except Exception as e:
240
+ try:
241
+ comm.send({
242
+ 'type': 'error',
243
+ 'message': f'Serialization error: {e}'
244
+ })
245
+ except Exception:
246
+ pass
247
+
248
+
249
+ def _on_post_execute():
250
+ """Called after every cell execution — send updated variable list."""
251
+ if _active_comm is not None:
252
+ try:
253
+ _send_variable_list(_active_comm)
254
+ except Exception:
255
+ pass
@@ -0,0 +1,235 @@
1
+ """Paginated data extraction from DataFrame-like objects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from .sorter import apply_sort
8
+
9
+
10
+ def get_data_page(
11
+ var_name: str,
12
+ obj: Any,
13
+ start_row: int,
14
+ end_row: int,
15
+ sort_model: list[dict] | None = None,
16
+ child_key: str | None = None
17
+ ) -> dict:
18
+ """Extract a page of data from a DataFrame-like object.
19
+
20
+ child_key: for containers (list/dict of DataFrames), which child to show.
21
+ e.g. "0" for list index, or "my_key" for dict key.
22
+ """
23
+ import pandas as pd
24
+
25
+ # If accessing a child of a container
26
+ if child_key is not None:
27
+ obj = _get_child(obj, child_key)
28
+ if obj is None:
29
+ return {
30
+ 'type': 'error',
31
+ 'message': f"Child '{child_key}' not found",
32
+ 'requestType': 'get_data'
33
+ }
34
+
35
+ # Convert to DataFrame if needed
36
+ df = _to_dataframe(obj)
37
+ if df is None:
38
+ return {
39
+ 'type': 'error',
40
+ 'message': f"Cannot convert {type(obj).__name__} to tabular format",
41
+ 'requestType': 'get_data'
42
+ }
43
+
44
+ total_rows = len(df)
45
+
46
+ if sort_model:
47
+ df = apply_sort(df, sort_model)
48
+
49
+ page = df.iloc[start_row:end_row]
50
+ columns = _get_column_defs(df)
51
+ rows = _serialize_rows(page, start_row)
52
+
53
+ return {
54
+ 'type': 'data_page',
55
+ 'variable': var_name,
56
+ 'startRow': start_row,
57
+ 'totalRows': total_rows,
58
+ 'columns': columns,
59
+ 'rows': rows,
60
+ }
61
+
62
+
63
+ def _get_child(obj: Any, key: str) -> Any:
64
+ """Access a child element from a container."""
65
+ if isinstance(obj, dict):
66
+ return obj.get(key)
67
+ if isinstance(obj, (list, tuple)):
68
+ try:
69
+ return obj[int(key)]
70
+ except (ValueError, IndexError):
71
+ return None
72
+ return None
73
+
74
+
75
+ def _to_dataframe(obj: Any):
76
+ """Convert various types to pandas DataFrame."""
77
+ try:
78
+ import pandas as pd
79
+
80
+ if isinstance(obj, pd.DataFrame):
81
+ return obj
82
+
83
+ if isinstance(obj, pd.Series):
84
+ return obj.to_frame()
85
+
86
+ # numpy array
87
+ try:
88
+ import numpy as np
89
+ if isinstance(obj, np.ndarray):
90
+ if obj.ndim == 1:
91
+ return pd.DataFrame({'values': obj})
92
+ elif obj.ndim == 2:
93
+ return pd.DataFrame(obj, columns=[f'col_{i}' for i in range(obj.shape[1])])
94
+ except ImportError:
95
+ pass
96
+
97
+ # Dict types
98
+ if isinstance(obj, dict):
99
+ values = list(obj.values())
100
+ if not values:
101
+ return pd.DataFrame()
102
+
103
+ # Dict of lists → standard conversion
104
+ if all(isinstance(v, (list, tuple)) for v in values):
105
+ return pd.DataFrame(obj)
106
+
107
+ # Dict of dicts → each key becomes a row
108
+ if all(isinstance(v, dict) for v in values):
109
+ return pd.DataFrame.from_dict(obj, orient='index')
110
+
111
+ # Dict of DataFrames → show summary table
112
+ if all(isinstance(v, pd.DataFrame) for v in values):
113
+ rows = []
114
+ for k, v in obj.items():
115
+ rows.append({
116
+ 'key': str(k),
117
+ 'type': 'DataFrame',
118
+ 'rows': len(v),
119
+ 'columns': len(v.columns),
120
+ 'memory': f"{v.memory_usage(deep=True).sum():,.0f} B",
121
+ 'column_names': ', '.join(str(c) for c in v.columns[:10]),
122
+ })
123
+ return pd.DataFrame(rows)
124
+
125
+ # Dict with scalar values → single-row or key-value table
126
+ try:
127
+ return pd.DataFrame([obj])
128
+ except Exception:
129
+ return pd.DataFrame({
130
+ 'key': [str(k) for k in obj.keys()],
131
+ 'value': [str(v) for v in obj.values()],
132
+ 'type': [type(v).__name__ for v in obj.values()]
133
+ })
134
+
135
+ # List types
136
+ if isinstance(obj, (list, tuple)):
137
+ if not obj:
138
+ return pd.DataFrame()
139
+
140
+ # List of DataFrames → show summary table
141
+ if all(isinstance(v, pd.DataFrame) for v in obj):
142
+ rows = []
143
+ for i, v in enumerate(obj):
144
+ rows.append({
145
+ 'index': i,
146
+ 'type': 'DataFrame',
147
+ 'rows': len(v),
148
+ 'columns': len(v.columns),
149
+ 'memory': f"{v.memory_usage(deep=True).sum():,.0f} B",
150
+ 'column_names': ', '.join(str(c) for c in v.columns[:10]),
151
+ })
152
+ return pd.DataFrame(rows)
153
+
154
+ # List of dicts → standard conversion (JSON-like data)
155
+ if all(isinstance(v, dict) for v in obj):
156
+ return pd.DataFrame(obj)
157
+
158
+ # List of lists/tuples → tabular
159
+ if all(isinstance(v, (list, tuple)) for v in obj):
160
+ max_cols = max(len(v) for v in obj)
161
+ cols = [f'col_{i}' for i in range(max_cols)]
162
+ # Pad shorter rows with None
163
+ padded = [list(v) + [None] * (max_cols - len(v)) for v in obj]
164
+ return pd.DataFrame(padded, columns=cols)
165
+
166
+ # Simple list of scalars → single column
167
+ return pd.DataFrame({'value': obj})
168
+
169
+ return None
170
+
171
+ except Exception:
172
+ return None
173
+
174
+
175
+ def _get_column_defs(df) -> list[dict]:
176
+ """Build column definitions from a DataFrame."""
177
+ import pandas as pd
178
+
179
+ columns = []
180
+ for col in df.columns:
181
+ dtype = df[col].dtype
182
+ columns.append({
183
+ 'name': str(col),
184
+ 'dtype': str(dtype),
185
+ 'isNumeric': pd.api.types.is_numeric_dtype(dtype) and not pd.api.types.is_bool_dtype(dtype),
186
+ 'isBool': pd.api.types.is_bool_dtype(dtype),
187
+ 'isDatetime': pd.api.types.is_datetime64_any_dtype(dtype),
188
+ })
189
+ return columns
190
+
191
+
192
+ def _serialize_rows(page, start_row: int) -> list[dict]:
193
+ """Serialize DataFrame rows to list of dicts."""
194
+ rows = []
195
+ for idx, (_, row) in enumerate(page.iterrows()):
196
+ record: dict[str, Any] = {'__row_index__': start_row + idx}
197
+ for col, val in row.items():
198
+ record[str(col)] = _serialize_value(val)
199
+ rows.append(record)
200
+ return rows
201
+
202
+
203
+ def _serialize_value(val: Any) -> Any:
204
+ """Convert a single value to JSON-safe type."""
205
+ if val is None:
206
+ return None
207
+
208
+ import pandas as pd
209
+ import numpy as np
210
+
211
+ if pd.isna(val):
212
+ return None
213
+
214
+ if isinstance(val, (np.integer,)):
215
+ return int(val)
216
+ if isinstance(val, (np.floating,)):
217
+ v = float(val)
218
+ if np.isnan(v) or np.isinf(v):
219
+ return None
220
+ return v
221
+ if isinstance(val, (np.bool_,)):
222
+ return bool(val)
223
+
224
+ if isinstance(val, pd.Timestamp):
225
+ return val.isoformat()
226
+ if hasattr(val, 'isoformat'):
227
+ return val.isoformat()
228
+
229
+ if isinstance(val, bytes):
230
+ return val.decode('utf-8', errors='replace')
231
+
232
+ if isinstance(val, str) and len(val) > 500:
233
+ return val[:500] + '...'
234
+
235
+ return val
@@ -0,0 +1,88 @@
1
+ """Cell value editing — write back to the DataFrame in the kernel."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ def edit_cell(df: Any, row_index: int, column: str, new_value: str) -> dict:
9
+ """Modify a cell value in a DataFrame.
10
+
11
+ Returns a result dict with success/error info.
12
+ """
13
+ try:
14
+ import pandas as pd
15
+
16
+ if not isinstance(df, pd.DataFrame):
17
+ return {
18
+ 'type': 'edit_result',
19
+ 'success': False,
20
+ 'rowIndex': row_index,
21
+ 'column': column,
22
+ 'error': 'Only DataFrame editing is supported'
23
+ }
24
+
25
+ if column not in df.columns:
26
+ return {
27
+ 'type': 'edit_result',
28
+ 'success': False,
29
+ 'rowIndex': row_index,
30
+ 'column': column,
31
+ 'error': f"Column '{column}' not found"
32
+ }
33
+
34
+ # Convert value to match column dtype
35
+ dtype = df[column].dtype
36
+ converted = _convert_value(new_value, dtype)
37
+
38
+ # Apply the edit
39
+ df.iat[row_index, df.columns.get_loc(column)] = converted
40
+
41
+ return {
42
+ 'type': 'edit_result',
43
+ 'success': True,
44
+ 'rowIndex': row_index,
45
+ 'column': column,
46
+ }
47
+
48
+ except Exception as e:
49
+ return {
50
+ 'type': 'edit_result',
51
+ 'success': False,
52
+ 'rowIndex': row_index,
53
+ 'column': column,
54
+ 'error': str(e)
55
+ }
56
+
57
+
58
+ def _convert_value(value_str: str, dtype: Any) -> Any:
59
+ """Convert a string value to the appropriate dtype."""
60
+ import pandas as pd
61
+ import numpy as np
62
+
63
+ # Handle empty/null
64
+ if value_str in ('', 'null', 'None', 'NaN', 'nan', 'NA'):
65
+ if pd.api.types.is_numeric_dtype(dtype):
66
+ return np.nan
67
+ if pd.api.types.is_bool_dtype(dtype):
68
+ return pd.NA
69
+ return None
70
+
71
+ # Bool
72
+ if pd.api.types.is_bool_dtype(dtype):
73
+ return value_str.lower() in ('true', '1', 'yes')
74
+
75
+ # Integer
76
+ if pd.api.types.is_integer_dtype(dtype):
77
+ return dtype.type(int(float(value_str)))
78
+
79
+ # Float
80
+ if pd.api.types.is_float_dtype(dtype):
81
+ return dtype.type(float(value_str))
82
+
83
+ # Datetime
84
+ if pd.api.types.is_datetime64_any_dtype(dtype):
85
+ return pd.Timestamp(value_str)
86
+
87
+ # String/object
88
+ return value_str