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.
- {deva-1.3.2 → deva-1.4.0}/PKG-INFO +5 -28
- deva-1.4.0/deva/__init__.py +123 -0
- {deva-1.3.2 → deva-1.4.0}/deva/admin.py +364 -23
- {deva-1.3.2 → deva-1.4.0}/deva/browser.py +6 -39
- {deva-1.3.2 → deva-1.4.0}/deva/core.py +580 -229
- {deva-1.3.2 → deva-1.4.0}/deva/endpoints.py +63 -1
- {deva-1.3.2 → deva-1.4.0}/deva/future.py +28 -3
- {deva-1.3.2 → deva-1.4.0}/deva/gpt.py +37 -9
- deva-1.4.0/deva/graph.py +358 -0
- {deva-1.3.2 → deva-1.4.0}/deva/lambdas.py +33 -0
- {deva-1.3.2 → deva-1.4.0}/deva/namespace.py +37 -9
- deva-1.4.0/deva/new_bus.py +166 -0
- {deva-1.3.2 → deva-1.4.0}/deva/page.py +98 -17
- {deva-1.3.2 → deva-1.4.0}/deva/pipe.py +29 -2
- {deva-1.3.2 → deva-1.4.0}/deva/sources.py +5 -5
- deva-1.4.0/deva/store.py +241 -0
- {deva-1.3.2 → deva-1.4.0}/deva/when.py +1 -1
- {deva-1.3.2 → deva-1.4.0}/deva.egg-info/PKG-INFO +5 -28
- {deva-1.3.2 → deva-1.4.0}/deva.egg-info/SOURCES.txt +1 -0
- {deva-1.3.2 → deva-1.4.0}/deva.egg-info/requires.txt +3 -0
- {deva-1.3.2 → deva-1.4.0}/setup.py +4 -1
- deva-1.3.2/deva/__init__.py +0 -24
- deva-1.3.2/deva/graph.py +0 -233
- deva-1.3.2/deva/store.py +0 -239
- {deva-1.3.2 → deva-1.4.0}/README.rst +0 -0
- {deva-1.3.2 → deva-1.4.0}/deva/bus.py +0 -0
- {deva-1.3.2 → deva-1.4.0}/deva/compute.py +0 -0
- {deva-1.3.2 → deva-1.4.0}/deva/monitor.py +0 -0
- {deva-1.3.2 → deva-1.4.0}/deva/search.py +0 -0
- {deva-1.3.2 → deva-1.4.0}/deva/topic.py +0 -0
- {deva-1.3.2 → deva-1.4.0}/deva/utils/__init__.py +0 -0
- {deva-1.3.2 → deva-1.4.0}/deva/utils/simhash.py +0 -0
- {deva-1.3.2 → deva-1.4.0}/deva/utils/sqlitedict.py +0 -0
- {deva-1.3.2 → deva-1.4.0}/deva/utils/time.py +0 -0
- {deva-1.3.2 → deva-1.4.0}/deva/utils/whooshalchemy.py +0 -0
- {deva-1.3.2 → deva-1.4.0}/deva.egg-info/dependency_links.txt +0 -0
- {deva-1.3.2 → deva-1.4.0}/deva.egg-info/not-zip-safe +0 -0
- {deva-1.3.2 → deva-1.4.0}/deva.egg-info/top_level.txt +0 -0
- {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
|
+
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
|
-
|
|
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 = [
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
|
233
|
+
if content.startswith("检索"):
|
|
153
234
|
return buffer, accumulated_text, start_time
|
|
154
235
|
|
|
155
|
-
buffer +=
|
|
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
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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=
|
|
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
|
-
|