deeptrade-quant 0.5.0__tar.gz → 0.7.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 (72) hide show
  1. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/CHANGELOG.md +92 -0
  2. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/PKG-INFO +1 -1
  3. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/__init__.py +1 -1
  4. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/cli.py +22 -1
  5. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/cli_plugin.py +12 -0
  6. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/core/config.py +54 -2
  7. deeptrade_quant-0.7.0/deeptrade/core/dep_installer.py +434 -0
  8. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/core/llm_client.py +31 -1
  9. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/core/paths.py +17 -1
  10. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/core/plugin_manager.py +83 -9
  11. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/core/secrets.py +66 -10
  12. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/core/tushare_client.py +49 -1
  13. deeptrade_quant-0.7.0/deeptrade/plugins_api/__init__.py +85 -0
  14. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/plugins_api/base.py +26 -3
  15. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/plugins_api/metadata.py +47 -13
  16. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/pyproject.toml +1 -1
  17. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/tests/core/test_config.py +88 -0
  18. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/tests/core/test_llm_client.py +123 -3
  19. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/tests/core/test_plugin_dependencies.py +204 -1
  20. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/tests/core/test_plugin_install.py +14 -4
  21. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/tests/core/test_plugin_security.py +66 -12
  22. deeptrade_quant-0.7.0/tests/core/test_secrets.py +178 -0
  23. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/tests/core/test_tushare_client.py +79 -0
  24. deeptrade_quant-0.7.0/tests/plugins_api/test_api_version_2.py +222 -0
  25. deeptrade_quant-0.5.0/deeptrade/core/dep_installer.py +0 -226
  26. deeptrade_quant-0.5.0/deeptrade/plugins_api/__init__.py +0 -36
  27. deeptrade_quant-0.5.0/tests/core/test_secrets.py +0 -89
  28. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/.gitignore +0 -0
  29. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/LICENSE +0 -0
  30. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/README.md +0 -0
  31. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/cli_config.py +0 -0
  32. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/cli_data.py +0 -0
  33. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/core/__init__.py +0 -0
  34. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/core/config_migrations.py +0 -0
  35. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/core/db.py +0 -0
  36. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/core/github_fetch.py +0 -0
  37. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/core/llm_manager.py +0 -0
  38. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/core/logging_config.py +0 -0
  39. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/core/migrations/__init__.py +0 -0
  40. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/core/migrations/core/20260509_001_init.sql +0 -0
  41. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/core/migrations/core/20260512_001_drop_legacy_tushare_cache.sql +0 -0
  42. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/core/migrations/core/20260515_002_affected_tables.sql +0 -0
  43. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/core/migrations/core/__init__.py +0 -0
  44. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/core/plugin_source.py +0 -0
  45. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/core/registry.py +0 -0
  46. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/core/run_status.py +0 -0
  47. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/plugins_api/errors.py +0 -0
  48. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/plugins_api/events.py +0 -0
  49. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/plugins_api/llm.py +0 -0
  50. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/deeptrade/theme.py +0 -0
  51. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/tests/__init__.py +0 -0
  52. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/tests/cli/__init__.py +0 -0
  53. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/tests/cli/test_config_cmd.py +0 -0
  54. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/tests/cli/test_plugin_cmd.py +0 -0
  55. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/tests/cli/test_routing.py +0 -0
  56. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/tests/cli/test_user_facing_strings_are_chinese.py +0 -0
  57. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/tests/conftest.py +0 -0
  58. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/tests/core/__init__.py +0 -0
  59. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/tests/core/test_config_migrations.py +0 -0
  60. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/tests/core/test_db.py +0 -0
  61. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/tests/core/test_github_fetch.py +0 -0
  62. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/tests/core/test_llm_manager.py +0 -0
  63. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/tests/core/test_paths.py +0 -0
  64. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/tests/core/test_plugin_source.py +0 -0
  65. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/tests/core/test_plugin_upgrade.py +0 -0
  66. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/tests/core/test_registry.py +0 -0
  67. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/tests/core/test_tushare_classifier.py +0 -0
  68. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/tests/core/test_tushare_retry_r1.py +0 -0
  69. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/tests/plugins_api/__init__.py +0 -0
  70. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/tests/plugins_api/test_errors.py +0 -0
  71. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/tests/plugins_api/test_protocol.py +0 -0
  72. {deeptrade_quant-0.5.0 → deeptrade_quant-0.7.0}/tests/test_smoke.py +0 -0
