chore: require python 3.12 and adopt PEP 695 type parameters
The telemetry write budget needs asyncio.timeout, whose uncancel accounting was only fixed after 3.11.1 — pinning the floor at 3.12 removes that hazard instead of working around it. Raising ruff's target-version turns on UP047, so gather_bounded, _anext_within and stream_with_liveness_timeouts move to def f[T](...) and the two module-level TypeVars go away. That syntax is a SyntaxError on 3.11, so it can only land together with the version bump.
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
- **核心目标**: PolyGateway = 统一的大语言模型(LLM/VLM/OCR,音频预留)调度与中转库。治理单位是**一次模型调用**:请求封装、多源多账号、限流、错误分类与重试、熔断、Redis 响应缓存、流式看门狗、遥测(含成本)、结构化输出策略。全组件端口化可插拔。
|
||||
- **架构权威文档**: `research-wiki/ARCHITECTURE.md`(架构单一事实源,含 D1-D14 决策及讨论过程、子系统设计、三项目迁移验收标准;**不受 400 行设计文档限制**,以无歧义传达既有讨论为准绳)。开发顺序见 `research-wiki/ROADMAP.md`;`research-wiki/designs/` 仅存放每次实现具体功能的设计文档。
|
||||
- **参考项目**: `reference/` 下三个项目是本库的需求来源与代码蓝本(**只读,勿改**;M4 起"只读"指工作区文件与 main 检出不变——迁移实施经 `git worktree` 在 `~/Projects/m4-worktrees/` 的 feature 分支进行,worktree 的 git 操作会写 `reference/*/.git` 元数据,属预期);库必须能按 ARCHITECTURE.md §11 被它们迁移接入,否则即边界缺口。
|
||||
- **技术栈**: Python 3.11+,核心仅依赖 `httpx` + `pydantic`,其余(redis/sqlite/postgres/json_repair/openai)一律 optional extras。conda 环境 `PolyGateway`。
|
||||
- **技术栈**: Python 3.12+,核心仅依赖 `httpx` + `pydantic`,其余(redis/sqlite/postgres/json_repair/openai)一律 optional extras。conda 环境 `PolyGateway`。
|
||||
|
||||
## 2. 常用命令
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ pip install --extra-index-url https://gitea.iomgaa.online/api/packages/iomgaa/py
|
||||
| `structured` | json-repair | 结构化输出的修复策略 |
|
||||
| `sdk` | openai | 可选的 SDK transport(默认手写 httpx,不需要) |
|
||||
|
||||
要求 Python ≥ 3.11。
|
||||
要求 Python ≥ 3.12。
|
||||
|
||||
## 快速开始
|
||||
|
||||
@@ -461,7 +461,7 @@ graph LR
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
conda create -n PolyGateway python=3.11 && conda activate PolyGateway
|
||||
conda create -n PolyGateway python=3.12 && conda activate PolyGateway
|
||||
make install # editable 安装(dev + 全部 extras)
|
||||
make test # pytest + 覆盖率(目标 ≥80%)
|
||||
make lint # ruff + import-linter
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ description = "PolyGateway:实验室统一的大语言模型(LLM/VLM/OCR)
|
||||
# registry 包页面的正文只认这一项:缺了页面就是一片空白(1.1.2 的教训,twine 会警告
|
||||
# long_description missing 但不阻塞上传)。README 在打包时被固化进产物,发布后再改无效。
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"httpx>=0.27",
|
||||
"pydantic>=2.8",
|
||||
@@ -56,7 +56,7 @@ markers = [
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py311"
|
||||
target-version = "py312"
|
||||
line-length = 100
|
||||
|
||||
[tool.ruff.lint]
|
||||
|
||||
@@ -13,7 +13,7 @@ import hashlib
|
||||
import json
|
||||
import random
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any, Literal, TypeVar
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
|
||||
from polygateway.backends.memory.breaker import InMemoryGate
|
||||
from polygateway.backends.memory.cache import InMemoryCache
|
||||
@@ -63,8 +63,6 @@ if TYPE_CHECKING:
|
||||
SourceConfig,
|
||||
)
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
def _guard_thinking(
|
||||
sources: list[SourceConfig],
|
||||
@@ -442,7 +440,7 @@ def _build_structured(
|
||||
return None, None
|
||||
|
||||
|
||||
async def gather_bounded(aws: Iterable[Awaitable[_T]], *, concurrency: int) -> list[_T]:
|
||||
async def gather_bounded[T](aws: Iterable[Awaitable[T]], *, concurrency: int) -> list[T]:
|
||||
"""有界并发 gather(D5 便利函数,替代 VT 手搓 semaphore+gather 样板)。
|
||||
|
||||
语义与 `asyncio.gather` 默认一致: 结果保序、首个异常上抛;仅增加并发上限。
|
||||
@@ -451,7 +449,7 @@ async def gather_bounded(aws: Iterable[Awaitable[_T]], *, concurrency: int) -> l
|
||||
raise ValueError("concurrency 必须 ≥ 1")
|
||||
sem = asyncio.Semaphore(concurrency)
|
||||
|
||||
async def _run(aw: Awaitable[_T]) -> _T:
|
||||
async def _run(aw: Awaitable[T]) -> T:
|
||||
async with sem:
|
||||
return await aw
|
||||
|
||||
|
||||
@@ -14,13 +14,11 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import contextlib
|
||||
import time
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
class StreamLivenessTimeout(Exception): # noqa: N818 — 三项目冻结的公共名
|
||||
"""流活性超时异常。
|
||||
@@ -38,14 +36,14 @@ class StreamLivenessTimeout(Exception): # noqa: N818 — 三项目冻结的公
|
||||
super().__init__(f"流活性超时({kind}, elapsed={elapsed_s:.1f}s)")
|
||||
|
||||
|
||||
async def _anext_within(
|
||||
it: AsyncIterator[_T],
|
||||
async def _anext_within[T](
|
||||
it: AsyncIterator[T],
|
||||
timeout_s: float,
|
||||
*,
|
||||
kind: str,
|
||||
start: float,
|
||||
first: bool,
|
||||
) -> _T:
|
||||
) -> T:
|
||||
"""限时取下一项;本层 deadline 触发抛 StreamLivenessTimeout(kind)。
|
||||
|
||||
上游自抛的 TimeoutError 用 cm.expired() 区分,原样上抛不误吞。
|
||||
@@ -59,13 +57,13 @@ async def _anext_within(
|
||||
raise StreamLivenessTimeout(kind, time.monotonic() - start, not first) from None
|
||||
|
||||
|
||||
async def stream_with_liveness_timeouts(
|
||||
source: AsyncIterator[_T],
|
||||
async def stream_with_liveness_timeouts[T](
|
||||
source: AsyncIterator[T],
|
||||
*,
|
||||
ttft_s: float,
|
||||
inter_token_s: float,
|
||||
total_s: float,
|
||||
) -> AsyncIterator[_T]:
|
||||
) -> AsyncIterator[T]:
|
||||
"""逐项产出 source,并施加三层活性超时。
|
||||
|
||||
关键实现: 超时**只包裹单次 __anext__**,绝不包裹 yield——否则总时长
|
||||
|
||||
Reference in New Issue
Block a user