deeptrade-quant 0.0.2__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.
- deeptrade/__init__.py +8 -0
- deeptrade/channels_builtin/__init__.py +0 -0
- deeptrade/channels_builtin/stdout/__init__.py +0 -0
- deeptrade/channels_builtin/stdout/deeptrade_plugin.yaml +25 -0
- deeptrade/channels_builtin/stdout/migrations/20260429_001_init.sql +13 -0
- deeptrade/channels_builtin/stdout/stdout_channel/__init__.py +0 -0
- deeptrade/channels_builtin/stdout/stdout_channel/channel.py +180 -0
- deeptrade/cli.py +214 -0
- deeptrade/cli_config.py +396 -0
- deeptrade/cli_data.py +33 -0
- deeptrade/cli_plugin.py +176 -0
- deeptrade/core/__init__.py +8 -0
- deeptrade/core/config.py +344 -0
- deeptrade/core/config_migrations.py +138 -0
- deeptrade/core/db.py +176 -0
- deeptrade/core/llm_client.py +591 -0
- deeptrade/core/llm_manager.py +174 -0
- deeptrade/core/logging_config.py +61 -0
- deeptrade/core/migrations/__init__.py +0 -0
- deeptrade/core/migrations/core/20260427_001_init.sql +121 -0
- deeptrade/core/migrations/core/20260501_002_drop_llm_calls_stage.sql +10 -0
- deeptrade/core/migrations/core/__init__.py +0 -0
- deeptrade/core/notifier.py +302 -0
- deeptrade/core/paths.py +49 -0
- deeptrade/core/plugin_manager.py +616 -0
- deeptrade/core/run_status.py +29 -0
- deeptrade/core/secrets.py +152 -0
- deeptrade/core/tushare_client.py +824 -0
- deeptrade/plugins_api/__init__.py +44 -0
- deeptrade/plugins_api/base.py +66 -0
- deeptrade/plugins_api/channel.py +42 -0
- deeptrade/plugins_api/events.py +61 -0
- deeptrade/plugins_api/llm.py +46 -0
- deeptrade/plugins_api/metadata.py +84 -0
- deeptrade/plugins_api/notify.py +67 -0
- deeptrade/strategies_builtin/__init__.py +0 -0
- deeptrade/strategies_builtin/limit_up_board/__init__.py +0 -0
- deeptrade/strategies_builtin/limit_up_board/deeptrade_plugin.yaml +101 -0
- deeptrade/strategies_builtin/limit_up_board/limit_up_board/__init__.py +0 -0
- deeptrade/strategies_builtin/limit_up_board/limit_up_board/calendar.py +65 -0
- deeptrade/strategies_builtin/limit_up_board/limit_up_board/cli.py +269 -0
- deeptrade/strategies_builtin/limit_up_board/limit_up_board/config.py +76 -0
- deeptrade/strategies_builtin/limit_up_board/limit_up_board/data.py +1191 -0
- deeptrade/strategies_builtin/limit_up_board/limit_up_board/pipeline.py +869 -0
- deeptrade/strategies_builtin/limit_up_board/limit_up_board/plugin.py +30 -0
- deeptrade/strategies_builtin/limit_up_board/limit_up_board/profiles.py +85 -0
- deeptrade/strategies_builtin/limit_up_board/limit_up_board/prompts.py +485 -0
- deeptrade/strategies_builtin/limit_up_board/limit_up_board/render.py +890 -0
- deeptrade/strategies_builtin/limit_up_board/limit_up_board/runner.py +1087 -0
- deeptrade/strategies_builtin/limit_up_board/limit_up_board/runtime.py +172 -0
- deeptrade/strategies_builtin/limit_up_board/limit_up_board/schemas.py +178 -0
- deeptrade/strategies_builtin/limit_up_board/migrations/20260430_001_init.sql +150 -0
- deeptrade/strategies_builtin/limit_up_board/migrations/20260501_002_lub_stage_results_llm_provider.sql +8 -0
- deeptrade/strategies_builtin/limit_up_board/migrations/20260508_001_lub_lhb_tables.sql +36 -0
- deeptrade/strategies_builtin/limit_up_board/migrations/20260508_002_lub_cyq_perf.sql +18 -0
- deeptrade/strategies_builtin/limit_up_board/migrations/20260508_003_lub_lhb_pk_fix.sql +46 -0
- deeptrade/strategies_builtin/limit_up_board/migrations/20260508_004_lub_lhb_drop_pk.sql +53 -0
- deeptrade/strategies_builtin/limit_up_board/migrations/20260508_005_lub_config.sql +17 -0
- deeptrade/strategies_builtin/volume_anomaly/__init__.py +0 -0
- deeptrade/strategies_builtin/volume_anomaly/deeptrade_plugin.yaml +59 -0
- deeptrade/strategies_builtin/volume_anomaly/migrations/20260430_001_init.sql +94 -0
- deeptrade/strategies_builtin/volume_anomaly/migrations/20260601_001_realized_returns.sql +44 -0
- deeptrade/strategies_builtin/volume_anomaly/migrations/20260601_002_dimension_scores.sql +13 -0
- deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/__init__.py +0 -0
- deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/calendar.py +52 -0
- deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/cli.py +247 -0
- deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/data.py +2154 -0
- deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/pipeline.py +327 -0
- deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/plugin.py +22 -0
- deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/profiles.py +49 -0
- deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/prompts.py +187 -0
- deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/prompts_examples.py +84 -0
- deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/render.py +906 -0
- deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/runner.py +772 -0
- deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/runtime.py +90 -0
- deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/schemas.py +97 -0
- deeptrade/strategies_builtin/volume_anomaly/volume_anomaly/stats.py +174 -0
- deeptrade/theme.py +48 -0
- deeptrade_quant-0.0.2.dist-info/METADATA +166 -0
- deeptrade_quant-0.0.2.dist-info/RECORD +83 -0
- deeptrade_quant-0.0.2.dist-info/WHEEL +4 -0
- deeptrade_quant-0.0.2.dist-info/entry_points.txt +2 -0
- deeptrade_quant-0.0.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""Plugin-managed CLI for limit-up-board.
|
|
2
|
+
|
|
3
|
+
Subcommands:
|
|
4
|
+
run — full pipeline (Step 0..5)
|
|
5
|
+
sync — data-only path (no LLM)
|
|
6
|
+
history — list recent runs
|
|
7
|
+
report — re-render a finished run's terminal summary
|
|
8
|
+
|
|
9
|
+
Invoked via the framework's pure pass-through dispatch:
|
|
10
|
+
deeptrade limit-up-board <subcommand> [...]
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
import questionary
|
|
18
|
+
import typer
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
from rich.table import Table
|
|
21
|
+
|
|
22
|
+
from deeptrade.core import paths
|
|
23
|
+
from deeptrade.core.config import ConfigService
|
|
24
|
+
from deeptrade.core.db import Database
|
|
25
|
+
from deeptrade.core.llm_manager import LLMManager
|
|
26
|
+
|
|
27
|
+
from .config import LubConfig, list_for_show, load_config, save_config
|
|
28
|
+
from .runner import LubRunner, PreconditionError, RunParams, render_finished_run
|
|
29
|
+
from .runtime import LubRuntime
|
|
30
|
+
|
|
31
|
+
app = typer.Typer(
|
|
32
|
+
name="limit-up-board",
|
|
33
|
+
help="打板策略 — A 股涨停板双轮 LLM 漏斗。",
|
|
34
|
+
no_args_is_help=True,
|
|
35
|
+
add_completion=False,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
settings_app = typer.Typer(
|
|
39
|
+
name="settings",
|
|
40
|
+
help="本插件可持久化的运行参数(流通市值 / 当前股价 上限)。",
|
|
41
|
+
no_args_is_help=False,
|
|
42
|
+
add_completion=False,
|
|
43
|
+
invoke_without_command=True,
|
|
44
|
+
)
|
|
45
|
+
app.add_typer(settings_app, name="settings")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _open_runtime() -> tuple[Database, LubRuntime]:
|
|
49
|
+
db = Database(paths.db_path())
|
|
50
|
+
cfg = ConfigService(db)
|
|
51
|
+
rt = LubRuntime(db=db, config=cfg, llms=LLMManager(db, cfg))
|
|
52
|
+
return db, rt
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@app.command("run")
|
|
56
|
+
def cmd_run(
|
|
57
|
+
trade_date: str | None = typer.Option(None, "--trade-date", help="YYYYMMDD"),
|
|
58
|
+
allow_intraday: bool = typer.Option(False, "--allow-intraday"),
|
|
59
|
+
force_sync: bool = typer.Option(False, "--force-sync"),
|
|
60
|
+
daily_lookback: int = typer.Option(30, "--daily-lookback"),
|
|
61
|
+
moneyflow_lookback: int = typer.Option(5, "--moneyflow-lookback"),
|
|
62
|
+
debate: bool = typer.Option(
|
|
63
|
+
False,
|
|
64
|
+
"--debate",
|
|
65
|
+
help="启用多 LLM 辩论模式(需要 ≥2 个已配置的 LLM provider)",
|
|
66
|
+
),
|
|
67
|
+
debate_llms: str | None = typer.Option(
|
|
68
|
+
None,
|
|
69
|
+
"--debate-llms",
|
|
70
|
+
help=(
|
|
71
|
+
"逗号分隔的 LLM provider 子集(如 'deepseek,kimi'),"
|
|
72
|
+
"必须配合 --debate 使用;不指定则使用全部已配置 provider"
|
|
73
|
+
),
|
|
74
|
+
),
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Run the full打板策略 pipeline."""
|
|
77
|
+
debate_llms_list: list[str] | None = None
|
|
78
|
+
if debate_llms is not None:
|
|
79
|
+
if not debate:
|
|
80
|
+
typer.echo("✘ --debate-llms 必须配合 --debate 使用")
|
|
81
|
+
raise typer.Exit(2)
|
|
82
|
+
debate_llms_list = [s.strip() for s in debate_llms.split(",") if s.strip()]
|
|
83
|
+
if not debate_llms_list:
|
|
84
|
+
typer.echo("✘ --debate-llms 解析后为空")
|
|
85
|
+
raise typer.Exit(2)
|
|
86
|
+
|
|
87
|
+
db, rt = _open_runtime()
|
|
88
|
+
try:
|
|
89
|
+
params = RunParams(
|
|
90
|
+
trade_date=trade_date,
|
|
91
|
+
allow_intraday=allow_intraday,
|
|
92
|
+
force_sync=force_sync,
|
|
93
|
+
daily_lookback=daily_lookback,
|
|
94
|
+
moneyflow_lookback=moneyflow_lookback,
|
|
95
|
+
debate=debate,
|
|
96
|
+
debate_llms=debate_llms_list,
|
|
97
|
+
)
|
|
98
|
+
runner = LubRunner(rt)
|
|
99
|
+
outcome = runner.execute(params)
|
|
100
|
+
typer.echo(f"\nstatus: {outcome.status.value} run_id: {outcome.run_id}")
|
|
101
|
+
if outcome.error:
|
|
102
|
+
typer.echo(f"error: {outcome.error}")
|
|
103
|
+
if outcome.status.value not in {"success", "partial_failed"}:
|
|
104
|
+
raise typer.Exit(1)
|
|
105
|
+
# Print the terminal summary right after a successful run.
|
|
106
|
+
render_finished_run(outcome.run_id)
|
|
107
|
+
finally:
|
|
108
|
+
db.close()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@app.command("sync")
|
|
112
|
+
def cmd_sync(
|
|
113
|
+
trade_date: str | None = typer.Option(None, "--trade-date", help="YYYYMMDD"),
|
|
114
|
+
allow_intraday: bool = typer.Option(False, "--allow-intraday"),
|
|
115
|
+
force_sync: bool = typer.Option(False, "--force-sync"),
|
|
116
|
+
daily_lookback: int = typer.Option(30, "--daily-lookback"),
|
|
117
|
+
moneyflow_lookback: int = typer.Option(5, "--moneyflow-lookback"),
|
|
118
|
+
) -> None:
|
|
119
|
+
"""Fetch + persist data only (no LLM stages)."""
|
|
120
|
+
db, rt = _open_runtime()
|
|
121
|
+
try:
|
|
122
|
+
params = RunParams(
|
|
123
|
+
trade_date=trade_date,
|
|
124
|
+
allow_intraday=allow_intraday,
|
|
125
|
+
force_sync=force_sync,
|
|
126
|
+
daily_lookback=daily_lookback,
|
|
127
|
+
moneyflow_lookback=moneyflow_lookback,
|
|
128
|
+
)
|
|
129
|
+
runner = LubRunner(rt)
|
|
130
|
+
outcome = runner.execute_sync_only(params)
|
|
131
|
+
typer.echo(f"\nstatus: {outcome.status.value} run_id: {outcome.run_id}")
|
|
132
|
+
if outcome.error:
|
|
133
|
+
typer.echo(f"error: {outcome.error}")
|
|
134
|
+
if outcome.status.value != "success":
|
|
135
|
+
raise typer.Exit(1)
|
|
136
|
+
finally:
|
|
137
|
+
db.close()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@app.command("history")
|
|
141
|
+
def cmd_history(limit: int = typer.Option(20, "--limit")) -> None:
|
|
142
|
+
"""List recent runs of this plugin."""
|
|
143
|
+
db = Database(paths.db_path())
|
|
144
|
+
try:
|
|
145
|
+
rows = db.fetchall(
|
|
146
|
+
"SELECT run_id, trade_date, status, started_at, finished_at FROM lub_runs "
|
|
147
|
+
"ORDER BY started_at DESC LIMIT ?",
|
|
148
|
+
(limit,),
|
|
149
|
+
)
|
|
150
|
+
finally:
|
|
151
|
+
db.close()
|
|
152
|
+
if not rows:
|
|
153
|
+
typer.echo("(no runs)")
|
|
154
|
+
return
|
|
155
|
+
for r in rows:
|
|
156
|
+
typer.echo(f"{r[0]} {r[1]:<10} {r[2]:<15} {r[3]} → {r[4] or '-'}")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@app.command("report")
|
|
160
|
+
def cmd_report(
|
|
161
|
+
run_id: str = typer.Argument(..., help="Run UUID to view"),
|
|
162
|
+
full: bool = typer.Option(
|
|
163
|
+
False, "--full", help="Print the full markdown summary instead of the concise view."
|
|
164
|
+
),
|
|
165
|
+
) -> None:
|
|
166
|
+
"""Re-display a finished run's report."""
|
|
167
|
+
if full:
|
|
168
|
+
from rich.console import Console
|
|
169
|
+
from rich.markdown import Markdown
|
|
170
|
+
|
|
171
|
+
from deeptrade.theme import EVA_THEME
|
|
172
|
+
|
|
173
|
+
report_dir = paths.reports_dir() / run_id
|
|
174
|
+
summary = report_dir / "summary.md"
|
|
175
|
+
if not summary.is_file():
|
|
176
|
+
typer.echo(f"✘ no report at {summary}")
|
|
177
|
+
raise typer.Exit(2)
|
|
178
|
+
Console(theme=EVA_THEME).print(Markdown(summary.read_text(encoding="utf-8")))
|
|
179
|
+
typer.echo(f"\nReport directory: {summary.parent}")
|
|
180
|
+
return
|
|
181
|
+
render_finished_run(run_id)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
# settings — plugin-local persisted run filters (v0.4)
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _prompt_positive_float(prompt: str, current: float) -> float:
|
|
190
|
+
raw = questionary.text(f"{prompt} [{current}]:", default=str(current)).ask()
|
|
191
|
+
if raw is None:
|
|
192
|
+
raise typer.Exit(1)
|
|
193
|
+
raw = raw.strip()
|
|
194
|
+
if not raw:
|
|
195
|
+
return current
|
|
196
|
+
try:
|
|
197
|
+
v = float(raw)
|
|
198
|
+
except ValueError as e:
|
|
199
|
+
typer.echo(f"✘ 无法解析为数字: {raw!r}")
|
|
200
|
+
raise typer.Exit(2) from e
|
|
201
|
+
if v <= 0:
|
|
202
|
+
typer.echo(f"✘ 必须大于 0: {v}")
|
|
203
|
+
raise typer.Exit(2)
|
|
204
|
+
return v
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@settings_app.callback()
|
|
208
|
+
def cmd_settings(ctx: typer.Context) -> None:
|
|
209
|
+
"""交互式编辑当前持久化设置(不带子命令时进入交互;`show` 子命令仅展示)。"""
|
|
210
|
+
if ctx.invoked_subcommand is not None:
|
|
211
|
+
return
|
|
212
|
+
db = Database(paths.db_path())
|
|
213
|
+
try:
|
|
214
|
+
cur = load_config(db)
|
|
215
|
+
new_mv = _prompt_positive_float("流通市值上限(亿)", cur.max_float_mv_yi)
|
|
216
|
+
new_close = _prompt_positive_float("当前股价上限(元)", cur.max_close_yuan)
|
|
217
|
+
new_cfg = LubConfig(max_float_mv_yi=new_mv, max_close_yuan=new_close)
|
|
218
|
+
save_config(db, new_cfg)
|
|
219
|
+
typer.echo(
|
|
220
|
+
f"✔ Saved: 流通市值 < {new_cfg.max_float_mv_yi}亿、股价 < {new_cfg.max_close_yuan}元"
|
|
221
|
+
)
|
|
222
|
+
finally:
|
|
223
|
+
db.close()
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@settings_app.command("show")
|
|
227
|
+
def cmd_settings_show() -> None:
|
|
228
|
+
"""展示当前生效的设置(来源 = persisted / default)。"""
|
|
229
|
+
db = Database(paths.db_path())
|
|
230
|
+
try:
|
|
231
|
+
rows = list_for_show(db)
|
|
232
|
+
finally:
|
|
233
|
+
db.close()
|
|
234
|
+
console = Console()
|
|
235
|
+
table = Table(title="limit-up-board settings")
|
|
236
|
+
table.add_column("Key", style="cyan")
|
|
237
|
+
table.add_column("Value", overflow="fold")
|
|
238
|
+
table.add_column("Source", style="yellow")
|
|
239
|
+
for key, value, source in rows:
|
|
240
|
+
table.add_row(key, "" if value is None else str(value), source)
|
|
241
|
+
console.print(table)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
# entry point
|
|
246
|
+
# ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def main(argv: list[str]) -> int:
|
|
250
|
+
"""Plugin's dispatch entry. Returns exit code."""
|
|
251
|
+
try:
|
|
252
|
+
app(argv, standalone_mode=False)
|
|
253
|
+
return 0
|
|
254
|
+
except typer.Exit as e:
|
|
255
|
+
return int(e.exit_code or 0)
|
|
256
|
+
except SystemExit as e:
|
|
257
|
+
try:
|
|
258
|
+
return int(e.code or 0)
|
|
259
|
+
except (TypeError, ValueError):
|
|
260
|
+
return 1
|
|
261
|
+
except KeyboardInterrupt:
|
|
262
|
+
sys.stderr.write("\n✘ cancelled by user\n")
|
|
263
|
+
return 130
|
|
264
|
+
except PreconditionError as e:
|
|
265
|
+
sys.stderr.write(f"✘ {e}\n")
|
|
266
|
+
return 2
|
|
267
|
+
except Exception as e: # noqa: BLE001 — reflect to framework as exit 1
|
|
268
|
+
sys.stderr.write(f"✘ {type(e).__name__}: {e}\n")
|
|
269
|
+
return 1
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Plugin-local settings (v0.4).
|
|
2
|
+
|
|
3
|
+
Persisted in the ``lub_config`` table. Defaults live on :class:`LubConfig` and
|
|
4
|
+
are re-applied automatically when a row is missing — DB rows only override.
|
|
5
|
+
|
|
6
|
+
Currently exposed via ``deeptrade limit-up-board settings``:
|
|
7
|
+
* ``max_float_mv_yi`` — 流通市值上限(亿)
|
|
8
|
+
* ``max_close_yuan`` — 当前股价上限(元)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from dataclasses import dataclass, fields
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
18
|
+
from deeptrade.core.db import Database
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class LubConfig:
|
|
23
|
+
"""User-tunable run filters. Defaults reflect a typical 打板 watchlist."""
|
|
24
|
+
|
|
25
|
+
max_float_mv_yi: float = 100.0
|
|
26
|
+
max_close_yuan: float = 15.0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_KEY_PREFIX = "lub."
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _full_key(field_name: str) -> str:
|
|
33
|
+
return f"{_KEY_PREFIX}{field_name}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def load_config(db: Database) -> LubConfig:
|
|
37
|
+
"""Materialize a :class:`LubConfig` from ``lub_config``; missing rows fall
|
|
38
|
+
through to the dataclass default."""
|
|
39
|
+
overrides: dict[str, Any] = {}
|
|
40
|
+
for f in fields(LubConfig):
|
|
41
|
+
row = db.fetchone("SELECT value_json FROM lub_config WHERE key = ?", (_full_key(f.name),))
|
|
42
|
+
if row is not None:
|
|
43
|
+
overrides[f.name] = json.loads(row[0])
|
|
44
|
+
return LubConfig(**overrides)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def save_config(db: Database, cfg: LubConfig) -> None:
|
|
48
|
+
"""Upsert every field of *cfg* into ``lub_config``."""
|
|
49
|
+
with db.transaction():
|
|
50
|
+
for f in fields(LubConfig):
|
|
51
|
+
key = _full_key(f.name)
|
|
52
|
+
value = getattr(cfg, f.name)
|
|
53
|
+
payload = json.dumps(value)
|
|
54
|
+
db.execute("DELETE FROM lub_config WHERE key = ?", (key,))
|
|
55
|
+
db.execute(
|
|
56
|
+
"INSERT INTO lub_config(key, value_json) VALUES (?, ?)",
|
|
57
|
+
(key, payload),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def list_for_show(db: Database) -> list[tuple[str, Any, str]]:
|
|
62
|
+
"""``[(key, value, source)]`` for the ``settings show`` table.
|
|
63
|
+
|
|
64
|
+
``source`` is ``"persisted"`` if the field has a row in ``lub_config``,
|
|
65
|
+
otherwise ``"default"``.
|
|
66
|
+
"""
|
|
67
|
+
out: list[tuple[str, Any, str]] = []
|
|
68
|
+
defaults = LubConfig()
|
|
69
|
+
for f in fields(LubConfig):
|
|
70
|
+
key = _full_key(f.name)
|
|
71
|
+
row = db.fetchone("SELECT value_json FROM lub_config WHERE key = ?", (key,))
|
|
72
|
+
if row is not None:
|
|
73
|
+
out.append((key, json.loads(row[0]), "persisted"))
|
|
74
|
+
else:
|
|
75
|
+
out.append((key, getattr(defaults, f.name), "default"))
|
|
76
|
+
return out
|