@@ -2,6 +2,98 @@
2
2
 
3
3
  All notable changes to DeepTrade. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and SemVer.
4
4
 
5
+ ## [v0.7.0] — 2026-05-15 — 依赖隔离稳健化 + timezone IANA 校验
6
+
7
+ 本版按 2026-05-15 评审研判文档 §5 v0.7 路线落地 2 项主线,至此 v0.5 § 5 全部"采纳项"清零:
8
+
9
+ 1. **H6 依赖隔离稳健化**:
10
+ - **dry-run 预检**:``run_install`` 之前先跑 ``uv pip install --dry-run`` 抓 uv 的 change plan;解析 ``~`` (update) / ``-`` (remove) 行,与 ``framework_core_canonicals()`` 求交集。若插件依赖会顺带升级 / 降级 / 移除任何框架核心 dep,默认拒绝,要求显式 ``--allow-core-bump`` 才放行。``uv`` 不在 PATH 时降级为不做检查(``pip`` 没有对等结构化 dry-run),保持 v0.4 装机不动手的承诺。
11
+ - **dep snapshot**:``run_install`` 之前把 ``pip list --format=freeze`` 输出落到 ``~/.deeptrade/dep_snapshots/<plugin_id>/pre-install-<UTC>.txt``。失败时错误信息把这个目录甩到用户面前,便于 ``diff`` 回滚。
12
+ - 仍**不做**:venv-per-plugin / subprocess 隔离(重型方案,违反"框架轻");框架 deps 的 pin lockfile(会污染用户项目的解析)。
13
+ 2. **L1 timezone IANA 校验**:``AppConfig.app_timezone`` 改用 ``zoneinfo.available_timezones()`` 做硬校验,把 ``Asia/Shangai`` 这种近似拼写在写时拦住,而不是延迟到第一次 ``ZoneInfo(...)`` 调用爆 ``ZoneInfoNotFoundError``。``base_url`` HTTPS 强制按方案 §4 评估**仍不做**(本地 LLM 网关 / Ollama 常用 http)。
14
+
15
+ ### Added
16
+
17
+ - ``deeptrade/core/dep_installer.py``:
18
+ - ``framework_core_canonicals()``——读取 ``deeptrade-quant`` distribution 的 ``Requires``,过滤掉 ``extra`` marker,输出框架核心 dep 的 canonical 名字集合。从源码运行(未 ``pip install -e``)时返回空集合,让预检自动降级为 no-op。
19
+ - ``preflight_dry_run(reqs, *, watched)``——best-effort 跑 ``uv pip install --dry-run``;解析 stderr / stdout 中的 ``~`` 与 ``-`` 行;返回与 ``watched`` 相交的 canonical 名字集合。``uv`` 缺席、resolution 失败、超时——一律返回空集合,让真正的 ``run_install`` 路径报底层错误,预检不抢话语权。
20
+ - ``_parse_dry_run_changes(text, watched)``——dry-run 输出解析的纯函数版本,便于单测覆盖。
21
+ - ``write_dep_snapshot(plugin_id, dir_root)``——优先 ``uv pip list``,回退 ``python -m pip list``(uv 管理的 venv 通常不带 pip);写 ``<dir_root>/<plugin_id>/pre-install-<UTC>.txt``,返回 ``Path`` 或失败时 ``None``。
22
+ - ``DRY_RUN_TIMEOUT_SECONDS`` 短超时常量,避免被卡住的 uv 阻塞真正的 install。
23
+ - ``deeptrade/core/paths.py::dep_snapshots_dir()``——``~/.deeptrade/dep_snapshots/``;纳入 ``ensure_layout()`` 自动创建。
24
+ - ``deeptrade/core/plugin_manager.py::PluginManager.install`` / ``upgrade`` 接 ``allow_core_bump: bool = False``;``_handle_dependencies`` 在 ``run_install`` 之前先跑 preflight 与 snapshot,并在 install error 信息里附上 snapshot 目录 hint。
25
+ - ``deeptrade/cli_plugin.py``:``plugin install`` 与 ``plugin upgrade`` 加 ``--allow-core-bump`` flag(中文 help 文案)。
26
+ - ``deeptrade/core/config.py::AppConfig._validate_app_timezone``——基于 ``zoneinfo.available_timezones()`` 的 ``@field_validator``;非法值抛 Pydantic ``ValidationError`` 并指出如何打印当前主机支持的全部 zones。
27
+ - 测试:``tests/core/test_config.py`` 5 个新用例(默认值通过、典型 IANA 名通过、typo / 空串 / 任意字符串拒绝)。``tests/core/test_plugin_dependencies.py`` 10 个新用例(``framework_core_canonicals`` 列出 must-have、``_parse_dry_run_changes`` 识别 update / remove、忽略 install 行、uv 缺席返回空集、空 requirements 返回空集、watched 为空跳过 subprocess、``_handle_dependencies`` 默认拒绝 core bump 与 ``--allow-core-bump`` 放行、``write_dep_snapshot`` 写文件、pip 缺失返回 ``None``、CLI 双向)。
28
+
29
+ ### Changed
30
+
31
+ - ``deeptrade/core/plugin_manager.py::_handle_dependencies``:新增 ``allow_core_bump`` kwarg;``run_install`` 调用包了 ``try/except DepInstallError``,把异常包裹一层附上 snapshot 目录提示,便于排障。
32
+ - ``deeptrade/core/dep_installer.py::write_dep_snapshot``:拆出 ``_snapshot_argv()`` 帮助选择 ``uv pip list`` vs ``python -m pip list``,与 ``detect_installer()`` 的优先级一致。
33
+
34
+ ### Migration notes
35
+
36
+ - **用户视角零强制动作**:默认行为是更严的(核心 dep 改动需要显式确认),但常见插件不会动框架核心 dep——绝大多数 ``deeptrade plugin install <短名>`` 命令仍然零摩擦。
37
+ - **遇到核心依赖冲突如何处理**:v0.7 之后若看到 ``plugin install would change framework core dep(s) [...]`` 的拒绝信息,先看 dry-run 报告的具体包:
38
+ - 如果是 ``duckdb`` / ``pydantic`` / ``click`` / ``typer`` 等加载链路上的运行时关键件——大概率是插件 spec 写得太死,反馈给插件作者放宽;强行 ``--allow-core-bump`` 可能让框架进程立刻 import 失败。
39
+ - 如果只是 minor 版本提升且已读过 release note——加 ``--allow-core-bump`` 显式确认放行。
40
+ - **uv 缺席的主机**:``preflight_dry_run`` 自动跳过,行为与 v0.4 一致。``write_dep_snapshot`` 也能用 ``python -m pip list`` 兜底(虽然 uv-only venv 上 pip 通常缺席,此时 snapshot 静默 no-op)。
41
+ - **dep_snapshots 目录的清理**:框架本身不做轮转。如果该目录变大,可以放心 ``rm`` 老的 ``pre-install-*.txt`` 文件——它们只用于回溯,不影响运行时。
42
+ - **timezone**:升级到 v0.7 之后第一次 ``Database()`` 打开会触发自动迁移,迁移本身不动 ``app.timezone``;但如果你之前不小心存了 typo 进 ``app_config``,下一次 ``ConfigService.get_app_config()`` 会抛 ``ValidationError``。修法:``deeptrade config set app.timezone Asia/Shanghai``(或你本地的正确 IANA 名)。
43
+
44
+ ## [v0.6.0] — 2026-05-15 — 插件运行时 API + LLM 方言 + 杂项收尾
45
+
46
+ 本版按 2026-05-15 评审研判文档 `DeepTrade-review-assessment-and-fix-plan-2026-05-15.md` §5 中 v0.6 路线落地 6 项主线:
47
+
48
+ 1. **H4 插件运行时 API**:``api_version="2"`` 的插件改用 ``dispatch(ctx, argv)`` 接收 ``PluginContext``,无需再 ``import deeptrade.core.*`` 私有路径取 db / config。``api_version="1"`` 永久向后兼容,运行时按 metadata 决定调用形态。``plugins_api/__init__.py`` 把 ``LLMManager`` / ``TushareClient`` 提升为公共 API。
49
+ 2. **H5 LLM 方言收口**:``OpenAICompatTransport`` 加 class 属性 ``supports_reasoning_effort`` 默认 False;新增 ``OpenAIOfficialTransport``(base_url 含 ``api.openai.com``)改 True。其余 OpenAI-compat provider 不再裸送 ``reasoning_effort``,消除国内非推理模型常见的 400 与忽略两种失败模式。
50
+ 3. **H1 hard error**:``table_prefix`` 派生不匹配从 v0.5 的 ``DeprecationWarning`` 转 ``ValueError``,与显式声明路径合流,让"插件表必须落在派生命名空间"成为可执行约束。
51
+ 4. **M6**:``env_var_for(key)`` 把 provider 名中的 ``-`` 归一化为 ``_``,避免 ``DEEPTRADE_LLM_QWEN-PLUS_API_KEY`` 这种 bash/sh 非法标识符;``set_llm_provider`` 在归一化后冲突时硬拒绝,避免 ``qwen-plus`` / ``qwen_plus`` 共用同一个 env var 时的静默 shadowing。
52
+ 5. **M8**:``_try_load_keyring`` 改用 ``keyring.get_keyring()`` 的非写入式 backend 探测,只在 ``backends.fail.*`` / ``backends.null.*`` 类时返回 None;结果缓存到模块级变量,配 ``_invalidate_keyring_cache`` 给测试用。彻底消除 ``SecretStore.__init__`` 每次都向 OS 凭据存储写一个 ``__probe__`` 的副作用。
53
+ 6. **M9**:``TushareClient`` 首次遇未知 API 改打 INFO 日志(进程级 + 实例级双 dedup,不再每次默认就刷屏);``PluginMetadata.permissions.tushare_apis.cache_overrides: dict[str, str]`` 让插件作者声明非默认缓存策略,``TushareClient(cache_overrides=...)`` 在分类决策中优先采纳。
54
+
55
+ ### Added
56
+
57
+ - ``deeptrade/plugins_api/base.py``:docstring 标注 ``api_version`` 与 ``dispatch`` 签名的对应关系;``Plugin.dispatch`` 改写为变参 Protocol 占位(``runtime_checkable`` 只校验属性存在,签名由框架按 metadata 决定)。
58
+ - ``deeptrade/core/plugin_manager.py::SUPPORTED_API_VERSIONS``(``frozenset({"1", "2"})``);install / upgrade 路径用集合包含判断替换原先的 ``!= CURRENT_API_VERSION``,对未来扩 ``"3"`` 留接口。
59
+ - ``deeptrade/cli.py::_dispatch``:按 ``rec.api_version`` 选择 ``plugin.dispatch(argv)`` 或 ``plugin.dispatch(ctx, argv)``;v2 路径由框架开 ``Database`` + 构 ``PluginContext`` + 收尾关库,插件不再操心生命周期。
60
+ - ``deeptrade/plugins_api/__init__.py``:重导出 ``LLMManager`` 与 ``TushareClient``,正式纳入公共 surface 的 ``__all__``。
61
+ - ``deeptrade/plugins_api/metadata.py::TushareApiPermissions.cache_overrides: dict[str, str]``,配 ``_cache_overrides_values_valid`` model_validator 强制值落在 ``{static, trade_day_immutable, trade_day_mutable, hot_or_anns}``;非法值在 yaml 解析阶段拒绝,避免运行时缓存行为静默退化。
62
+ - ``deeptrade/core/tushare_client.py``:``TushareClient.__init__`` 新增 ``cache_overrides`` kwarg;新增 ``_resolve_cache_class`` 把"插件 overrides → 框架表 → 默认 + 一次性 INFO 日志"三级优先级集中表达;模块级 ``_UNKNOWN_API_LOGGED`` 做进程级 dedup。
63
+ - ``deeptrade/core/llm_client.py``:``OpenAICompatTransport.supports_reasoning_effort: bool = False`` 类标志位;``OpenAIOfficialTransport`` 子类(``api.openai.com`` base_url 触发)将其覆盖为 True;``chat()`` 仅在 ``supports_reasoning_effort and reasoning_effort`` 时把字段写入 ``kwargs``。``_TRANSPORT_BY_BASE_URL`` 新增 ``("api.openai.com", OpenAIOfficialTransport)`` 路由。
64
+ - ``deeptrade/core/secrets.py``:模块级 ``_keyring_cache`` / ``_keyring_probed`` 缓存;``_invalidate_keyring_cache`` 测试钩子;``_try_load_keyring`` 重写为按 backend FQCN 子串识别 fail/null 类。
65
+ - ``deeptrade/core/config.py::ConfigService.set_llm_provider``:归一化后冲突检测;``env_var_for`` 文档说明 ``-``→``_`` 规则的动机。
66
+ - 测试新增:``tests/plugins_api/test_api_version_2.py``(6 用例,覆盖双 api_version 安装、unknown api_version 拒绝、v1/v2 dispatch arity、公开类导出)、``tests/core/test_secrets.py`` v0.6 M8 节 3 用例(fail backend 拒绝、real backend 不写探针、cache 可重置)、``tests/core/test_config.py`` 3 用例(env 归一化、归一化冲突拒绝、同名更新放行)、``tests/core/test_plugin_security.py`` 3 用例(cache_overrides 默认空 / 合法值 / 非法值)、``tests/core/test_tushare_client.py`` 3 用例(overrides 优先、未知 API override 路径、INFO 日志一次性)、``tests/core/test_llm_client.py`` 4 用例(supports_reasoning_effort 默认 False、OpenAI-official 路由、Generic transport 不发送、empty 字符串不发送)。
67
+
68
+ ### Changed
69
+
70
+ - ``deeptrade/plugins_api/metadata.py::_tables_match_prefix``:``table_prefix`` 省略且不匹配派生时从 ``warnings.warn(DeprecationWarning)`` 转 ``raise ValueError``;删除现已无用的 ``import warnings``。模块顶部 docstring 把 v0.5 advisory / v0.6 enforcement 的边界明确标注。
71
+ - ``deeptrade/plugins_api/metadata.py``:``TushareApiPermissions`` 加 ``cache_overrides`` 字段;定义 ``_CACHE_CLASS_VALUES`` 内部常量便于校验。
72
+ - ``deeptrade/core/tushare_client.py``:``cache_class = API_CACHE_CLASS.get(api_name, "trade_day_immutable")`` 改为 ``cache_class = self._resolve_cache_class(api_name)``,集中所有分类决策。
73
+ - ``deeptrade/cli.py``:v2 路径在 dispatch 周边内联 ``ConfigService`` / ``PluginContext`` 导入,避免顶层冷启动负担;其余跨 dispatch 路径行为不变。
74
+ - ``tests/core/test_plugin_security.py``:``test_plugin_table_prefix_warns_when_omitted_and_mismatched`` 改名 ``test_plugin_table_prefix_hard_fails_when_omitted_and_mismatched``,正向断言 ``ValidationError``;``test_plugin_table_prefix_no_warning_when_tables_match_derived`` 改名 ``test_plugin_table_prefix_passes_when_tables_match_derived``,保留 ``simplefilter("error", DeprecationWarning)`` 作为反向断言。
75
+ - ``tests/core/test_plugin_install.py::_make_minimal_plugin_dir``:``table_name`` 默认值从硬编码 ``test_table`` 改为按 ``plugin_id`` 派生(``<pkg>_t``),满足 v0.6 派生前缀约束;``table_ddl`` 默认也从默认值同步派生。
76
+ - ``tests/core/test_plugin_dependencies.py::_minimal_meta_dict``:表名 ``x_t`` 改为 ``minimal_x_t`` 以匹配 ``plugin_id="minimal-x"`` 的派生前缀。
77
+ - ``tests/core/test_llm_client.py::test_select_transport_class_defaults_to_generic``:``api.openai.com`` 从"回退到 Generic"断言改为"回退用 Generic / OpenAI-official 走专属路由"的双行声明;docstring 标注 v0.6 行为变化。
78
+
79
+ ### Migration notes
80
+
81
+ - **插件作者侧(推荐路径)**:迁移到 ``api_version="2"``——
82
+ 1. ``deeptrade_plugin.yaml::api_version`` 改为 ``"2"``。
83
+ 2. ``def dispatch(self, argv):`` 改写为 ``def dispatch(self, ctx, argv):``。
84
+ 3. 把 ``Database(paths.db_path())`` / ``ConfigService(...)`` 等手动构造替换为 ``ctx.db`` / ``ctx.config``;之前从 ``deeptrade.core.*`` import 的 ``LLMManager`` / ``TushareClient`` 改为 ``from deeptrade.plugins_api import LLMManager, TushareClient``。
85
+ - **插件作者侧(最低改动路径)**:不动 ``api_version="1"``,所有既有插件**零代码改动**继续工作;唯一硬约束是 ``metadata.tables`` 必须落在 ``<plugin_id 派生前缀>_*`` 命名空间内(或显式声明 ``table_prefix``)。
86
+ - **多 LLM provider 命名**:使用 ``qwen-plus`` / ``qwen-max-2024`` 这类带连字符的 provider 名时,``DEEPTRADE_LLM_QWEN_PLUS_API_KEY`` 形式的 env var 现在可以正常生效;不要再同时注册归一化后冲突的名字(``qwen-plus`` 与 ``qwen_plus``)。
87
+ - **官方 OpenAI 推理模型**:``reasoning_effort`` 现在只在 ``base_url`` 含 ``api.openai.com`` 时上行。其他 OpenAI-compat 网关如需该字段(少见),临时方案是 fork 当前 transport 类并覆写 ``supports_reasoning_effort = True``;后续版本会按需要在路由表里追加。
88
+ - **Tushare 缓存策略覆盖**:插件作者可在 ``deeptrade_plugin.yaml`` 写
89
+ ```yaml
90
+ permissions:
91
+ tushare_apis:
92
+ cache_overrides:
93
+ moneyflow_industry_ths: trade_day_mutable
94
+ ```
95
+ 把框架表里没列出的 API 显式钉到正确的缓存类,避免依赖默认 ``trade_day_immutable``。运行时由插件自己把这份 dict 作为 ``cache_overrides=...`` 传给 ``TushareClient``。
96
+
5
97
  ## [v0.5.0] — 2026-05-15 — 插件信任边界 + CLI 中文化
