"""异常与错误分类。
WCF 的错误是 HTML 页面,真正的信息藏在 "The exception message is '...'" 里。
分类的目的是决定「重试 / 跳过 / 整批中止」,而不是把所有失败都当网络问题重试。
"""
from __future__ import annotations
from enum import StrEnum
class Category(StrEnum):
NET = "NET" # 网络层:可重试
SP_MISSING = "SP_MISSING" # 存储过程不存在:配置/版本问题,不重试
PARAM = "PARAM" # 参数错误/类型转换:数据问题,不重试
EXCEL = "EXCEL" # Excel 解析失败(Column1 != 0)
TFS = "TFS" # 推送 TFS 失败
AUTH = "AUTH" # 登录失败
UNKNOWN = "UNKNOWN"
class QtError(RuntimeError):
"""带分类的接口错误。"""
def __init__(self, message: str, category: Category = Category.UNKNOWN, *, retryable: bool = False):
super().__init__(message)
self.message = message
self.category = category
self.retryable = retryable
def __str__(self) -> str: # pragma: no cover - 直通
return f"[{self.category}] {self.message}"
@classmethod
def from_http(cls, status: int, body: str) -> "QtError":
"""把 WCF 的 HTML 错误页转成可读错误。"""
message = extract_exception_message(body) or f"HTTP {status}"
lowered = message.lower()
if "could not find stored procedure" in lowered:
return cls(message, Category.SP_MISSING)
if "missing parameter" in lowered or "conversion" in lowered or "procedure or function" in lowered:
return cls(message, Category.PARAM)
if status >= 500:
return cls(message, Category.NET, retryable=True)
return cls(message, Category.UNKNOWN)
def extract_exception_message(body: str) -> str:
"""从 WCF 错误页里抠出真正的异常文本。"""
import html
import re
match = re.search(r"exception message is '(.*?)'\s*\.?\s*See server logs", body, re.S)
if match:
return html.unescape(match.group(1)).strip()
plain = re.sub(r"<[^>]+>", " ", body)
plain = html.unescape(re.sub(r"\s+", " ", plain)).strip()
return plain[:400]