deva 1.3.2__tar.gz → 1.4.0__tar.gz

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 (39) hide show
  1. {deva-1.3.2 → deva-1.4.0}/PKG-INFO +5 -28
  2. deva-1.4.0/deva/__init__.py +123 -0
  3. {deva-1.3.2 → deva-1.4.0}/deva/admin.py +364 -23
  4. {deva-1.3.2 → deva-1.4.0}/deva/browser.py +6 -39
  5. {deva-1.3.2 → deva-1.4.0}/deva/core.py +580 -229
  6. {deva-1.3.2 → deva-1.4.0}/deva/endpoints.py +63 -1
  7. {deva-1.3.2 → deva-1.4.0}/deva/future.py +28 -3
  8. {deva-1.3.2 → deva-1.4.0}/deva/gpt.py +37 -9
  9. deva-1.4.0/deva/graph.py +358 -0
  10. {deva-1.3.2 → deva-1.4.0}/deva/lambdas.py +33 -0
  11. {deva-1.3.2 → deva-1.4.0}/deva/namespace.py +37 -9
  12. deva-1.4.0/deva/new_bus.py +166 -0
  13. {deva-1.3.2 → deva-1.4.0}/deva/page.py +98 -17
  14. {deva-1.3.2 → deva-1.4.0}/deva/pipe.py +29 -2
  15. {deva-1.3.2 → deva-1.4.0}/deva/sources.py +5 -5
  16. deva-1.4.0/deva/store.py +241 -0
  17. {deva-1.3.2 → deva-1.4.0}/deva/when.py +1 -1
  18. {deva-1.3.2 → deva-1.4.0}/deva.egg-info/PKG-INFO +5 -28
  19. {deva-1.3.2 → deva-1.4.0}/deva.egg-info/SOURCES.txt +1 -0
  20. {deva-1.3.2 → deva-1.4.0}/deva.egg-info/requires.txt +3 -0
  21. {deva-1.3.2 → deva-1.4.0}/setup.py +4 -1
  22. deva-1.3.2/deva/__init__.py +0 -24
  23. deva-1.3.2/deva/graph.py +0 -233
  24. deva-1.3.2/deva/store.py +0 -239
  25. {deva-1.3.2 → deva-1.4.0}/README.rst +0 -0
  26. {deva-1.3.2 → deva-1.4.0}/deva/bus.py +0 -0
  27. {deva-1.3.2 → deva-1.4.0}/deva/compute.py +0 -0
  28. {deva-1.3.2 → deva-1.4.0}/deva/monitor.py +0 -0
  29. {deva-1.3.2 → deva-1.4.0}/deva/search.py +0 -0
  30. {deva-1.3.2 → deva-1.4.0}/deva/topic.py +0 -0
  31. {deva-1.3.2 → deva-1.4.0}/deva/utils/__init__.py +0 -0
  32. {deva-1.3.2 → deva-1.4.0}/deva/utils/simhash.py +0 -0
  33. {deva-1.3.2 → deva-1.4.0}/deva/utils/sqlitedict.py +0 -0
  34. {deva-1.3.2 → deva-1.4.0}/deva/utils/time.py +0 -0
  35. {deva-1.3.2 → deva-1.4.0}/deva/utils/whooshalchemy.py +0 -0
  36. {deva-1.3.2 → deva-1.4.0}/deva.egg-info/dependency_links.txt +0 -0
  37. {deva-1.3.2 → deva-1.4.0}/deva.egg-info/not-zip-safe +0 -0
  38. {deva-1.3.2 → deva-1.4.0}/deva.egg-info/top_level.txt +0 -0
  39. {deva-1.3.2 → deva-1.4.0}/setup.cfg +0 -0
@@ -1,39 +1,14 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: deva
3
- Version: 1.3.2
3
+ Version: 1.4.0
4
4
  Summary: data eval in future
5
5
  Home-page: https://github.com/sostc/deva
6
6
  Author: spark
7
7
  Author-email: zjw0358@gmail.com
8
8
  License: http://www.apache.org/licenses/LICENSE-2.0.html
9
+ Platform: UNKNOWN
9
10
  Requires-Python: >=3.5
