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.
- variable_explorer/__init__.py +7 -0
- variable_explorer/_version.py +4 -0
- variable_explorer/kernel/__init__.py +5 -0
- variable_explorer/kernel/comm_handler.py +255 -0
- variable_explorer/kernel/data_provider.py +235 -0
- variable_explorer/kernel/editor.py +88 -0
- variable_explorer/kernel/introspection.py +186 -0
- variable_explorer/kernel/serialization.py +73 -0
- variable_explorer/kernel/sorter.py +34 -0
- variable_explorer/kernel/statistics.py +101 -0
- variable_explorer/labextension/build_log.json +726 -0
- variable_explorer/labextension/package.json +96 -0
- variable_explorer/labextension/schemas/variable-explorer/package.json.orig +91 -0
- variable_explorer/labextension/schemas/variable-explorer/plugin.json +75 -0
- variable_explorer/labextension/static/lib_index_js.88a2cd3be0f2bf49f0eb.js +1417 -0
- variable_explorer/labextension/static/lib_index_js.88a2cd3be0f2bf49f0eb.js.map +1 -0
- variable_explorer/labextension/static/remoteEntry.a8ed3dcc7548f0b68f93.js +576 -0
- variable_explorer/labextension/static/remoteEntry.a8ed3dcc7548f0b68f93.js.map +1 -0
- variable_explorer/labextension/static/style.js +4 -0
- variable_explorer/labextension/static/style_index_js-data_font_woff2_charset_utf-8_base64_d09GMgABAAAAABmsAAsAAAAANbQAABlcAAEAAAAAA-5c9677.c69a59632d259bde8f84.js +785 -0
- variable_explorer/labextension/static/style_index_js-data_font_woff2_charset_utf-8_base64_d09GMgABAAAAABmsAAsAAAAANbQAABlcAAEAAAAAA-5c9677.c69a59632d259bde8f84.js.map +1 -0
- variable_explorer/labextension/static/vendors-node_modules_ag-grid-community_dist_package_main_esm_mjs.c38425b170e91e5db052.js +50347 -0
- variable_explorer/labextension/static/vendors-node_modules_ag-grid-community_dist_package_main_esm_mjs.c38425b170e91e5db052.js.map +1 -0
- 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
- 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
- variable_explorer/labextension/static/vendors-node_modules_ag-grid-react_dist_package_index_esm_mjs.ca52d36c364e6562240a.js +2917 -0
- variable_explorer/labextension/static/vendors-node_modules_ag-grid-react_dist_package_index_esm_mjs.ca52d36c364e6562240a.js.map +1 -0
- variable_explorer-0.1.0.data/data/share/jupyter/labextensions/variable-explorer/build_log.json +726 -0
- variable_explorer-0.1.0.data/data/share/jupyter/labextensions/variable-explorer/install.json +5 -0
- variable_explorer-0.1.0.data/data/share/jupyter/labextensions/variable-explorer/package.json +96 -0
- variable_explorer-0.1.0.data/data/share/jupyter/labextensions/variable-explorer/schemas/variable-explorer/package.json.orig +91 -0
- variable_explorer-0.1.0.data/data/share/jupyter/labextensions/variable-explorer/schemas/variable-explorer/plugin.json +75 -0
- variable_explorer-0.1.0.data/data/share/jupyter/labextensions/variable-explorer/static/lib_index_js.88a2cd3be0f2bf49f0eb.js +1417 -0
- variable_explorer-0.1.0.data/data/share/jupyter/labextensions/variable-explorer/static/lib_index_js.88a2cd3be0f2bf49f0eb.js.map +1 -0
- variable_explorer-0.1.0.data/data/share/jupyter/labextensions/variable-explorer/static/remoteEntry.a8ed3dcc7548f0b68f93.js +576 -0
- variable_explorer-0.1.0.data/data/share/jupyter/labextensions/variable-explorer/static/remoteEntry.a8ed3dcc7548f0b68f93.js.map +1 -0
- variable_explorer-0.1.0.data/data/share/jupyter/labextensions/variable-explorer/static/style.js +4 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- variable_explorer-0.1.0.dist-info/METADATA +80 -0
- variable_explorer-0.1.0.dist-info/RECORD +49 -0
- variable_explorer-0.1.0.dist-info/WHEEL +4 -0
- variable_explorer-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -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
|