6
98
 
7
99
  本版主线落地 2026-05-15 评审研判文档 `DeepTrade-review-assessment-and-fix-plan-2026-05-15.md` 中的 v0.5 路线:把"插件可以声明并清掉框架核心表"这条被忽视的信任边界明确收口,让框架的 SQL 迁移有边界、插件加载有 sys.modules 守卫、升级路径不会被静默改动的历史 migration 蒙混过关;同时把命令行用户向文案统一为中文,建立首道防回退的回归测试。整体不引入新框架概念(拒绝了报告里的 RuntimeContext 大对象、独立 venv、SQL DSL 等重型方案),保持"框架轻、插件重"的原则。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deeptrade-quant
3
- Version: 0.5.0
3
+ Version: 0.7.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
@@ -2,5 +2,5 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.5.0"
5
+ __version__ = "0.7.0"
6
6
  __all__ = ["__version__"]
@@ -126,8 +126,29 @@ def _build_plugin_command(plugin_id: str) -> click.Command | None:
126
126
  if not hasattr(plugin, "dispatch"):
127
127
  typer.echo(f"✘ 插件 {plugin_id!r} 未实现 dispatch()")
128
128
  raise typer.Exit(2)
129
+
130
+ argv = list(ctx.args)
129
131
  try:
130
- rc = plugin.dispatch(list(ctx.args))
132
+ # v0.6 H4 — dispatch arity is selected by ``metadata.api_version``.
133
+ # ``"1"`` (legacy) keeps the historical ``dispatch(argv)`` signature
134
+ # so already-shipped plugins remain runnable without a rebuild.
135
+ # ``"2"`` receives the same ``PluginContext`` shape passed to
136
+ # ``validate_static`` at install time, removing the need for plugins
137
+ # to import ``deeptrade.core.*`` private modules.
138
+ if rec.api_version == "2":
139
+ from deeptrade.core.config import ConfigService
140
+ from deeptrade.plugins_api.base import PluginContext
141
+
142
+ # Reuse the framework's open Database / dispose lifecycle —
143
+ # plugins should NOT manage the framework's DB handle.
144
+ db = Database(paths.db_path())
145
+ try:
146
+ plugin_ctx = PluginContext(db=db, config=ConfigService(db), plugin_id=plugin_id)
147
+ rc = plugin.dispatch(plugin_ctx, argv)
148
+ finally:
149
+ db.close()
150
+ else:
151
+ rc = plugin.dispatch(argv)
131
152
  except (SystemExit, KeyboardInterrupt):