10
- Requires-Dist: toolz
11
- Requires-Dist: zict
12
- Requires-Dist: jieba
13
- Requires-Dist: six
14
- Requires-Dist: requests
15
- Requires-Dist: pandas
16
- Requires-Dist: pandas-compat
17
- Requires-Dist: dill
18
- Requires-Dist: Whoosh
19
- Requires-Dist: SQLAlchemy
20
- Requires-Dist: tornado
21
- Requires-Dist: easyquotation
22
- Requires-Dist: pampy
23
- Requires-Dist: pymaybe
24
- Requires-Dist: requests-html
25
- Requires-Dist: aioredis>=2.0
26
- Requires-Dist: apscheduler
27
- Requires-Dist: werkzeug==1.0.0
28
- Requires-Dist: networkx==2
29
- Requires-Dist: graphviz
30
- Requires-Dist: sockjs-tornado>=1.0.7
31
- Requires-Dist: expiringdict
32
- Requires-Dist: aiosmtplib
33
- Requires-Dist: trafilatura
34
- Requires-Dist: newspaper3k
35
- Requires-Dist: boilerpy3
36
- Requires-Dist: sumy
11
+ Provides-Extra: llm
37
12
 
38
13
  .. image:: https://raw.githubusercontent.com/sostc/deva/master/deva.jpeg
39
14
  :target: https://github.com/sostc/deva
@@ -273,3 +248,5 @@ workers
273
248
  when('open', source=bus).then(lambda: print(f'开盘啦'))
274
249
  Deva.run()
275
250
 
251
+
252
+
@@ -0,0 +1,123 @@
1
+ from __future__ import absolute_import, division, print_function
2
+
3
+ from .core import *
4
+ from .compute import *
5
+ from .graph import *
6
+ from .sources import *
7
+ from .namespace import *
8
+ from .when import *
9
+ from .endpoints import *
10
+ from .future import *
11
+
12
+
13
+ from .bus import *
14
+ from .search import IndexStream
15
+ from .pipe import *
16
+ # from .monitor import Monitor
17
+ from .lambdas import _
18
+ from .browser import browser, tab, tabs
19
+ from .core import *
20
+
21
+
22
+ def sync_gpt(prompts):
23
+ from .gpt import sync_gpt as _sync_gpt
24
+ return _sync_gpt(prompts)
25
+
26
+
27
+ async def async_gpt(prompts):
28
+ from .gpt import async_gpt as _async_gpt
29
+ return await _async_gpt(prompts)
30
+
31
+ """
32
+ 流式计算框架 Deva - 构建智能数据管道的核心工具
33
+
34
+ 基于声明式流编程范式,提供高效的数据管道构建与执行能力,特别适用于开发实时监控系统、数据分析系统等事件驱动型应用。核心定位:
35
+
36
+ ■ 流计算范式 - 数据自动流动与级联计算
37
+ ■ 可视化编排 - 支持拖拽式管道设计
38
+ ■ 弹性扩展 - 动态添加/移除处理节点
39
+ ■ 状态管理 - 带状态计算的自动持久化
40
+
41
+ 核心能力架构:
42
+
43
+ 1. 流式编程模型
44
+ - 声明式管道: 通过 >> 操作符构建数据流图,自动建立处理链路
45
+ - 响应式计算: 数据变更自动触发下游计算,支持级联更新
46
+ - 函数式组合: 提供 map/filter/reduce 等操作符链式组合
47
+
48
+ 2. 计算原语体系
49
+ - 内置流类型:
50
+ * DBStream: 时序数据库流(自动维护存储/时间窗口查询)
51
+ * IndexStream: 全文检索流
52
+ * FileLogStream: 文件日志流(滚动存储/实时追踪)
53
+
54
+ 3. 事件驱动应用
55
+ - 监控系统构建:
56
+ sensors >> anomaly_detect >> alert # 异常检测告警
57
+ logs >> pattern_analyze >> dashboard # 日志实时分析
58
+
59
+ - 数据分析系统:
60
+ kafka_source >> realtime_etl >> feature_store >> ml_pipeline
61
+ db_stream.window(300).aggregate() >> report_generator
62
+
63
+ 4. 高效开发实践
64
+ - 流式lambda简化:
65
+ _ * 2 >> log # 自动展开为 lambda x: x*2
66
+ - 异步处理集成:
67
+ async_data | async_db_query | async_emit
68
+ - 可视化调试工具:
69
+ stream.visualize() # 生成流拓扑图
70
+ stream.webview() # Web监控面板
71
+
72
+ 5. 生产级特性
73
+ - 智能背压管理: 自动缓冲控制与流速调节
74
+ - 持久化保障: 重要状态自动持久化到 DBStream
75
+ - 错误恢复: 支持异常流重试与数据重放
76
+ DBStream('events').replay(speed=2) # 2倍速历史回放
77
+ - 资源治理: 连接数/内存/存储的自动管控
78
+
79
+ 典型应用场景:
80
+
81
+ ▌智能监控系统
82
+ - 设备指标实时分析:
83
+ sensors.window(60).mean() >> threshold_check >> alert
84
+ - 日志异常检测:
85
+ log_stream.map(parse) >> detect_errors >> ops_center
86
+
87
+ ▌实时分析管道
88
+ - 流式ETL:
89
+ kafka_source >> clean >> transform >> feature_store
90
+ - 交互式分析:
91
+ (browser.inputs
92
+ >> feature_extract
93
+ >> model.predict
94
+ >> visualize)
95
+
96
+ ▌数据采集系统
97
+ - 智能爬虫:
98
+ BrowserCrawler(urls)
99
+ >> extract_data
100
+ >> DBStream('crawled')
101
+ >> auto_export
102
+ - IoT数据处理:
103
+ device_streams.merge()
104
+ >> deduplicate
105
+ >> time_window_aggregate
106
+
107
+ 技术体系:
108
+
109
+ 数据输入 -> 流计算层 -> 输出系统
110
+ │ │ │
111
+ ├─事件驱动──┼─流水线处理─┼─实时可视化
112
+ ├─消息队列 │ 状态计算 │ 时序数据库
113
+ └─日志文件 └─AI模型集成─┴─API服务
114
+
115
+ 核心优势:
116
+ • 复杂事件处理(CEP)支持: 内置时间窗口/模式匹配等语义
117
+ • 计算存储一体化: 流处理与DBStream深度集成
118
+ • 多范式统一: 兼容同步/异步/批处理混合编程
119
+ • 生产就绪: 内置背压控制/自动扩容/故障恢复机制
120
+ """
121
+
122
+
123
+ __version__ = '1.4.0'
@@ -8,6 +8,9 @@ Deva 管理面板 - 基于 PyWebIO 和 Tornado 的 Web 应用程序
8
8
  - 数据表展示:支持分页和实时更新的表格数据展示
