fix(deps): 声明 httpx[http2] / h2,修复干净环境缺包启动失败
utils/api_client.py 用 httpx.AsyncClient(http2=True) 发请求,需要 h2 包。 旧 requirements 只写了 httpx,未声明 http2 extra;开发机碰巧已装 h2 所以 未暴露,干净 release 环境启动即报错: Using http2=True, but the 'h2' package is not installed. - requirements.txt: httpx -> httpx[http2] - requirements.lock.txt: httpx[http2]==0.28.1,并显式锁 h2==4.1.0 - scripts/gen_requirements_lock.py: 升级为可解析 extras,自动锁定 extra 引入的传递依赖(如 httpx[http2] -> h2),避免以后重生 lock 丢失 h2; 通用于任意 extra Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
582f292114
commit
04cdf964af
@ -13,7 +13,8 @@ flask==2.3.3
|
|||||||
flask-socketio==5.3.6
|
flask-socketio==5.3.6
|
||||||
flask-cors==3.0.10
|
flask-cors==3.0.10
|
||||||
werkzeug==2.3.7
|
werkzeug==2.3.7
|
||||||
httpx==0.28.1
|
httpx[http2]==0.28.1
|
||||||
|
h2==4.1.0 # 由 httpx[http2] 引入
|
||||||
openai==2.8.1
|
openai==2.8.1
|
||||||
cryptography==43.0.3
|
cryptography==43.0.3
|
||||||
pillow==9.3.0
|
pillow==9.3.0
|
||||||
|
|||||||
@ -4,7 +4,7 @@ flask
|
|||||||
flask-socketio
|
flask-socketio
|
||||||
flask-cors
|
flask-cors
|
||||||
werkzeug
|
werkzeug
|
||||||
httpx
|
httpx[http2]
|
||||||
openai
|
openai
|
||||||
cryptography
|
cryptography
|
||||||
pillow
|
pillow
|
||||||
|
|||||||
@ -35,41 +35,93 @@ _LOCK_HEADER = """\
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def _parse_requirement_names(text: str) -> list[str]:
|
def _parse_requirements(text: str) -> list[tuple[str, list[str]]]:
|
||||||
"""从 requirements.txt 提取包名(忽略注释、空行、已带版本约束的尾巴)。"""
|
"""从 requirements.txt 提取 (包名, extras 列表)。
|
||||||
names: list[str] = []
|
|
||||||
|
忽略注释/空行;去掉版本约束;保留 extras。
|
||||||
|
例: httpx[http2]>=0.28 -> ("httpx", ["http2"]) ; flask -> ("flask", [])
|
||||||
|
"""
|
||||||
|
items: list[tuple[str, list[str]]] = []
|
||||||
for raw in text.splitlines():
|
for raw in text.splitlines():
|
||||||
line = raw.strip()
|
line = raw.strip()
|
||||||
if not line or line.startswith("#"):
|
if not line or line.startswith("#"):
|
||||||
continue
|
continue
|
||||||
# 去掉版本约束/extras,仅留包名: flask[async]>=2 -> flask
|
# 先剥掉版本约束/环境标记: name[extra]>=1 ; marker
|
||||||
name = re.split(r"[<>=!~\[ ;]", line, 1)[0].strip()
|
head = re.split(r"[<>=!~ ;]", line, 1)[0].strip()
|
||||||
if name:
|
m = re.match(r"^([A-Za-z0-9._-]+)(?:\[([^\]]*)\])?$", head)
|
||||||
names.append(name)
|
if not m:
|
||||||
return names
|
continue
|
||||||
|
name = m.group(1)
|
||||||
|
extras = [e.strip() for e in (m.group(2) or "").split(",") if e.strip()]
|
||||||
|
items.append((name, extras))
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
def _extra_dependency_names(pkg: str, extras: list[str]) -> list[str]:
|
||||||
|
"""解析某包在指定 extras 下引入的依赖包名(用于一并锁定其传递依赖)。
|
||||||
|
|
||||||
|
例: httpx + ["http2"] -> ["h2"]。仅返回包名,版本由调用方查实际安装值。
|
||||||
|
无法解析时返回空列表(静默降级)。
|
||||||
|
"""
|
||||||
|
if not extras:
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
requires = md.requires(pkg) or []
|
||||||
|
except md.PackageNotFoundError:
|
||||||
|
return []
|
||||||
|
wanted = {e.lower() for e in extras}
|
||||||
|
found: list[str] = []
|
||||||
|
for req in requires:
|
||||||
|
# 仅取声明了 extra == '<name>' 且匹配我们启用的 extra 的依赖
|
||||||
|
if "extra ==" not in req:
|
||||||
|
continue
|
||||||
|
if not any(f"extra == '{e}'" in req or f'extra == "{e}"' in req for e in wanted):
|
||||||
|
continue
|
||||||
|
dep = re.split(r"[<>=!~ ;]", req.strip(), 1)[0].strip()
|
||||||
|
dep = re.sub(r"\[[^\]]*\]", "", dep) # 去掉 extra 自身可能带的 []
|
||||||
|
if dep and dep not in found:
|
||||||
|
found.append(dep)
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
if not REQ_PATH.exists():
|
if not REQ_PATH.exists():
|
||||||
print(f"找不到 {REQ_PATH}", file=sys.stderr)
|
print(f"找不到 {REQ_PATH}", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
names = _parse_requirement_names(REQ_PATH.read_text(encoding="utf-8"))
|
items = _parse_requirements(REQ_PATH.read_text(encoding="utf-8"))
|
||||||
if not names:
|
if not items:
|
||||||
print("requirements.txt 中没有可锁定的依赖。", file=sys.stderr)
|
print("requirements.txt 中没有可锁定的依赖。", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
lines: list[str] = []
|
lines: list[str] = []
|
||||||
missing: list[str] = []
|
missing: list[str] = []
|
||||||
for name in names:
|
seen: set[str] = set() # 去重(extra 依赖可能与直接依赖重复)
|
||||||
|
|
||||||
|
for name, extras in items:
|
||||||
|
spec = f"{name}[{','.join(extras)}]" if extras else name
|
||||||
try:
|
try:
|
||||||
version = md.version(name)
|
version = md.version(name)
|
||||||
lines.append(f"{name}=={version}")
|
lines.append(f"{spec}=={version}")
|
||||||
except md.PackageNotFoundError:
|
except md.PackageNotFoundError:
|
||||||
missing.append(name)
|
missing.append(name)
|
||||||
lines.append(f"# {name} (未安装,无法锁定版本)")
|
lines.append(f"# {spec} (未安装,无法锁定版本)")
|
||||||
|
seen.add(name.lower())
|
||||||
|
|
||||||
|
# 一并锁定该包在启用 extras 下引入的依赖(如 httpx[http2] -> h2),
|
||||||
|
# 否则干净环境装 lock 时不会拉到这些可选依赖。
|
||||||
|
for dep in _extra_dependency_names(name, extras):
|
||||||
|
if dep.lower() in seen:
|
||||||
|
continue
|
||||||
|
seen.add(dep.lower())
|
||||||
|
try:
|
||||||
|
dep_ver = md.version(dep)
|
||||||
|
lines.append(f"{dep}=={dep_ver} # 由 {name}[{','.join(extras)}] 引入")
|
||||||
|
except md.PackageNotFoundError:
|
||||||
|
missing.append(dep)
|
||||||
|
lines.append(f"# {dep} (由 {name} extra 引入,但未安装)")
|
||||||
|
|
||||||
LOCK_PATH.write_text(_LOCK_HEADER + "\n" + "\n".join(lines) + "\n", encoding="utf-8")
|
LOCK_PATH.write_text(_LOCK_HEADER + "\n" + "\n".join(lines) + "\n", encoding="utf-8")
|
||||||
print(f"已写出 {LOCK_PATH}({len(names)} 项依赖)")
|
print(f"已写出 {LOCK_PATH}({len(items)} 项直接依赖 + extra 传递依赖)")
|
||||||
for line in lines:
|
for line in lines:
|
||||||
print(" " + line)
|
print(" " + line)
|
||||||
if missing:
|
if missing:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user