fix: consolidate remaining assembly validation into GatewaySettings
Round two of the from_env-only validation problem. Fifteen checks still lived in the env parsing functions: six enum domains, the redis_url requirement for redis-backed limiter/breaker/cache, cache namespace and TTL, telemetry path and DSN, non-negative structured retries and non-blank scope. from_settings and direct construction bypassed all of them. The five asserts in client.py that claimed config had already validated redis_url and the telemetry targets now hold on every path, so they revert to what CLAUDE.md permits: internal invariant declarations that also narrow the Optional for type checkers. Their comments now name the method that guarantees them, since the previous wording is exactly what went stale. Postgres DSNs built by hand now get the SQLAlchemy +driver suffix stripped the way from_env has always stripped it, with a warning so the rewrite is not silent. The env path strips earlier, so it stays quiet.
This commit is contained in:
@@ -79,10 +79,15 @@ assert settings.redis_url is not None # 内部不变量: config 已校验
|
||||
|
||||
| 选项 | 权衡 |
|
||||
|---|---|
|
||||
| **A. 构造期校验,含 `+driver` 即报错(推荐)** | 显式优于隐式;frozen dataclass 里改字段要 `object.__setattr__`,是在用户背后动他给的值。报错消息直接告诉他去掉后缀即可 |
|
||||
| B. 构造期规范化(`object.__setattr__` 剥后缀) | 与 `from_env` 行为完全对齐,调用方不用管;但 frozen 类在构造期悄悄改字段,后续 `replace`/相等性比较都会有惊喜 |
|
||||
| A. 构造期校验,含 `+driver` 即报错 | 显式,库不碰用户给的值;但两条装配路对同一输入接受度不同 |
|
||||
| B. 构造期静默剥后缀 | 两条路完全对齐;但 frozen 类在构造期悄悄改字段,调用方不知情 |
|
||||
| **C. 构造期剥后缀 + `logger.warning`(用户 2026-07-30 拍板)** | 两条路行为对齐,同时不静默——调用方在日志里看得见库动了他的值,想根治就自己改 DSN |
|
||||
|
||||
推荐 A。代价是两条装配路对同一输入的**接受度**不同(`from_env` 接受带后缀的、直接构造不接受),但两者的**产出**一致——`GatewaySettings.telemetry_pg_dsn` 永远是干净 DSN。这个不对称是有意的:env 那条路要吃下三项目历史遗留的 SQLAlchemy DSN 写法(迁移兼容),代码构造那条路没有历史包袱。
|
||||
选 C。实现要点:`object.__setattr__` 改 frozen 字段(`SourceConfig` 无此先例,但 frozen 的约束是对**外部**不可变,构造期规范化是既有 dataclass 惯用法);warning 走 loguru(核心依赖,库内 `ocr.py:183`/`embedding.py:318` 同款用法)。
|
||||
|
||||
**warning 不会打扰 env 用户**:`_load_pg_dsn` 保留现有的剥离逻辑,`from_env` 传给构造函数时 DSN 已经干净,`__post_init__` 无事可做。只有手工构造传了带后缀的 DSN 才会触发。三项目 `.env` 里那些 SQLAlchemy 写法不会每次装配刷一条 warning。
|
||||
|
||||
代价是同一件事有两处剥离逻辑。用同一个模块级 helper `_strip_dsn_driver(dsn)` 供两处调用,避免实现分叉。
|
||||
|
||||
## 6. 行为审计
|
||||
|
||||
@@ -112,7 +117,8 @@ assert settings.redis_url is not None # 内部不变量: config 已校验
|
||||
| cache namespace 缺失 / ttl ≤ 0 | 抛 `ValueError` |
|
||||
| telemetry sqlite path / pg dsn 缺失 | 抛 `ValueError` |
|
||||
| `structured_max_retries=-1`、`scope=""` | 抛 `ValueError` |
|
||||
| pg dsn 含 `+asyncpg` | 抛 `ValueError`,消息给出去后缀的写法 |
|
||||
| pg dsn 含 `+asyncpg`(直接构造) | 后缀被剥,字段值为干净 DSN,且发出一条 warning(用 `caplog`/loguru sink 断言) |
|
||||
| pg dsn 干净(直接构造)、或经 `from_env` 传入 | **不发** warning——env 路已在 `_load_pg_dsn` 剥过,不该刷噪音 |
|
||||
| 合法组合(每种 backend 组合各一) | 构造成功——收紧的是错的那些 |
|
||||
| **回归护栏**:`GatewayClient.from_settings` 走 redis 三后端的合法配置 | 装配成功,证明 assert 前提真的被保证了 |
|
||||
|
||||
@@ -120,7 +126,11 @@ TDD:先跑出红,预计 ≥14 条失败。要求同第一轮——每条实现
|
||||
|
||||
版本:**1.0.2**(patch),CHANGELOG 同样单列"行为收紧"小节。
|
||||
|
||||
## 9. 待人类拍板
|
||||
## 9. 人类拍板结论(2026-07-30)
|
||||
|
||||
1. §5 的 DSN 处置:选 A(校验拒绝,推荐)还是 B(构造期剥后缀)?
|
||||
2. §4 的 assert 保留 + 改注释,是否认同?
|
||||
| 问题 | 结论 |
|
||||
|---|---|
|
||||
| §5 DSN 处置 | **选 C**:构造期剥后缀 + `logger.warning`。不静默改用户的值,也不让两条装配路产出不一致 |
|
||||
| §4 assert 处置 | **保留,只改注释**,点明由哪个方法保证前提 |
|
||||
| 方案主体 | 沿用第一轮已批准的方案 A,无需重新论证 |
|
||||
| 版本 | 1.0.2(patch) |
|
||||
|
||||
Reference in New Issue
Block a user