9
9
  - 日志系统:实时日志监控和手动日志写入
10
10
  - 用户认证:基于用户名和密码的登录系统
11
+ - 数据库管理:支持 SQLite 数据库的 CRUD 操作和状态监控
12
+ - 流式处理:实时监控和操作 Deva 数据流
13
+ - 对象检查:支持 Python 对象的详细属性检查
11
14
 
12
15
  主要模块:
13
16
  - 数据流模块:实时监控多个数据流,包括访问日志、新闻、板块数据等
@@ -15,12 +18,32 @@ Deva 管理面板 - 基于 PyWebIO 和 Tornado 的 Web 应用程序
15
18
  - 数据表模块:支持分页、过滤和实时更新的表格展示
16
19
  - 日志模块:提供日志查看器和手动日志写入功能
17
20
  - 用户认证模块:基于 PyWebIO 的 basic_auth 实现
21
+ - 数据库管理模块:提供 SQLite 数据库的 CRUD 操作和状态监控
22
+ - 流式处理模块:支持 Deva 数据流的实时监控和操作
23
+ - 对象检查模块:支持 Python 对象的详细属性检查
18
24
 
19
25
  技术栈:
20
26
  - 前端:PyWebIO
21
27
  - 后端:Tornado
22
28
  - 数据流:Deva 流处理框架
29
+ - 数据库:SQLite
23
30
  - 缓存:基于 ExpiringDict 的缓存系统