132
153
  # Exit codes / Ctrl-C must propagate unaltered.
133
154
  raise
@@ -75,6 +75,11 @@ def cmd_install(
75
75
  reinstall_deps: bool = typer.Option(
76
76
  False, "--reinstall-deps", help="对全部依赖重新运行安装器(uv/pip --upgrade)"
77
77
  ),
78
+ allow_core_bump: bool = typer.Option(
79
+ False,
80
+ "--allow-core-bump",
81
+ help="允许该插件的依赖安装顺带升级 / 降级 / 移除框架核心 dep(默认拒绝)",
82
+ ),
78
83
  ) -> None:
79
84
  """从注册表 / GitHub URL / 本地目录安装一个插件。"""
80
85
  resolver = SourceResolver()
@@ -109,6 +114,7 @@ def cmd_install(
109
114
  resolved.path,
110
115
  install_deps=not no_deps,
111
116
  reinstall_deps=reinstall_deps,
117
+ allow_core_bump=allow_core_bump,
112
118
  )
113
119
  except PluginInstallError as e:
114
120
  typer.echo(f"✘ 安装失败:{e}")
@@ -269,6 +275,11 @@ def cmd_upgrade(
269
275
  reinstall_deps: bool = typer.Option(
270
276
  False, "--reinstall-deps", help="对全部依赖重新运行安装器(uv/pip --upgrade)"
271
277
  ),
278
+ allow_core_bump: bool = typer.Option(
279
+ False,
280
+ "--allow-core-bump",
281
+ help="允许该插件的依赖升级顺带升级 / 降级 / 移除框架核心 dep(默认拒绝)",
282
+ ),
272
283
  ) -> None:
