deeptrade-quant 0.0.2__tar.gz → 0.2.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.
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/.gitignore +3 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/PKG-INFO +24 -7
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/README.md +22 -6
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/__init__.py +1 -1
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/cli.py +33 -6
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/cli_config.py +66 -2
- deeptrade_quant-0.2.0/deeptrade/cli_plugin.py +330 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/core/config.py +86 -3
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/core/config_migrations.py +44 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/core/db.py +4 -0
- deeptrade_quant-0.2.0/deeptrade/core/github_fetch.py +218 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/core/llm_manager.py +24 -7
- deeptrade_quant-0.0.2/deeptrade/core/migrations/core/20260427_001_init.sql → deeptrade_quant-0.2.0/deeptrade/core/migrations/core/20260509_001_init.sql +2 -10
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/core/notifier.py +7 -4
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/core/plugin_manager.py +40 -7
- deeptrade_quant-0.2.0/deeptrade/core/plugin_source.py +194 -0
- deeptrade_quant-0.2.0/deeptrade/core/registry.py +191 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/pyproject.toml +2 -1
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/cli/test_config_cmd.py +1 -0
- deeptrade_quant-0.2.0/tests/cli/test_plugin_cmd.py +320 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/cli/test_routing.py +0 -6
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/core/test_config.py +101 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/core/test_config_migrations.py +57 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/core/test_db.py +3 -8
- deeptrade_quant-0.2.0/tests/core/test_github_fetch.py +206 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/core/test_llm_manager.py +33 -0
- deeptrade_quant-0.2.0/tests/core/test_plugin_source.py +256 -0
- deeptrade_quant-0.2.0/tests/core/test_plugin_upgrade.py +142 -0
- deeptrade_quant-0.2.0/tests/core/test_registry.py +261 -0
- deeptrade_quant-0.0.2/deeptrade/channels_builtin/stdout/deeptrade_plugin.yaml +0 -25
- deeptrade_quant-0.0.2/deeptrade/channels_builtin/stdout/migrations/20260429_001_init.sql +0 -13
- deeptrade_quant-0.0.2/deeptrade/channels_builtin/stdout/stdout_channel/channel.py +0 -180
- deeptrade_quant-0.0.2/deeptrade/cli_plugin.py +0 -176
- deeptrade_quant-0.0.2/deeptrade/core/migrations/core/20260501_002_drop_llm_calls_stage.sql +0 -10
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/__init__.py +0 -0
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/deeptrade_plugin.yaml +0 -101
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/limit_up_board/__init__.py +0 -0
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/limit_up_board/calendar.py +0 -65
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/limit_up_board/cli.py +0 -269
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/limit_up_board/config.py +0 -76
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/limit_up_board/data.py +0 -1191
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/limit_up_board/pipeline.py +0 -869
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/limit_up_board/plugin.py +0 -30
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/limit_up_board/profiles.py +0 -85
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/limit_up_board/prompts.py +0 -485
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/limit_up_board/render.py +0 -890
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/limit_up_board/runner.py +0 -1087
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/limit_up_board/runtime.py +0 -172
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/limit_up_board/schemas.py +0 -178
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/migrations/20260430_001_init.sql +0 -150
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/migrations/20260501_002_lub_stage_results_llm_provider.sql +0 -8
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/migrations/20260508_001_lub_lhb_tables.sql +0 -36
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/migrations/20260508_002_lub_cyq_perf.sql +0 -18
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/migrations/20260508_003_lub_lhb_pk_fix.sql +0 -46
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/migrations/20260508_004_lub_lhb_drop_pk.sql +0 -53
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/limit_up_board/migrations/20260508_005_lub_config.sql +0 -17
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/__init__.py +0 -0
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/deeptrade_plugin.yaml +0 -59
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/migrations/20260430_001_init.sql +0 -94
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/migrations/20260601_001_realized_returns.sql +0 -44
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/migrations/20260601_002_dimension_scores.sql +0 -13
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/__init__.py +0 -0
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/calendar.py +0 -52
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/cli.py +0 -247
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/data.py +0 -2154
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/pipeline.py +0 -327
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/plugin.py +0 -22
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/profiles.py +0 -49
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/prompts.py +0 -187
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/prompts_examples.py +0 -84
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/render.py +0 -906
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/runner.py +0 -772
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/runtime.py +0 -90
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/schemas.py +0 -97
- deeptrade_quant-0.0.2/deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/stats.py +0 -174
- deeptrade_quant-0.0.2/tests/__init__.py +0 -0
- deeptrade_quant-0.0.2/tests/cli/__init__.py +0 -0
- deeptrade_quant-0.0.2/tests/core/__init__.py +0 -0
- deeptrade_quant-0.0.2/tests/plugins_api/__init__.py +0 -0
- deeptrade_quant-0.0.2/tests/strategies_builtin/__init__.py +0 -0
- deeptrade_quant-0.0.2/tests/strategies_builtin/limit_up_board/__init__.py +0 -0
- deeptrade_quant-0.0.2/tests/strategies_builtin/limit_up_board/test_phase_a_factors.py +0 -250
- deeptrade_quant-0.0.2/tests/strategies_builtin/limit_up_board/test_phase_b_factors.py +0 -164
- deeptrade_quant-0.0.2/tests/strategies_builtin/limit_up_board/test_v04_settings.py +0 -150
- deeptrade_quant-0.0.2/tests/strategies_builtin/volume_anomaly/__init__.py +0 -0
- deeptrade_quant-0.0.2/tests/strategies_builtin/volume_anomaly/test_alpha_features.py +0 -201
- deeptrade_quant-0.0.2/tests/strategies_builtin/volume_anomaly/test_candidate_features.py +0 -344
- deeptrade_quant-0.0.2/tests/strategies_builtin/volume_anomaly/test_dimension_scores.py +0 -128
- deeptrade_quant-0.0.2/tests/strategies_builtin/volume_anomaly/test_prompt_consistency.py +0 -186
- deeptrade_quant-0.0.2/tests/strategies_builtin/volume_anomaly/test_realized_returns.py +0 -167
- deeptrade_quant-0.0.2/tests/strategies_builtin/volume_anomaly/test_screen_rules.py +0 -225
- deeptrade_quant-0.0.2/tests/strategies_builtin/volume_anomaly/test_stats_query.py +0 -167
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/LICENSE +0 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/cli_data.py +0 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/core/__init__.py +0 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/core/llm_client.py +0 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/core/logging_config.py +0 -0
- {deeptrade_quant-0.0.2/deeptrade/channels_builtin → deeptrade_quant-0.2.0/deeptrade/core/migrations}/__init__.py +0 -0
- {deeptrade_quant-0.0.2/deeptrade/channels_builtin/stdout → deeptrade_quant-0.2.0/deeptrade/core/migrations/core}/__init__.py +0 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/core/paths.py +0 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/core/run_status.py +0 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/core/secrets.py +0 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/core/tushare_client.py +0 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/plugins_api/__init__.py +0 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/plugins_api/base.py +0 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/plugins_api/channel.py +0 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/plugins_api/events.py +0 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/plugins_api/llm.py +0 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/plugins_api/metadata.py +0 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/plugins_api/notify.py +0 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/deeptrade/theme.py +0 -0
- {deeptrade_quant-0.0.2/deeptrade/channels_builtin/stdout/stdout_channel → deeptrade_quant-0.2.0/tests}/__init__.py +0 -0
- {deeptrade_quant-0.0.2/deeptrade/core/migrations → deeptrade_quant-0.2.0/tests/cli}/__init__.py +0 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/conftest.py +0 -0
- {deeptrade_quant-0.0.2/deeptrade/core/migrations → deeptrade_quant-0.2.0/tests}/core/__init__.py +0 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/core/test_llm_client.py +0 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/core/test_notifier.py +0 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/core/test_paths.py +0 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/core/test_plugin_install.py +0 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/core/test_secrets.py +0 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/core/test_tushare_client.py +0 -0
- {deeptrade_quant-0.0.2/deeptrade/strategies_builtin → deeptrade_quant-0.2.0/tests/plugins_api}/__init__.py +0 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/plugins_api/test_notify.py +0 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/plugins_api/test_protocol.py +0 -0
- {deeptrade_quant-0.0.2 → deeptrade_quant-0.2.0}/tests/test_smoke.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deeptrade-quant
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: LLM-driven A-share (Shanghai/Shenzhen main board) stock screening CLI
|
|
5
5
|
Project-URL: Homepage, https://github.com/ty19880929/deeptrade
|
|
6
6
|
Project-URL: Repository, https://github.com/ty19880929/deeptrade
|
|
@@ -14,6 +14,7 @@ Requires-Dist: click>=8.1
|
|
|
14
14
|
Requires-Dist: duckdb>=1.0
|
|
15
15
|
Requires-Dist: keyring>=25.0
|
|
16
16
|
Requires-Dist: openai>=1.0
|
|
17
|
+
Requires-Dist: packaging>=23.0
|
|
17
18
|
Requires-Dist: pandas>=2.2
|
|
18
19
|
Requires-Dist: pydantic>=2.7
|
|
19
20
|
Requires-Dist: pyyaml>=6.0
|
|
@@ -52,10 +53,18 @@ Description-Content-Type: text/markdown
|
|
|
52
53
|
### 安装
|
|
53
54
|
|
|
54
55
|
```bash
|
|
55
|
-
#
|
|
56
|
+
# 推荐:pipx 隔离环境(命令名仍是 deeptrade)
|
|
57
|
+
pipx install deeptrade-quant
|
|
58
|
+
# 或
|
|
59
|
+
uv tool install deeptrade-quant
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
> **注**:PyPI 项目名是 `deeptrade-quant`,CLI 命令是 `deeptrade`,Python 包名是 `deeptrade`(`import deeptrade`)。三者不同是 Python 生态常态(同 `pip install scikit-learn` → `import sklearn`)。
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# 开发模式(克隆本仓库,editable install)
|
|
56
66
|
uv sync --all-extras
|
|
57
67
|
uv run pre-commit install
|
|
58
|
-
|
|
59
68
|
# 兜底(无 uv)
|
|
60
69
|
python -m venv .venv && source .venv/bin/activate # Windows: .\.venv\Scripts\activate
|
|
61
70
|
pip install -e ".[dev]"
|
|
@@ -72,12 +81,18 @@ deeptrade config test-llm # 对所有 provider 做连通性自
|
|
|
72
81
|
deeptrade config show # 表格展示当前配置(密钥脱敏)
|
|
73
82
|
```
|
|
74
83
|
|
|
75
|
-
###
|
|
84
|
+
### 安装官方插件并运行
|
|
85
|
+
|
|
86
|
+
官方插件维护在 [DeepTradePluginOfficial](https://github.com/ty19880929/DeepTradePluginOfficial),框架通过短名查注册表 → 拉 GitHub release tarball 自动安装。
|
|
76
87
|
|
|
77
88
|
```bash
|
|
78
|
-
#
|
|
79
|
-
deeptrade plugin
|
|
80
|
-
|
|
89
|
+
# 浏览注册表
|
|
90
|
+
deeptrade plugin search
|
|
91
|
+
|
|
92
|
+
# 按短名安装(注册表 → 最新 release tag)
|
|
93
|
+
deeptrade plugin install limit-up-board
|
|
94
|
+
deeptrade plugin install volume-anomaly
|
|
95
|
+
deeptrade plugin install stdout-channel
|
|
81
96
|
|
|
82
97
|
deeptrade plugin list # 查看已安装
|
|
83
98
|
|
|
@@ -95,6 +110,8 @@ deeptrade volume-anomaly prune --days 30 # 剔除追踪 ≥30 日的标的
|
|
|
95
110
|
deeptrade stdout-channel test
|
|
96
111
|
```
|
|
97
112
|
|
|
113
|
+
> **第三方插件 / 本地开发**:`deeptrade plugin install ./path/to/my-plugin` 仍可装本地目录;`deeptrade plugin install https://github.com/owner/repo` 装完整 git 仓库(仓库根需含 `deeptrade_plugin.yaml`)。详见 [`docs/plugin-development.md`](docs/plugin-development.md)。
|
|
114
|
+
|
|
98
115
|
报告产出在 `~/.deeptrade/reports/<run_id>/`。
|
|
99
116
|
|
|
100
117
|
## 📦 命令矩阵
|
|
@@ -19,10 +19,18 @@
|
|
|
19
19
|
### 安装
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
#
|
|
22
|
+
# 推荐:pipx 隔离环境(命令名仍是 deeptrade)
|
|
23
|
+
pipx install deeptrade-quant
|
|
24
|
+
# 或
|
|
25
|
+
uv tool install deeptrade-quant
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
> **注**:PyPI 项目名是 `deeptrade-quant`,CLI 命令是 `deeptrade`,Python 包名是 `deeptrade`(`import deeptrade`)。三者不同是 Python 生态常态(同 `pip install scikit-learn` → `import sklearn`)。
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# 开发模式(克隆本仓库,editable install)
|
|
23
32
|
uv sync --all-extras
|
|
24
33
|
uv run pre-commit install
|
|
25
|
-
|
|
26
34
|
# 兜底(无 uv)
|
|
27
35
|
python -m venv .venv && source .venv/bin/activate # Windows: .\.venv\Scripts\activate
|
|
28
36
|
pip install -e ".[dev]"
|
|
@@ -39,12 +47,18 @@ deeptrade config test-llm # 对所有 provider 做连通性自
|
|
|
39
47
|
deeptrade config show # 表格展示当前配置(密钥脱敏)
|
|
40
48
|
```
|
|
41
49
|
|
|
42
|
-
###
|
|
50
|
+
### 安装官方插件并运行
|
|
51
|
+
|
|
52
|
+
官方插件维护在 [DeepTradePluginOfficial](https://github.com/ty19880929/DeepTradePluginOfficial),框架通过短名查注册表 → 拉 GitHub release tarball 自动安装。
|
|
43
53
|
|
|
44
54
|
```bash
|
|
45
|
-
#
|
|
46
|
-
deeptrade plugin
|
|
47
|
-
|
|
55
|
+
# 浏览注册表
|
|
56
|
+
deeptrade plugin search
|
|
57
|
+
|
|
58
|
+
# 按短名安装(注册表 → 最新 release tag)
|
|
59
|
+
deeptrade plugin install limit-up-board
|
|
60
|
+
deeptrade plugin install volume-anomaly
|
|
61
|
+
deeptrade plugin install stdout-channel
|
|
48
62
|
|
|
49
63
|
deeptrade plugin list # 查看已安装
|
|
50
64
|
|
|
@@ -62,6 +76,8 @@ deeptrade volume-anomaly prune --days 30 # 剔除追踪 ≥30 日的标的
|
|
|
62
76
|
deeptrade stdout-channel test
|
|
63
77
|
```
|
|
64
78
|
|
|
79
|
+
> **第三方插件 / 本地开发**:`deeptrade plugin install ./path/to/my-plugin` 仍可装本地目录;`deeptrade plugin install https://github.com/owner/repo` 装完整 git 仓库(仓库根需含 `deeptrade_plugin.yaml`)。详见 [`docs/plugin-development.md`](docs/plugin-development.md)。
|
|
80
|
+
|
|
65
81
|
报告产出在 `~/.deeptrade/reports/<run_id>/`。
|
|
66
82
|
|
|
67
83
|
## 📦 命令矩阵
|
|
@@ -184,14 +184,8 @@ def init(
|
|
|
184
184
|
fresh = not db_file.exists()
|
|
185
185
|
db = Database(db_file)
|
|
186
186
|
try:
|
|
187
|
-
applied = apply_core_migrations(db)
|
|
188
187
|
if fresh:
|
|
189
188
|
typer.echo(f"✔ Database created: {db_file}")
|
|
190
|
-
if applied:
|
|
191
|
-
for v in applied:
|
|
192
|
-
typer.echo(f"✔ Schema applied: {v}")
|
|
193
|
-
else:
|
|
194
|
-
typer.echo("✔ Database already initialized; schema up-to-date")
|
|
195
189
|
finally:
|
|
196
190
|
db.close()
|
|
197
191
|
|
|
@@ -210,5 +204,38 @@ def init(
|
|
|
210
204
|
cmd_set_llm()
|
|
211
205
|
|
|
212
206
|
|
|
207
|
+
|
|
208
|
+
@app.command(name="db", context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
|
|
209
|
+
def db_cmd(ctx: click.Context) -> None:
|
|
210
|
+
"""Database migration and management commands (legacy stub; use `deeptrade db init` via group if added)."""
|
|
211
|
+
pass
|
|
212
|
+
|
|
213
|
+
db_app = typer.Typer(name="db", help="Database migration and management commands.")
|
|
214
|
+
app.add_typer(db_app, name="db")
|
|
215
|
+
|
|
216
|
+
@db_app.command("init")
|
|
217
|
+
def db_init() -> None:
|
|
218
|
+
"""Initialize the core database tables and apply migrations."""
|
|
219
|
+
paths.ensure_layout()
|
|
220
|
+
db_file = paths.db_path()
|
|
221
|
+
fresh = not db_file.exists()
|
|
222
|
+
db = Database(db_file)
|
|
223
|
+
try:
|
|
224
|
+
applied = apply_core_migrations(db)
|
|
225
|
+
if fresh:
|
|
226
|
+
typer.echo(f"✔ Database created: {db_file}")
|
|
227
|
+
if applied:
|
|
228
|
+
for v in applied:
|
|
229
|
+
typer.echo(f"✔ Schema applied: {v}")
|
|
230
|
+
else:
|
|
231
|
+
typer.echo("✔ Database already initialized; schema up-to-date")
|
|
232
|
+
finally:
|
|
233
|
+
db.close()
|
|
234
|
+
|
|
235
|
+
@db_app.command("upgrade")
|
|
236
|
+
def db_upgrade() -> None:
|
|
237
|
+
"""Apply any pending core migrations."""
|
|
238
|
+
db_init()
|
|
239
|
+
|
|
213
240
|
if __name__ == "__main__":
|
|
214
241
|
app()
|
|
@@ -191,13 +191,35 @@ def _set_llm_new(svc: ConfigService) -> None:
|
|
|
191
191
|
typer.echo(f"Provider {name!r} already exists; pick edit instead.")
|
|
192
192
|
raise typer.Exit(2)
|
|
193
193
|
default_base = _DEFAULT_BASE_URLS.get(name.split("-")[0], "")
|
|
194
|
-
|
|
194
|
+
# Adding into an empty dict auto-promotes to default; offer the choice
|
|
195
|
+
# only when a default is already in place.
|
|
196
|
+
promote_default: bool | None = None
|
|
197
|
+
if cfg.llm_providers:
|
|
198
|
+
promote_default = bool(
|
|
199
|
+
questionary.confirm(
|
|
200
|
+
f"Set {name!r} as the default LLM provider?",
|
|
201
|
+
default=False,
|
|
202
|
+
).ask()
|
|
203
|
+
)
|
|
204
|
+
_prompt_and_save_provider(
|
|
205
|
+
svc,
|
|
206
|
+
name,
|
|
207
|
+
defaults=None,
|
|
208
|
+
default_base_url=default_base,
|
|
209
|
+
is_default=promote_default,
|
|
210
|
+
)
|
|
195
211
|
|
|
196
212
|
|
|
197
213
|
def _set_llm_edit(svc: ConfigService, name: str) -> None:
|
|
198
214
|
cfg = svc.get_app_config()
|
|
199
215
|
cur = cfg.llm_providers[name]
|
|
200
|
-
_prompt_and_save_provider(
|
|
216
|
+
_prompt_and_save_provider(
|
|
217
|
+
svc,
|
|
218
|
+
name,
|
|
219
|
+
defaults=cur.model_dump(),
|
|
220
|
+
default_base_url=cur.base_url,
|
|
221
|
+
is_default=None,
|
|
222
|
+
)
|
|
201
223
|
|
|
202
224
|
|
|
203
225
|
def _prompt_and_save_provider(
|
|
@@ -206,6 +228,7 @@ def _prompt_and_save_provider(
|
|
|
206
228
|
*,
|
|
207
229
|
defaults: dict | None,
|
|
208
230
|
default_base_url: str,
|
|
231
|
+
is_default: bool | None = None,
|
|
209
232
|
) -> None:
|
|
210
233
|
base_url_default = defaults.get("base_url", default_base_url) if defaults else default_base_url
|
|
211
234
|
model_default = defaults.get("model", "") if defaults else ""
|
|
@@ -251,6 +274,7 @@ def _prompt_and_save_provider(
|
|
|
251
274
|
model=model,
|
|
252
275
|
timeout=timeout,
|
|
253
276
|
api_key=api_key if api_key else None,
|
|
277
|
+
is_default=is_default,
|
|
254
278
|
)
|
|
255
279
|
except ValueError as e:
|
|
256
280
|
typer.echo(f"Invalid provider: {e}")
|
|
@@ -274,8 +298,48 @@ def _set_llm_delete(svc: ConfigService, existing: list[str]) -> None:
|
|
|
274
298
|
).ask()
|
|
275
299
|
if not confirm:
|
|
276
300
|
raise typer.Exit(1)
|
|
301
|
+
prior_default = svc.get_default_llm_provider()
|
|
277
302
|
svc.delete_llm_provider(name)
|
|
278
303
|
typer.echo(f"✔ Deleted LLM provider {name!r}")
|
|
304
|
+
new_default = svc.get_default_llm_provider()
|
|
305
|
+
if prior_default == name and new_default is not None:
|
|
306
|
+
typer.echo(f"✔ Default LLM provider auto-switched to {new_default!r}")
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@app.command("set-default-llm")
|
|
310
|
+
def cmd_set_default_llm(
|
|
311
|
+
name: str = typer.Argument(..., help="Provider name to mark as default."),
|
|
312
|
+
) -> None:
|
|
313
|
+
"""Mark ``name`` as the default LLM provider.
|
|
314
|
+
|
|
315
|
+
The default is consumed by ``LLMManager.get_client()`` when callers
|
|
316
|
+
don't name a provider (non-debate plugin path). Switching default
|
|
317
|
+
clears the flag on every other provider so the
|
|
318
|
+
"exactly-one-default" invariant holds.
|
|
319
|
+
"""
|
|
320
|
+
db, svc = _open_service()
|
|
321
|
+
try:
|
|
322
|
+
cfg = svc.get_app_config()
|
|
323
|
+
provider = cfg.llm_providers.get(name)
|
|
324
|
+
if provider is None:
|
|
325
|
+
typer.echo(
|
|
326
|
+
f"Unknown LLM provider: {name!r}; configured providers: "
|
|
327
|
+
+ (", ".join(sorted(cfg.llm_providers.keys())) or "(none)")
|
|
328
|
+
)
|
|
329
|
+
raise typer.Exit(2)
|
|
330
|
+
if provider.is_default:
|
|
331
|
+
typer.echo(f"{name!r} is already the default LLM provider")
|
|
332
|
+
return
|
|
333
|
+
svc.set_llm_provider(
|
|
334
|
+
name,
|
|
335
|
+
base_url=provider.base_url,
|
|
336
|
+
model=provider.model,
|
|
337
|
+
timeout=provider.timeout,
|
|
338
|
+
is_default=True,
|
|
339
|
+
)
|
|
340
|
+
typer.echo(f"✔ Default LLM provider set to {name!r}")
|
|
341
|
+
finally:
|
|
342
|
+
db.close()
|
|
279
343
|
|
|
280
344
|
|
|
281
345
|
@app.command("list-llm")
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""`deeptrade plugin` subcommand group."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import questionary
|
|
6
|
+
import typer
|
|
7
|
+
import yaml
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from deeptrade.core import paths
|
|
12
|
+
from deeptrade.core.db import Database
|
|
13
|
+
from deeptrade.core.github_fetch import (
|
|
14
|
+
GitHubFetchError,
|
|
15
|
+
NoMatchingReleaseError,
|
|
16
|
+
TarballFetchError,
|
|
17
|
+
)
|
|
18
|
+
from deeptrade.core.plugin_manager import (
|
|
19
|
+
PluginInstallError,
|
|
20
|
+
PluginManager,
|
|
21
|
+
PluginNotFoundError,
|
|
22
|
+
UpgradeNoop,
|
|
23
|
+
_load_metadata_yaml,
|
|
24
|
+
summarize_for_install,
|
|
25
|
+
)
|
|
26
|
+
from deeptrade.core.plugin_source import (
|
|
27
|
+
ResolvedSource,
|
|
28
|
+
SourceResolveError,
|
|
29
|
+
SourceResolver,
|
|
30
|
+
)
|
|
31
|
+
from deeptrade.core.registry import (
|
|
32
|
+
RegistryClient,
|
|
33
|
+
RegistryFetchError,
|
|
34
|
+
RegistryNotFoundError,
|
|
35
|
+
RegistrySchemaError,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
app = typer.Typer(help="Install / manage plugins", no_args_is_help=True)
|
|
39
|
+
|
|
40
|
+
_RESOLVE_ERRORS: tuple[type[Exception], ...] = (
|
|
41
|
+
RegistryNotFoundError,
|
|
42
|
+
RegistryFetchError,
|
|
43
|
+
RegistrySchemaError,
|
|
44
|
+
NoMatchingReleaseError,
|
|
45
|
+
TarballFetchError,
|
|
46
|
+
GitHubFetchError,
|
|
47
|
+
SourceResolveError,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _open() -> tuple[Database, PluginManager]:
|
|
52
|
+
db = Database(paths.db_path())
|
|
53
|
+
return db, PluginManager(db)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _format_origin(resolved: ResolvedSource) -> str:
|
|
57
|
+
d = resolved.origin_detail
|
|
58
|
+
if resolved.origin == "local":
|
|
59
|
+
return f"本地路径 ({d.get('local_path', resolved.path)})"
|
|
60
|
+
if resolved.origin == "github_registry":
|
|
61
|
+
return (
|
|
62
|
+
f"GitHub 注册表 ({d['repo']}@{d['ref']}, subdir={d['subdir']})"
|
|
63
|
+
)
|
|
64
|
+
if resolved.origin == "github_url":
|
|
65
|
+
return f"GitHub URL ({d['repo']}@{d['ref']})"
|
|
66
|
+
return resolved.origin
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@app.command("install")
|
|
70
|
+
def cmd_install(
|
|
71
|
+
source: str = typer.Argument(
|
|
72
|
+
..., help="短名(注册表)/ 本地路径 / GitHub URL"
|
|
73
|
+
),
|
|
74
|
+
ref: str | None = typer.Option(
|
|
75
|
+
None, "--ref", help="Tag / branch / sha (默认 = 该插件最新 release)"
|
|
76
|
+
),
|
|
77
|
+
yes: bool = typer.Option(False, "-y", "--yes", help="Skip the confirmation prompt"),
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Install a plugin from the registry, a GitHub URL, or a local directory."""
|
|
80
|
+
resolver = SourceResolver()
|
|
81
|
+
try:
|
|
82
|
+
resolved = resolver.resolve(source, ref)
|
|
83
|
+
except _RESOLVE_ERRORS as e:
|
|
84
|
+
typer.echo(f"✘ {e}")
|
|
85
|
+
raise typer.Exit(2) from e
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
try:
|
|
89
|
+
meta = _load_metadata_yaml(resolved.path / "deeptrade_plugin.yaml")
|
|
90
|
+
except PluginInstallError as e:
|
|
91
|
+
typer.echo(f"✘ {e}")
|
|
92
|
+
raise typer.Exit(2) from e
|
|
93
|
+
|
|
94
|
+
typer.echo("─── 即将安装 ─────────────────────────────")
|
|
95
|
+
typer.echo(f"来源: {_format_origin(resolved)}")
|
|
96
|
+
typer.echo(summarize_for_install(meta, resolved.path))
|
|
97
|
+
typer.echo("──────────────────────────────────────────")
|
|
98
|
+
if not yes:
|
|
99
|
+
ok = questionary.confirm("确认安装?", default=False).ask()
|
|
100
|
+
if not ok:
|
|
101
|
+
typer.echo("Aborted.")
|
|
102
|
+
raise typer.Exit(1)
|
|
103
|
+
|
|
104
|
+
db, mgr = _open()
|
|
105
|
+
try:
|
|
106
|
+
rec = mgr.install(resolved.path)
|
|
107
|
+
except PluginInstallError as e:
|
|
108
|
+
typer.echo(f"✘ Install failed: {e}")
|
|
109
|
+
raise typer.Exit(2) from e
|
|
110
|
+
finally:
|
|
111
|
+
db.close()
|
|
112
|
+
|
|
113
|
+
typer.echo(f"✔ 已安装: {rec.plugin_id} v{rec.version}")
|
|
114
|
+
finally:
|
|
115
|
+
if resolved.cleanup is not None:
|
|
116
|
+
resolved.cleanup()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@app.command("list")
|
|
120
|
+
def cmd_list() -> None:
|
|
121
|
+
"""List installed plugins."""
|
|
122
|
+
db, mgr = _open()
|
|
123
|
+
try:
|
|
124
|
+
records = mgr.list_all()
|
|
125
|
+
finally:
|
|
126
|
+
db.close()
|
|
127
|
+
|
|
128
|
+
console = Console()
|
|
129
|
+
table = Table(title="Installed Plugins")
|
|
130
|
+
table.add_column("plugin_id", style="cyan")
|
|
131
|
+
table.add_column("name")
|
|
132
|
+
table.add_column("version")
|
|
133
|
+
table.add_column("enabled", style="green")
|
|
134
|
+
if not records:
|
|
135
|
+
typer.echo("(no plugins installed)")
|
|
136
|
+
return
|
|
137
|
+
for r in records:
|
|
138
|
+
table.add_row(r.plugin_id, r.name, r.version, "yes" if r.enabled else "no")
|
|
139
|
+
console.print(table)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@app.command("info")
|
|
143
|
+
def cmd_info(plugin_id: str = typer.Argument(...)) -> None:
|
|
144
|
+
"""Show metadata for a plugin.
|
|
145
|
+
|
|
146
|
+
If installed locally: shows the full installed metadata.yaml.
|
|
147
|
+
If not installed: falls back to the registry entry (with an install hint).
|
|
148
|
+
"""
|
|
149
|
+
db, mgr = _open()
|
|
150
|
+
try:
|
|
151
|
+
try:
|
|
152
|
+
rec = mgr.info(plugin_id)
|
|
153
|
+
typer.echo(
|
|
154
|
+
yaml.safe_dump(rec.metadata.model_dump(mode="json"), allow_unicode=True)
|
|
155
|
+
)
|
|
156
|
+
return
|
|
157
|
+
except PluginNotFoundError:
|
|
158
|
+
pass # fall through to registry lookup
|
|
159
|
+
finally:
|
|
160
|
+
db.close()
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
entry = RegistryClient().resolve(plugin_id)
|
|
164
|
+
except RegistryNotFoundError as e:
|
|
165
|
+
typer.echo(f"✘ {plugin_id} 既未安装,也不在注册表中")
|
|
166
|
+
raise typer.Exit(2) from e
|
|
167
|
+
except (RegistryFetchError, RegistrySchemaError) as e:
|
|
168
|
+
typer.echo(f"✘ {plugin_id} 未安装;查询注册表失败: {e}")
|
|
169
|
+
raise typer.Exit(2) from e
|
|
170
|
+
|
|
171
|
+
typer.echo(f"(未安装) {entry.plugin_id}")
|
|
172
|
+
typer.echo(f" name: {entry.name}")
|
|
173
|
+
typer.echo(f" type: {entry.type}")
|
|
174
|
+
typer.echo(f" description: {entry.description}")
|
|
175
|
+
typer.echo(f" repo: {entry.repo}")
|
|
176
|
+
typer.echo(f" subdir: {entry.subdir}")
|
|
177
|
+
typer.echo(f" install: deeptrade plugin install {entry.plugin_id}")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@app.command("disable")
|
|
181
|
+
def cmd_disable(plugin_id: str = typer.Argument(...)) -> None:
|
|
182
|
+
db, mgr = _open()
|
|
183
|
+
try:
|
|
184
|
+
try:
|
|
185
|
+
mgr.disable(plugin_id)
|
|
186
|
+
except PluginNotFoundError as e:
|
|
187
|
+
typer.echo(f"✘ {plugin_id} not installed")
|
|
188
|
+
raise typer.Exit(2) from e
|
|
189
|
+
typer.echo(f"✔ disabled: {plugin_id}")
|
|
190
|
+
finally:
|
|
191
|
+
db.close()
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@app.command("enable")
|
|
195
|
+
def cmd_enable(plugin_id: str = typer.Argument(...)) -> None:
|
|
196
|
+
db, mgr = _open()
|
|
197
|
+
try:
|
|
198
|
+
try:
|
|
199
|
+
mgr.enable(plugin_id)
|
|
200
|
+
except PluginNotFoundError as e:
|
|
201
|
+
typer.echo(f"✘ {plugin_id} not installed")
|
|
202
|
+
raise typer.Exit(2) from e
|
|
203
|
+
typer.echo(f"✔ enabled: {plugin_id}")
|
|
204
|
+
finally:
|
|
205
|
+
db.close()
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@app.command("uninstall")
|
|
209
|
+
def cmd_uninstall(
|
|
210
|
+
plugin_id: str = typer.Argument(...),
|
|
211
|
+
purge: bool = typer.Option(False, "--purge", help="DROP plugin tables and forget all data"),
|
|
212
|
+
yes: bool = typer.Option(False, "-y", "--yes", help="Skip confirmation"),
|
|
213
|
+
) -> None:
|
|
214
|
+
"""Uninstall a plugin. Default: keep tables (just disable). --purge: drop tables."""
|
|
215
|
+
db, mgr = _open()
|
|
216
|
+
try:
|
|
217
|
+
try:
|
|
218
|
+
rec = mgr.info(plugin_id)
|
|
219
|
+
except PluginNotFoundError as e:
|
|
220
|
+
typer.echo(f"✘ {plugin_id} not installed")
|
|
221
|
+
raise typer.Exit(2) from e
|
|
222
|
+
|
|
223
|
+
if purge and not yes:
|
|
224
|
+
tables = [t.name for t in rec.metadata.tables if t.purge_on_uninstall]
|
|
225
|
+
typer.echo(f"将删除以下表(不可恢复): {tables}")
|
|
226
|
+
ok = questionary.confirm("确认 --purge?", default=False).ask()
|
|
227
|
+
if not ok:
|
|
228
|
+
typer.echo("Aborted.")
|
|
229
|
+
raise typer.Exit(1)
|
|
230
|
+
|
|
231
|
+
result = mgr.uninstall(plugin_id, purge=purge)
|
|
232
|
+
action = "purged" if purge else "disabled"
|
|
233
|
+
typer.echo(f"✔ {action}: {plugin_id} (dropped tables: {result['purged_tables']})")
|
|
234
|
+
finally:
|
|
235
|
+
db.close()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@app.command("upgrade")
|
|
239
|
+
def cmd_upgrade(
|
|
240
|
+
source: str = typer.Argument(
|
|
241
|
+
..., help="短名(注册表)/ 本地路径 / GitHub URL"
|
|
242
|
+
),
|
|
243
|
+
ref: str | None = typer.Option(
|
|
244
|
+
None, "--ref", help="Tag / branch / sha (默认 = 该插件最新 release)"
|
|
245
|
+
),
|
|
246
|
+
) -> None:
|
|
247
|
+
"""Upgrade an installed plugin.
|
|
248
|
+
|
|
249
|
+
Exit codes:
|
|
250
|
+
0 — upgraded, or already at the candidate version
|
|
251
|
+
2 — not installed / candidate < installed (downgrade forbidden) /
|
|
252
|
+
network or registry failure
|
|
253
|
+
"""
|
|
254
|
+
resolver = SourceResolver()
|
|
255
|
+
try:
|
|
256
|
+
resolved = resolver.resolve(source, ref)
|
|
257
|
+
except _RESOLVE_ERRORS as e:
|
|
258
|
+
typer.echo(f"✘ {e}")
|
|
259
|
+
raise typer.Exit(2) from e
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
db, mgr = _open()
|
|
263
|
+
try:
|
|
264
|
+
try:
|
|
265
|
+
result = mgr.upgrade(resolved.path)
|
|
266
|
+
except PluginNotFoundError as e:
|
|
267
|
+
try:
|
|
268
|
+
meta = _load_metadata_yaml(resolved.path / "deeptrade_plugin.yaml")
|
|
269
|
+
pid = meta.plugin_id
|
|
270
|
+
except PluginInstallError:
|
|
271
|
+
pid = source
|
|
272
|
+
typer.echo(
|
|
273
|
+
f'✘ 插件 "{pid}" 未安装,请先执行 deeptrade plugin install'
|
|
274
|
+
)
|
|
275
|
+
raise typer.Exit(2) from e
|
|
276
|
+
except PluginInstallError as e:
|
|
277
|
+
typer.echo(f"✘ Upgrade failed: {e}")
|
|
278
|
+
raise typer.Exit(2) from e
|
|
279
|
+
|
|
280
|
+
if isinstance(result, UpgradeNoop):
|
|
281
|
+
typer.echo(f"已是最新版本 v{result.version}")
|
|
282
|
+
return
|
|
283
|
+
typer.echo(f"✔ upgraded: {result.plugin_id} → v{result.version}")
|
|
284
|
+
finally:
|
|
285
|
+
db.close()
|
|
286
|
+
finally:
|
|
287
|
+
if resolved.cleanup is not None:
|
|
288
|
+
resolved.cleanup()
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@app.command("search")
|
|
292
|
+
def cmd_search(
|
|
293
|
+
keyword: str | None = typer.Argument(
|
|
294
|
+
None, help="可选过滤关键词(匹配 plugin_id / name / description)"
|
|
295
|
+
),
|
|
296
|
+
no_cache: bool = typer.Option(
|
|
297
|
+
False, "--no-cache", help="强制刷新注册表(旁路 ETag 缓存)"
|
|
298
|
+
),
|
|
299
|
+
) -> None:
|
|
300
|
+
"""List plugins available in the official registry."""
|
|
301
|
+
try:
|
|
302
|
+
registry = RegistryClient().fetch(force=no_cache)
|
|
303
|
+
except (RegistryFetchError, RegistrySchemaError) as e:
|
|
304
|
+
typer.echo(f"✘ {e}")
|
|
305
|
+
raise typer.Exit(2) from e
|
|
306
|
+
|
|
307
|
+
rows = list(registry.plugins.values())
|
|
308
|
+
if keyword:
|
|
309
|
+
kw = keyword.lower()
|
|
310
|
+
rows = [
|
|
311
|
+
entry
|
|
312
|
+
for entry in rows
|
|
313
|
+
if kw in entry.plugin_id.lower()
|
|
314
|
+
or kw in entry.name.lower()
|
|
315
|
+
or kw in entry.description.lower()
|
|
316
|
+
]
|
|
317
|
+
|
|
318
|
+
if not rows:
|
|
319
|
+
typer.echo("(no plugins matched)" if keyword else "(registry is empty)")
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
console = Console()
|
|
323
|
+
table = Table(title="Available Plugins")
|
|
324
|
+
table.add_column("plugin_id", style="cyan")
|
|
325
|
+
table.add_column("name")
|
|
326
|
+
table.add_column("type", style="green")
|
|
327
|
+
table.add_column("description")
|
|
328
|
+
for entry in sorted(rows, key=lambda x: x.plugin_id):
|
|
329
|
+
table.add_row(entry.plugin_id, entry.name, entry.type, entry.description)
|
|
330
|
+
console.print(table)
|