31
+ - 异步处理:Tornado 异步框架
32
+ - 持久化存储:基于 DBStream 的时序数据存储
33
+
34
+ 核心特性:
35
+ - 实时性:支持毫秒级数据更新和监控
36
+ - 可扩展性:模块化设计,易于功能扩展
37
+ - 安全性:完善的用户认证机制
38
+ - 易用性:简洁的 API 和直观的 Web 界面
39
+ - 高性能:基于异步 IO 的高效处理能力
40
+ - 持久化:支持数据自动持久化和历史数据回放
41
+
42
+ 典型应用场景:
43
+ - 实时监控系统:设备指标、日志异常等实时监控
44
+ - 数据分析系统:流式 ETL、特征提取、模型预测
45
+ - 数据采集系统:智能爬虫、IoT 数据处理
46
+ - 任务调度系统:定时任务管理和监控
24
47
  """
25
48
 
26
49
 
@@ -32,6 +55,7 @@ import os
32
55
  import traceback
33
56
  import json
34
57
  import time
58
+ import requests
35
59
  from urllib.parse import urljoin
36
60
  from typing import Callable, Union
37
61
 
@@ -54,9 +78,9 @@ from pywebio.output import (
54
78
  )
55
79
  from pywebio.platform.tornado import webio_handler
56
80
  from pywebio_battery import put_logbox, logbox_append, set_localstorage, get_localstorage
57
- from pywebio.pin import pin, put_input
81
+ from pywebio.pin import pin, put_file_upload, put_input
58
82
  from pywebio.session import set_env, run_async, run_js, run_asyncio_coroutine, get_session_implement
59
- from pywebio.input import input, input_group, PASSWORD, textarea, actions, TEXT
83
+ from pywebio.input import input, input_group, PASSWORD, textarea, actions, TEXT, file_upload
60
84
 
61
85
 
62
86
  @timer(5,start=False)
@@ -84,7 +108,7 @@ async def get_gpt_response(prompt, session=None, scope=None, model_type='deepsee
84
108
  """
85
109
  config = NB(model_type)
86
110
  required_configs = ['api_key', 'base_url', 'model']
87
- missing_configs = [config for config in required_configs if config not in config]
111
+ missing_configs = [c for c in required_configs if c not in config]
88
112
  if missing_configs:
89
113
  message = "警告: 在NB配置中缺少以下必要配置项: " + ', '.join(missing_configs) + ". 请确保在其他地方正确设置这些配置项的值。"
90
114
  message >> warn