273
284
  """升级一个已安装的插件。
274
285
 
@@ -292,6 +303,7 @@ def cmd_upgrade(
292
303
  resolved.path,
293
304
  install_deps=not no_deps,
294
305
  reinstall_deps=reinstall_deps,
306
+ allow_core_bump=allow_core_bump,
295
307
  )
296
308
  except PluginNotFoundError as e:
297
309
  try:
@@ -118,6 +118,28 @@ class AppConfig(BaseModel):
118
118
  return time(int(parts[0]), int(parts[1]), int(parts[2]))
119
119
  return v
120
120
 
121
+ @field_validator("app_timezone")
122
+ @classmethod
123
+ def _validate_app_timezone(cls, v: str) -> str:
124
+ """v0.7 L1 — reject ``app.timezone`` values that aren't IANA zone names.
125
+
126
+ ``zoneinfo.available_timezones()`` is the authoritative source on the
127
+ runtime host (Python 3.11+; reads ``/usr/share/zoneinfo`` on Linux,
128
+ falls back to the bundled ``tzdata`` wheel on Windows). Without this,
129
+ a typo like ``Asia/Shangai`` reaches the eventual ``ZoneInfo(...)``
130
+ call and crashes much later with a confusing ``ZoneInfoNotFoundError``.
131
+ """
132
+ from zoneinfo import available_timezones # noqa: PLC0415
133
+
134
+ zones = available_timezones()
135
+ if v not in zones:
136
+ raise ValueError(
137
+ f"app.timezone {v!r} is not a valid IANA zone; "
138
+ f"see `python -c 'import zoneinfo; print(sorted(zoneinfo.available_timezones()))'` "
139
+ f"for the list this host supports"
140
+ )
141
+ return v
142
+
121
143
  @field_validator("llm_providers", mode="before")
122
144
  @classmethod
123
145
  def _parse_llm_providers(cls, v: Any) -> Any:
@@ -173,8 +195,20 @@ def llm_api_key_name(key: str) -> str | None:
173
195
 
174
196
 
175
197
  def env_var_for(key: str) -> str:
176
- """Map dotted key → DEEPTRADE_<UPPER_SNAKE> env var name."""
177
- return "DEEPTRADE_" + key.upper().replace(".", "_")
198
+ """Map dotted key → ``DEEPTRADE_<UPPER_SNAKE>`` env var name.
199
+
200
+ Hyphens are normalized to underscores so that hyphen-containing provider
201
+ names (e.g. ``llm.qwen-plus.api_key``) yield a POSIX-valid identifier
202
+ (``DEEPTRADE_LLM_QWEN_PLUS_API_KEY``). Without this, the env var
203
+ ``DEEPTRADE_LLM_QWEN-PLUS_API_KEY`` cannot be set via ``export`` on
204
+ bash / sh — variable names there must match ``[A-Za-z_][A-Za-z0-9_]*``.
205
+
206
+ Collision risk: provider names ``qwen-plus`` and ``qwen_plus`` would map
207
+ to the same env var. :meth:`ConfigService.set_llm_provider` refuses such
208
+ a registration so users discover the conflict at write time rather than
209
+ at first env-var read.
210
+ """
211
+ return "DEEPTRADE_" + key.upper().replace(".", "_").replace("-", "_")
178
212
 
179
213
 
180
214
  def known_keys() -> list[str]:
@@ -364,6 +398,24 @@ class ConfigService:
364
398
  )
365
399
  current_raw = self.get("llm.providers")
366
400
  current: dict[str, Any] = dict(current_raw) if isinstance(current_raw, dict) else {}
401
+
402
+ # M6 — refuse a name that collides with an existing provider after
403
+ # ``env_var_for`` normalization (``-`` → ``_``). Without this, two
404
+ # providers ``qwen-plus`` and ``qwen_plus`` would share the env var
405
+ # ``DEEPTRADE_LLM_QWEN_PLUS_API_KEY`` and silently shadow each other
406
+ # on read. Surface the conflict at registration time.
407
+ normalized = name.replace("-", "_")
408
+ for existing_name in current:
409
+ if existing_name == name:
410
+ continue
411
+ if existing_name.replace("-", "_") == normalized:
412
+ raise ValueError(
413
+ f"provider name {name!r} collides with {existing_name!r} after "
414
+ f"env-var normalization (both map to "
415
+ f"{env_var_for(f'llm.{name}.api_key')!r}); rename one to avoid "
416
+ f"silent env-var shadowing"
417
+ )
418
+
367
419
  is_first = len(current) == 0
368
420
  existing = dict(current.get(name) or {})
369
421
  prior_default = bool(existing.get("is_default", False))