|
6 | 6 | from collections.abc import Iterable, Mapping |
7 | 7 | from dataclasses import dataclass |
8 | 8 | from enum import Enum |
| 9 | +from types import MappingProxyType |
9 | 10 |
|
10 | 11 |
|
11 | 12 | STATUS_VERSION = "pert.feed_status_decision.v1" |
@@ -62,7 +63,7 @@ def wire(self) -> dict[str, str]: |
62 | 63 |
|
63 | 64 | @dataclass(frozen=True) |
64 | 65 | class StatusEvidence: |
65 | | - status: dict[str, object] |
| 66 | + status: Mapping[str, object] |
66 | 67 | canonical_bytes: bytes |
67 | 68 |
|
68 | 69 |
|
@@ -133,8 +134,16 @@ def _parse_outcome(value: object) -> tuple[dict[str, object], list[_Row]]: |
133 | 134 | return {"feed_id": feed_id, "feed_url": feed_url, "kind": kind, "state": state, "error_code": error}, rows |
134 | 135 |
|
135 | 136 |
|
136 | | -def _row_key(row: _Row) -> tuple[str, str]: |
137 | | - return row.published_at, row.item_id |
| 137 | +def _row_key(row: _Row) -> tuple[str, ...]: |
| 138 | + return tuple(getattr(row, key) for key in _ROW_KEYS) |
| 139 | + |
| 140 | + |
| 141 | +def _freeze(value: object) -> object: |
| 142 | + if isinstance(value, dict): |
| 143 | + return MappingProxyType({key: _freeze(child) for key, child in value.items()}) |
| 144 | + if isinstance(value, list): |
| 145 | + return tuple(_freeze(child) for child in value) |
| 146 | + return value |
138 | 147 |
|
139 | 148 |
|
140 | 149 | def _digest(rows: list[_Row]) -> str: |
@@ -216,7 +225,7 @@ def build_status_decision(outcomes: Iterable[Mapping[str, object]]) -> StatusDec |
216 | 225 | if len(set(ids)) != len(ids): |
217 | 226 | _fail("feed_duplicate") |
218 | 227 | status = _build_status(parsed) |
219 | | - evidence = StatusEvidence(status, _canonical(status)) |
| 228 | + evidence = StatusEvidence(_freeze(status), _canonical(status)) |
220 | 229 | if status["failed_feed_count"]: |
221 | 230 | kind = DecisionKind.HARD_FAIL |
222 | 231 | elif status["quarantined_feed_count"]: |
|
0 commit comments