@@ -105,6 +129,56 @@ async def get_gpt_response(prompt, session=None, scope=None, model_type='deepsee
105
129
  api_key = config.get('api_key')
106
130
  base_url = config.get('base_url')
107
131
  model = config.get('model')
132
+
133
+ async def diagnose_backend_error():
134
+ """探测后端错误详情,返回可读文本。"""
135
+ try:
136
+ url = base_url.rstrip('/') + '/chat/completions'
137
+ payload = {
138
+ "model": model,
139
+ "messages": messages,
140
+ "stream": False,
141
+ "max_tokens": 64,
142
+ }
143
+ headers = {
144
+ "Authorization": f"Bearer {api_key}",
145
+ "Content-Type": "application/json",
146
+ }
147
+ resp = await asyncio.to_thread(
148
+ requests.post,
149
+ url,
150
+ headers=headers,
151
+ data=json.dumps(payload),
152
+ timeout=15,
153
+ )
154
+ text = (resp.text or "").strip()
155
+ try:
156
+ data = resp.json()
157
+ except Exception:
158
+ data = None
159
+
160
+ if isinstance(data, dict):
161
+ code = data.get("code")
162
+ message = data.get("message")
163
+ if code not in (None, 0):
164
+ return f"上游接口错误(code={code}, message={message})"
165
+ if message and not data.get("choices"):
166
+ return f"上游接口返回异常消息(message={message})"
167
+
168
+ return f"上游返回异常响应(status={resp.status_code}, body={text[:300]})"
169
+ except Exception as probe_error:
170
+ return f"上游诊断失败({type(probe_error).__name__}: {probe_error})"
171
+
172
+ def safe_toast(message, color='error'):
173
+ """仅在可用的 PyWebIO 任务上下文中弹出提示,避免后台协程报错。"""
174
+ if not session:
175
+ return
176
+ try:
177
+ toast(message, color=color)
178
+ except RuntimeError as e:
179
+ (f"toast skipped(no task context): {e}") >> log
180
+ except Exception as e:
181
+ (f"toast failed: {e}") >> log
108
182
 
109
183
  gpt_client = AsyncOpenAI(api_key=api_key, base_url=base_url)
110
184
  start_time = time.time()
@@ -127,9 +201,11 @@ async def get_gpt_response(prompt, session=None, scope=None, model_type='deepsee
127
201
  max_tokens=8192
128
202
  )
129
203
  except Exception as e:
130
- (f"请求失败: {traceback.format_exc()}")>>log
131
- toast("请求失败~")
132
- return ""
204
+ backend_error = await diagnose_backend_error()
205
+ (f"请求失败: {traceback.format_exc()} | {backend_error}") >> log
206
+ (f"GPT请求失败(model={model_type}/{model}): {backend_error}") >> warn
207
+ safe_toast("请求失败: " + backend_error, color='error')
208
+ return f"[GPT_ERROR] {backend_error}"
133
209
 
134
210
  # 初始化文本缓冲区
135
211
  buffer = ""
@@ -147,12 +223,17 @@ async def get_gpt_response(prompt, session=None, scope=None, model_type='deepsee
147
223
  返回:
148
224
  tuple: (更新后的buffer, 更新后的accumulated_text, 更新后的start_time)
149
225
  """
150
- if chunk.choices[0].delta.content:
226
+ content = ""
227
+ if getattr(chunk, "choices", None):
228
+ delta = getattr(chunk.choices[0], "delta", None)
229
+ content = getattr(delta, "content", "") or ""
230
+
231
+ if content:
151
232
  # 如果内容以"检索"开头,跳过该行
152
- if chunk.choices[0].delta.content.startswith("检索"):
233
+ if content.startswith("检索"):
153
234
  return buffer, accumulated_text, start_time
154
235
 
155
- buffer += chunk.choices[0].delta.content
236
+ buffer += content
156
237
 
157
238
  # 判断是否到达段落结尾(以句号、问号、感叹号+换行符为标志)
158
239
  paragraph_end_markers = ('.', '?', '!', '。', '?', '!')
@@ -200,20 +281,35 @@ async def get_gpt_response(prompt, session=None, scope=None, model_type='deepsee
200
281
  start_time = time.time()
201
282
 
202
283
  # 处理最后一个未显示的块
203
- if buffer and not chunk.choices[0].delta.content:
284
+ if buffer and not content:
204
285
  accumulated_text += buffer
205
286
  logfunc(buffer)
206
287
  start_time = time.time()
207
288
  buffer = ""
208
289
 
209
290
  return buffer, accumulated_text, start_time
210
- async for chunk in response:
211
- buffer, accumulated_text, start_time = await process_chunk(
212
- chunk, buffer, accumulated_text, start_time
213
- )
214
-
215
- # 返回完整的累计文本
216
- return accumulated_text
291
+ try:
292
+ async for chunk in response:
293
+ buffer, accumulated_text, start_time = await process_chunk(
294
+ chunk, buffer, accumulated_text, start_time
295
+ )
296
+
297
+ # OpenAI 流结束时不保证一定有空 content 结尾块,兜底 flush 剩余缓存
298
+ if buffer.strip():
299
+ accumulated_text += buffer
300
+ logfunc(buffer)
301
+ buffer = ""
302
+
303
+ if not accumulated_text.strip():
304
+ backend_error = await diagnose_backend_error()
305
+ (f"GPT空响应(model={model_type}/{model}): {backend_error}") >> warn
306
+ safe_toast("模型返回空内容: " + backend_error, color='error')
307
+ return f"[GPT_EMPTY] {backend_error}"
308
+
309
+ # 返回完整的累计文本
310
+ return accumulated_text
311
+ finally:
312
+ await gpt_client.close()
217
313
 
218
314
 
219
315
  # tab('http://secsay.com')
@@ -1045,6 +1141,7 @@ async def init_admin_ui(title):
1045
1141
  参数:
1046
1142
  title (str): 页面标题
1047
1143
  """
1144
+ cut_foot()
1048
1145
 
1049
1146
  admin_info = NB('admin')
1050
1147
  if not admin_info.get('username'):
@@ -1059,7 +1156,6 @@ async def init_admin_ui(title):
1059
1156
 
1060
1157
  create_sidebar()
1061
1158
  set_env(title=title)
1062
- cut_foot()
1063
1159
  create_nav_menu()
1064
1160
  put_text(f"Hello, {user_name}. 欢迎光临,恭喜发财")
1065
1161
 
@@ -1497,6 +1593,65 @@ async def document():
1497
1593
  })
1498
1594
  # 显示所有模块的tab
1499
1595
  put_tabs(tabs)
