354 lines
11 KiB
Python
354 lines
11 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
"""
|
|||
|
|
WindPy 环境检测与配置(macOS + Windows)。
|
|||
|
|
|
|||
|
|
核心功能: 检查 WindPy 是否可 import,不可时尝试自动修复。
|
|||
|
|
|
|||
|
|
macOS:
|
|||
|
|
WindPy.py 在 Wind API.app 内部,不在 site-packages。
|
|||
|
|
修复方式: 创建 symlink 到 site-packages + 链接 ~/.Wind 配置。
|
|||
|
|
|
|||
|
|
Windows:
|
|||
|
|
Wind 终端安装时会将 WindPy 注册到 Python 路径。
|
|||
|
|
如果不可 import,说明安装不完整,提示用户重新配置。
|
|||
|
|
|
|||
|
|
用法:
|
|||
|
|
# 作为模块
|
|||
|
|
from setup_windpy import ensure_windpy
|
|||
|
|
ensure_windpy()
|
|||
|
|
from WindPy import w
|
|||
|
|
|
|||
|
|
# 作为脚本
|
|||
|
|
python setup_windpy.py # 检测
|
|||
|
|
python setup_windpy.py --fix # 检测 + 修复
|
|||
|
|
python setup_windpy.py --verify # 检测 + 连接验证
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import os
|
|||
|
|
import platform
|
|||
|
|
import site
|
|||
|
|
import sys
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# macOS 常量
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
WIND_APP_PATH = "/Applications/Wind API.app"
|
|||
|
|
WIND_WINDPY_SOURCE_MAC = os.path.join(WIND_APP_PATH, "Contents", "python", "WindPy.py")
|
|||
|
|
WIND_DOT_DIR_CONTAINER = os.path.expanduser(
|
|||
|
|
"~/Library/Containers/com.wind.mac.api/Data/.Wind"
|
|||
|
|
)
|
|||
|
|
WIND_DOT_DIR_HOME = os.path.expanduser("~/.Wind")
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# 平台判断
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
def is_macos() -> bool:
|
|||
|
|
return platform.system() == "Darwin"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def is_windows() -> bool:
|
|||
|
|
return platform.system() == "Windows"
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# 通用检测
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
def windpy_importable() -> bool:
|
|||
|
|
"""WindPy 是否可 import。"""
|
|||
|
|
try:
|
|||
|
|
import importlib
|
|||
|
|
importlib.import_module("WindPy")
|
|||
|
|
return True
|
|||
|
|
except ImportError:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _find_windpy_location() -> str | None:
|
|||
|
|
"""查找 WindPy 在 sys.path 中的实际路径。"""
|
|||
|
|
for p in sys.path:
|
|||
|
|
for name in ("WindPy.py", "WindPy.pyd", "WindPy.so"):
|
|||
|
|
candidate = os.path.join(p, name)
|
|||
|
|
if os.path.exists(candidate):
|
|||
|
|
return candidate
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
|
|||
|
|
def verify_wind_connection() -> tuple[bool, str]:
|
|||
|
|
"""连接 Wind 并执行测试查询。返回 (success, message)。"""
|
|||
|
|
try:
|
|||
|
|
from WindPy import w
|
|||
|
|
|
|||
|
|
ret = w.start()
|
|||
|
|
if not w.isconnected():
|
|||
|
|
return False, f"w.start() 返回但未连接: {ret}"
|
|||
|
|
|
|||
|
|
ret = w.wss("000001.SZ", "sec_name", "")
|
|||
|
|
if hasattr(ret, "ErrorCode") and ret.ErrorCode != 0:
|
|||
|
|
w.stop()
|
|||
|
|
return False, f"测试查询失败 (ErrorCode={ret.ErrorCode}): {ret.Data}"
|
|||
|
|
|
|||
|
|
sec_name = ""
|
|||
|
|
if hasattr(ret, "Data") and ret.Data and ret.Data[0]:
|
|||
|
|
sec_name = str(ret.Data[0][0])
|
|||
|
|
|
|||
|
|
w.stop()
|
|||
|
|
return True, f"验证通过 — 000001.SZ = {sec_name}"
|
|||
|
|
|
|||
|
|
except ImportError:
|
|||
|
|
return False, "WindPy 导入失败"
|
|||
|
|
except Exception as e:
|
|||
|
|
return False, f"验证异常: {e}"
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# macOS: symlink 修复
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
def _force_symlink(src: str, dst: str):
|
|||
|
|
"""强制创建 symlink,已存在则替换。"""
|
|||
|
|
if os.path.islink(dst) or os.path.isfile(dst):
|
|||
|
|
os.remove(dst)
|
|||
|
|
os.symlink(src, dst)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _mac_fix_windpy_import() -> list[str]:
|
|||
|
|
"""创建 WindPy.py symlink 到 site-packages。返回创建的路径列表。"""
|
|||
|
|
if not os.path.isfile(WIND_WINDPY_SOURCE_MAC):
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
linked = []
|
|||
|
|
|
|||
|
|
# User site-packages
|
|||
|
|
user_sp = site.getusersitepackages()
|
|||
|
|
if user_sp:
|
|||
|
|
os.makedirs(user_sp, exist_ok=True)
|
|||
|
|
dst = os.path.join(user_sp, "WindPy.py")
|
|||
|
|
_force_symlink(WIND_WINDPY_SOURCE_MAC, dst)
|
|||
|
|
linked.append(dst)
|
|||
|
|
|
|||
|
|
# Global site-packages
|
|||
|
|
try:
|
|||
|
|
global_sps = site.getsitepackages()
|
|||
|
|
if global_sps:
|
|||
|
|
dst = os.path.join(global_sps[0], "WindPy.py")
|
|||
|
|
_force_symlink(WIND_WINDPY_SOURCE_MAC, dst)
|
|||
|
|
linked.append(dst)
|
|||
|
|
except (PermissionError, OSError):
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
return linked
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _mac_fix_dot_wind() -> bool:
|
|||
|
|
"""创建 ~/.Wind -> 容器目录的 symlink。"""
|
|||
|
|
if not os.path.isdir(WIND_DOT_DIR_CONTAINER):
|
|||
|
|
return False
|
|||
|
|
_force_symlink(WIND_DOT_DIR_CONTAINER, WIND_DOT_DIR_HOME)
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _mac_dot_wind_ok() -> bool:
|
|||
|
|
"""~/.Wind 是否正确链接。"""
|
|||
|
|
if not os.path.exists(WIND_DOT_DIR_HOME):
|
|||
|
|
return False
|
|||
|
|
if os.path.islink(WIND_DOT_DIR_HOME):
|
|||
|
|
return os.readlink(WIND_DOT_DIR_HOME) == WIND_DOT_DIR_CONTAINER
|
|||
|
|
return os.path.isdir(WIND_DOT_DIR_HOME)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# 诊断结果
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
class DiagResult:
|
|||
|
|
def __init__(self):
|
|||
|
|
self.platform: str = platform.system()
|
|||
|
|
self.windpy_importable: bool = False
|
|||
|
|
self.windpy_location: str | None = None
|
|||
|
|
self.dot_wind_ok: bool = True
|
|||
|
|
self.verified: bool = False
|
|||
|
|
self.verify_message: str = ""
|
|||
|
|
self.errors: list[str] = []
|
|||
|
|
self.warnings: list[str] = []
|
|||
|
|
self.fixed: list[str] = []
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def ready(self) -> bool:
|
|||
|
|
return self.windpy_importable
|
|||
|
|
|
|||
|
|
def summary(self) -> str:
|
|||
|
|
plat = "macOS" if self.platform == "Darwin" else self.platform
|
|||
|
|
lines = [
|
|||
|
|
"=" * 60,
|
|||
|
|
f"WindPy 环境诊断 ({plat})",
|
|||
|
|
"=" * 60,
|
|||
|
|
f" Python: {sys.version.split()[0]} ({sys.executable})",
|
|||
|
|
"",
|
|||
|
|
f" [{'OK' if self.windpy_importable else 'FAIL'}] WindPy 可 import",
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
if self.windpy_location:
|
|||
|
|
lines.append(f" 位置: {self.windpy_location}")
|
|||
|
|
|
|||
|
|
if self.platform == "Darwin":
|
|||
|
|
lines.append(f" [{'OK' if self.dot_wind_ok else 'WARN'}] ~/.Wind 配置目录")
|
|||
|
|
|
|||
|
|
if self.verified:
|
|||
|
|
lines.append(f" [ OK ] 连接验证: {self.verify_message}")
|
|||
|
|
elif self.verify_message:
|
|||
|
|
lines.append(f" [FAIL] 连接验证: {self.verify_message}")
|
|||
|
|
|
|||
|
|
if self.fixed:
|
|||
|
|
lines += ["", "已修复:"]
|
|||
|
|
for f in self.fixed:
|
|||
|
|
lines.append(f" [FIXED] {f}")
|
|||
|
|
|
|||
|
|
if self.warnings:
|
|||
|
|
lines += ["", "警告:"]
|
|||
|
|
for w in self.warnings:
|
|||
|
|
lines.append(f" [WARN] {w}")
|
|||
|
|
|
|||
|
|
if self.errors:
|
|||
|
|
lines += ["", "错误:"]
|
|||
|
|
for e in self.errors:
|
|||
|
|
lines.append(f" [ERROR] {e}")
|
|||
|
|
|
|||
|
|
lines += ["", f"状态: {'READY' if self.ready else 'NOT READY'}", "=" * 60]
|
|||
|
|
return "\n".join(lines)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# 诊断主流程
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
def diagnose(fix: bool = False, verify: bool = False) -> DiagResult:
|
|||
|
|
r = DiagResult()
|
|||
|
|
|
|||
|
|
# 1. 核心检查: WindPy 是否可 import
|
|||
|
|
r.windpy_importable = windpy_importable()
|
|||
|
|
r.windpy_location = _find_windpy_location()
|
|||
|
|
|
|||
|
|
# 2. 可 import → 跳到验证
|
|||
|
|
if r.windpy_importable:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# 3. 不可 import → 平台特定修复
|
|||
|
|
elif is_macos():
|
|||
|
|
if not os.path.isdir(WIND_APP_PATH):
|
|||
|
|
r.errors.append(f"Wind API.app 未安装 ({WIND_APP_PATH})")
|
|||
|
|
elif not os.path.isfile(WIND_WINDPY_SOURCE_MAC):
|
|||
|
|
r.errors.append(f"WindPy.py 不存在于 {WIND_WINDPY_SOURCE_MAC},安装可能不完整")
|
|||
|
|
elif fix:
|
|||
|
|
linked = _mac_fix_windpy_import()
|
|||
|
|
for path in linked:
|
|||
|
|
r.fixed.append(f"symlink: {path} -> {WIND_WINDPY_SOURCE_MAC}")
|
|||
|
|
import importlib
|
|||
|
|
importlib.invalidate_caches()
|
|||
|
|
r.windpy_importable = windpy_importable()
|
|||
|
|
r.windpy_location = _find_windpy_location()
|
|||
|
|
if not r.windpy_importable:
|
|||
|
|
r.errors.append("创建 symlink 后仍无法导入,请检查权限")
|
|||
|
|
else:
|
|||
|
|
r.errors.append(
|
|||
|
|
"WindPy 无法导入。运行 --fix 自动修复,或手动:\n"
|
|||
|
|
f" ln -sf '{WIND_WINDPY_SOURCE_MAC}' "
|
|||
|
|
f"'{os.path.join(site.getusersitepackages(), 'WindPy.py')}'"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
elif is_windows():
|
|||
|
|
r.errors.append(
|
|||
|
|
"WindPy 无法导入。请检查:\n"
|
|||
|
|
" 1. Wind 金融终端是否已安装\n"
|
|||
|
|
" 2. 安装时是否勾选了「Python API」组件\n"
|
|||
|
|
" 3. 当前 Python 版本是否与 Wind 支持的版本一致\n"
|
|||
|
|
" 修复: 打开 Wind 终端 → 帮助 → 修复 Python 接口"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
else:
|
|||
|
|
r.errors.append(f"不支持的平台: {r.platform}")
|
|||
|
|
return r
|
|||
|
|
|
|||
|
|
# 4. macOS: ~/.Wind 配置目录
|
|||
|
|
if is_macos():
|
|||
|
|
r.dot_wind_ok = _mac_dot_wind_ok()
|
|||
|
|
if not r.dot_wind_ok and fix:
|
|||
|
|
if _mac_fix_dot_wind():
|
|||
|
|
r.fixed.append(f"symlink: {WIND_DOT_DIR_HOME} -> {WIND_DOT_DIR_CONTAINER}")
|
|||
|
|
r.dot_wind_ok = True
|
|||
|
|
else:
|
|||
|
|
r.warnings.append("~/.Wind 配置目录不存在,请先打开 Wind API.app 并登录一次")
|
|||
|
|
elif not r.dot_wind_ok:
|
|||
|
|
r.warnings.append("~/.Wind 未正确链接,运行 --fix 修复")
|
|||
|
|
|
|||
|
|
# 5. 连接验证
|
|||
|
|
if r.windpy_importable and verify:
|
|||
|
|
r.verified, r.verify_message = verify_wind_connection()
|
|||
|
|
if not r.verified:
|
|||
|
|
r.warnings.append(f"连接验证失败: {r.verify_message}")
|
|||
|
|
|
|||
|
|
return r
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# 对外接口
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
def ensure_windpy(auto_fix: bool = True, verbose: bool = True):
|
|||
|
|
"""
|
|||
|
|
确保 WindPy 可用。不可用时自动修复(macOS: symlink)。
|
|||
|
|
|
|||
|
|
Raises:
|
|||
|
|
RuntimeError: WindPy 无法使用且无法修复。
|
|||
|
|
"""
|
|||
|
|
if windpy_importable():
|
|||
|
|
if verbose:
|
|||
|
|
loc = _find_windpy_location()
|
|||
|
|
print(f"[setup_windpy] WindPy 已就绪 ({loc})", flush=True)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
if verbose:
|
|||
|
|
print("[setup_windpy] WindPy 不可导入,开始诊断...", flush=True)
|
|||
|
|
|
|||
|
|
result = diagnose(fix=auto_fix)
|
|||
|
|
|
|||
|
|
if verbose:
|
|||
|
|
print(result.summary(), flush=True)
|
|||
|
|
|
|||
|
|
if not result.ready:
|
|||
|
|
errors = "; ".join(result.errors) if result.errors else "未知错误"
|
|||
|
|
raise RuntimeError(f"WindPy 环境配置失败: {errors}")
|
|||
|
|
|
|||
|
|
if verbose:
|
|||
|
|
print("[setup_windpy] WindPy 环境配置完成", flush=True)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
# CLI
|
|||
|
|
# ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
import argparse
|
|||
|
|
|
|||
|
|
parser = argparse.ArgumentParser(description="WindPy 环境检测与配置")
|
|||
|
|
parser.add_argument("--fix", action="store_true", help="自动修复")
|
|||
|
|
parser.add_argument("--check", action="store_true", help="仅检测不修复")
|
|||
|
|
parser.add_argument("--verify", action="store_true", help="连接 Wind 并执行测试查询")
|
|||
|
|
parser.add_argument("--quiet", action="store_true", help="静默模式,仅在失败时输出")
|
|||
|
|
args = parser.parse_args()
|
|||
|
|
|
|||
|
|
do_fix = args.fix and not args.check
|
|||
|
|
result = diagnose(fix=do_fix, verify=args.verify or args.fix)
|
|||
|
|
|
|||
|
|
if not args.quiet or not result.ready:
|
|||
|
|
print(result.summary())
|
|||
|
|
|
|||
|
|
sys.exit(0 if result.ready else 1)
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|