1596
+ def show_dtalk_archive():
1597
+ """显示 Dtalk 消息存档"""
1598
+ with use_scope('dtalk_archive_display', clear=True):
1599
+ # 获取 Dtalk 消息存档
1600
+ dtalk_archive = NB('dtalk_archive')
1601
+
1602
+ if not dtalk_archive:
1603
+ put_text('暂无 Dtalk 消息记录')
1604
+ return
1605
+
1606
+ # 创建消息表格
1607
+ archive_table = [['时间', '消息内容', '操作']]
1608
+
1609
+ # 按时间倒序显示(最新的在前面)
1610
+ for timestamp, message in sorted(dtalk_archive.items(), key=lambda x: float(x[0]), reverse=True):
1611
+ from datetime import datetime
1612
+ readable_time = datetime.fromtimestamp(float(timestamp)).strftime('%Y-%m-%d %H:%M:%S')
1613
+
1614
+ # 截断过长的消息
1615
+ display_message = message[:100] + '...' if len(message) > 100 else message
1616
+
1617
+ # 添加操作按钮
1618
+ actions = put_buttons([
1619
+ {'label': '查看', 'value': 'view'},
1620
+ {'label': '删除', 'value': 'delete'}
1621
+ ], onclick=lambda v, t=timestamp: view_dtalk_message(t, message) if v == 'view' else delete_dtalk_message(t))
1622
+
1623
+ archive_table.append([readable_time, display_message, actions])
1624
+
1625
+ put_table(archive_table)
1626
+
1627
+ # 添加清空所有消息的按钮
1628
+ put_button('清空所有消息', onclick=clear_all_dtalk_messages, color='danger')
1629
+
1630
+ def view_dtalk_message(timestamp, message):
1631
+ """查看完整的 Dtalk 消息"""
1632
+ from datetime import datetime
1633
+ readable_time = datetime.fromtimestamp(float(timestamp)).strftime('%Y-%m-%d %H:%M:%S')
1634
+
1635
+ popup(f'Dtalk 消息 - {readable_time}', [
1636
+ put_markdown(f'**发送时间:** {readable_time}'),
1637
+ put_markdown('**消息内容:**'),
1638
+ put_markdown(message)
1639
+ ], size='large')
1640
+
1641
+ def delete_dtalk_message(timestamp):
1642
+ """删除指定的 Dtalk 消息"""
1643
+ del NB('dtalk_archive')[timestamp]
1644
+ toast('消息已删除', color='success')
1645
+ # 刷新显示
1646
+ show_dtalk_archive()
1647
+
1648
+ def clear_all_dtalk_messages():
1649
+ """清空所有 Dtalk 消息"""
1650
+ NB('dtalk_archive').clear()
1651
+ toast('所有消息已清空', color='success')
1652
+ # 刷新显示
1653
+ show_dtalk_archive()
1654
+
1500
1655
  async def main():
1501
1656
  # await my_timer()
1502
1657
  # 这个将会把会话协程卡在这里不动,采用 run_async则不会堵塞
@@ -1761,6 +1916,11 @@ async def main():
1761
1916
  with put_collapse('其他控件', open=True):
1762
1917
  put_input('write_to_log', type='text', value='', placeholder='手动写入日志')
1763
1918
  put_button('>', onclick=write_to_log)
1919
+
1920
+ # Dtalk 消息存档展示
1921
+ put_markdown('### 📱 Dtalk 消息存档')
1922
+ set_scope('dtalk_archive_display')
1923
+ show_dtalk_archive()
1764
1924
 
1765
1925
 
1766
1926
 
@@ -1925,9 +2085,71 @@ def table_click(tablename):
1925
2085
  }
1926
2086
 
1927
2087
  put_button('新增数据', onclick=lambda: edit_data_popup(categorized_data['strings'],tablename=tablename))
2088
+ async def upload_table_data():
2089
+ # 获取用户输入的key值
2090
+ key = await pin['upload_key']
2091
+ # 获取上传的文件
2092
+ file = await pin['upload_file']
2093
+
2094
+ if not key:
2095
+ toast('请输入key值', color='error')
2096
+ return
2097
+
2098
+ if not file:
2099
+ toast('请选择要上传的文件', color='error')
2100
+ return
2101
+
2102
+ try:
2103
+ # 根据文件扩展名读取文件
2104
+ if file['filename'].endswith('.csv'):
2105
+ # 使用StringIO读取文件内容
2106
+ from io import StringIO
2107
+ content = file['content'].decode('utf-8')
2108
+ df = pd.read_csv(StringIO(content))
2109
+ elif file['filename'].endswith(('.xls', '.xlsx')):
2110
+ # 使用BytesIO读取二进制文件内容
2111
+ from io import BytesIO
2112
+ df = pd.read_excel(BytesIO(file['content']))
2113
+ else:
2114
+ toast('仅支持csv或excel文件', color='error')
2115
+ return
2116
+
2117
+ # 检查是否有列名
2118
+ if df.columns.empty:
2119
+ toast('文件必须包含列名', color='error')
2120
+ return
2121
+
2122
+ # 检查数据是否为空
2123
+ if df.empty:
2124
+ toast('上传的文件不能为空', color='error')
2125
+ return
2126
+
2127
+ # 保存到数据库
2128
+ (key, df) >> NB(tablename)
2129
+ toast('上传成功', color='success')
2130
+ close_popup()
2131
+ # 刷新页面
2132
+ table_click(tablename)
2133
+
2134
+ except pd.errors.EmptyDataError:
2135
+ toast('上传的文件为空或格式不正确', color='error')
2136
+ except pd.errors.ParserError:
2137
+ toast('文件解析失败,请检查文件格式', color='error')
2138
+ except UnicodeDecodeError:
2139
+ toast('文件编码错误,请使用UTF-8编码', color='error')
2140
+ except Exception as e:
2141
+ toast(f'上传失败: {str(e)}', color='error')
2142
+ log(f'上传失败详情: {traceback.format_exc()}') # 记录详细错误日志
2143
+ put_button('上传表格数据', onclick=lambda:
2144
+ popup('上传表格数据', [
2145
+ put_input('upload_key', placeholder='请输入key值'),
2146
+ put_file_upload('upload_file', accept='.csv,.xls,.xlsx', max_size='10M'),
2147
+ put_buttons(['上传', '取消'], onclick=[
2148
+ lambda: run_async(upload_table_data()),
2149
+ close_popup
2150
+ ])
2151
+ ]))
1928
2152
 
1929
-
1930
-
1931
2153
  # 显示字符串类型数据
1932
2154
  if categorized_data['strings']:
1933
2155
  with put_collapse('strings', open=True):
@@ -1972,9 +2194,118 @@ def table_click(tablename):
1972
2194
  if categorized_data['dataframes']:
1973
2195
  with put_collapse('dataframe', open=True):
1974
2196
  for df_name, df in categorized_data['dataframes']:
2197
+ # 将中文df_name转换为拼音
2198
+ if any('\u4e00' <= char <= '\u9fff' for char in df_name):
2199
+ from pypinyin import pinyin, Style
2200
+ scope_name = ''.join([item[0] for item in pinyin(df_name, style=Style.NORMAL)])
2201
+ else:
2202
+ scope_name = df_name
2203
+
1975
2204
  with put_collapse(df_name, open=True):
1976
- paginate_dataframe(scope=df_name, df=df, page_size=10)
1977
-
2205
+ paginate_dataframe(scope=scope_name, df=df, page_size=10)
2206
+ # 添加数据分析按钮
2207
+ with use_scope(f'analysis_{scope_name}'): # 为每个DataFrame创建独立的作用域
2208
+ put_buttons([
2209
+ '描述性统计',
2210
+ '数据透视表',
2211
+ '分组聚合',
2212
+ '缺失值分析'
2213
+ ], onclick=[
2214
+ lambda df=df, scope=scope_name: run_async(show_descriptive_stats(df, scope)),
2215
+ lambda df=df, scope=scope_name: run_async(show_pivot_table(df, scope)),
2216
+ lambda df=df, scope=scope_name: run_async(show_groupby_analysis(df, scope)),
2217
+ lambda df=df, scope=scope_name: run_async(show_missing_values(df, scope))
2218
+ ])
2219
+
2220
+ # 添加分析结果显示区域
2221
+ with use_scope(f'analysis_result_{scope_name}'):
2222
+ pass
2223
+
2224
+ put_button(f'删除 {df_name}', onclick=lambda df_name=df_name: run_async(delete_dataframe(df_name, tablename)))
2225
+
2226
+ # 定义分析函数
2227
+ async def show_descriptive_stats(df, scope):
2228
+ """显示描述性统计"""
2229
+ with use_scope(f'analysis_result_{scope}'):
2230
+ put_markdown('### 描述性统计')
2231
+ stats = df.describe(include='all').T
2232
+ put_table(stats.reset_index().values.tolist())
2233
+
2234
+ async def show_pivot_table(df, scope):
2235
+ """显示数据透视表"""
2236
+ with use_scope(f'analysis_result_{scope}'):
2237
+ put_markdown('### 数据透视表')
2238
+ # 获取所有数值列和分类列
2239
+ numeric_cols = df.select_dtypes(include='number').columns.tolist()
2240
+ category_cols = df.select_dtypes(include='object').columns.tolist()
2241
+
2242
+ if not category_cols or not numeric_cols:
2243
+ toast('需要至少一个分类列和一个数值列', color='error')
2244
+ return
2245
+
2246
+ # 创建交互式输入
2247
+ put_input('pivot_index', placeholder='选择行索引(分类列)')
2248
+ put_input('pivot_columns', placeholder='选择列索引(可选,分类列)')
2249
+ put_input('pivot_values', placeholder='选择聚合值(数值列)')
2250
+ put_buttons(['生成'], onclick=[
2251
+ lambda: run_async(generate_pivot(df, scope))
2252
+ ])
2253
+
2254
+ async def generate_pivot(df, scope):
2255
+ """生成数据透视表"""
2256
+ index = await pin['pivot_index']
2257
+ columns = await pin['pivot_columns'] or None
2258
+ values = await pin['pivot_values']
2259
+
2260
+ try:
2261
+ pivot = df.pivot_table(index=index, columns=columns, values=values, aggfunc='mean')
2262
+ with use_scope(f'analysis_result_{scope}'):
2263
+ put_table(pivot.reset_index().values.tolist())
2264
+ except Exception as e:
2265
+ toast(f'生成数据透视表失败: {str(e)}', color='error')
2266
+
2267
+ async def show_groupby_analysis(df, scope):
2268
+ """显示分组聚合分析"""
2269
+ with use_scope(f'analysis_result_{scope}'):
2270
+ put_markdown('### 分组聚合分析')
2271
+ # 获取所有分类列和数值列
2272
+ group_cols = df.select_dtypes(include='object').columns.tolist()
2273
+ agg_cols = df.select_dtypes(include='number').columns.tolist()
2274
+
2275
+ if not group_cols or not agg_cols:
2276
+ toast('需要至少一个分类列和一个数值列', color='error')
2277
+ return
2278
+
2279
+ # 创建交互式输入
2280
+ put_input('groupby_col', placeholder='选择分组列(分类列)')
2281
+ put_input('agg_col', placeholder='选择聚合列(数值列)')
2282
+ put_buttons(['分析'], onclick=[
2283
+ lambda: run_async(generate_groupby(df, scope))
2284
+ ])
2285
+
2286
+ async def generate_groupby(df, scope):
2287
+ """生成分组聚合结果"""
2288
+ group_col = await pin['groupby_col']
2289
+ agg_col = await pin['agg_col']
2290
+
2291
+ try:
2292
+ grouped = df.groupby(group_col)[agg_col].agg(['mean', 'sum', 'count'])
2293
+ with use_scope(f'analysis_result_{scope}'):
2294
+ put_table(grouped.reset_index().values.tolist())
2295
+ except Exception as e:
2296
+ toast(f'分组聚合失败: {str(e)}', color='error')
2297
+
2298
+ async def show_missing_values(df, scope):
2299
+ """显示缺失值分析"""
2300
+ with use_scope(f'analysis_result_{scope}'):
2301
+ put_markdown('### 缺失值分析')
2302
+ missing = df.isnull().sum()
2303
+ missing_pct = (missing / len(df)) * 100
2304
+ missing_df = pd.DataFrame({
2305
+ '缺失值数量': missing,
2306
+ '缺失值比例(%)': missing_pct
2307
+ })
2308
+ put_table(missing_df.reset_index().values.tolist())
1978
2309
  # 显示时间序列数据
1979
2310
  if categorized_data['timeseries']:
1980
2311
  with put_collapse('时间序列数据', open=True):
@@ -2010,6 +2341,17 @@ async def save_string(key,data,tablename):
2010
2341
  close_popup()
2011
2342
  # 重新打开编辑popup以刷新内容
2012
2343
  edit_data_popup(data,tablename=tablename)
2344
+ # 删除DataFrame的回调函数
2345
+ async def delete_dataframe(df_name, tablename):
2346
+ """删除指定的DataFrame"""
2347
+ try:
2348
+ del NB(tablename)[df_name]
2349
+ toast(f'已删除DataFrame: {df_name}', color='success')
2350
+ # 刷新显示
2351
+ table_click(tablename)
2352
+ except Exception as e:
2353
+ toast(f'删除失败: {str(e)}', color='error')
2354
+
2013
2355
  # 删除键值对的回调函数
2014
2356
  async def delete_string(key,data,tablename):
2015
2357
  # 删除数据
@@ -2201,4 +2543,3 @@ if __name__ == '__main__':
2201
2543
 
2202
2544
 
2203
2545
  Deva.run()
2204
-