Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/list_api/list_framework.py: 48%

324 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 12:01 +0000

1"""Generic list handling for LiteLLM-defined collection routes. 

2 

3A resource declares a `ListSpec`; `build_query_plan` turns query parameters into a 

4`QueryPlan` or an RFC 9457 problem without touching a database, and `handle_list` 

5runs that plan through an injected `ListExecutor`. Keeping the planning pure is what 

6lets a caller assert the plan as a value instead of asserting against a live Prisma 

7client, and it keeps this module free of any database dependency. 

8 

9A plan's `where` is a tuple of frozen `Predicate`s rather than a backend-shaped 

10mapping, so the framework never has to know which query builder executes it and a 

11planned predicate cannot be rewritten afterwards. `where_sql` renders one for a 

12raw-SQL executor with every caller-supplied value bound to a placeholder. 

13""" 

14 

15from collections.abc import Callable, Mapping, Sequence 

16from dataclasses import dataclass 

17from datetime import datetime, timezone 

18from functools import partial, reduce 

19from math import ceil 

20from typing import Final, Generic, Literal, Protocol, TypeVar 

21 

22from fastapi import Request 

23from pydantic import TypeAdapter, ValidationError 

24from typing_extensions import assert_never 

25 

26from litellm.proxy._types import UserAPIKeyAuth 

27from litellm.proxy.list_api.common import ( 

28 PROBLEM_TYPE_BASE, 

29 ManagementProblem, 

30 build_list_links, 

31 build_page_links, 

32 escape_like, 

33 unknown_query_param_problem, 

34) 

35from litellm.types.proxy.management_endpoints.management_v1 import ( 

36 FacetListResponse, 

37 ListMeta, 

38 ListResponse, 

39 PageMeta, 

40 ProblemDetail, 

41) 

42 

43ComparisonOp = Literal["eq", "gte", "lte", "gt", "lt", "contains", "not"] 

44# `is_null` is not in the design doc's operator set. It is here because there is no 

45# other way to ask for "max_budget IS NULL", and a table that renders nulls as 

46# "Unlimited" has to be able to filter on them. 

47FilterOp = ComparisonOp | Literal["in", "is_null"] 

48 

49FilterType = type[str] | type[int] | type[float] | type[datetime] 

50FilterValue = str | int | float | datetime 

51 

52PAGE_PARAM: Final = "page" 

53PAGE_SIZE_PARAM: Final = "page_size" 

54SORT_PARAM: Final = "sort" 

55SEARCH_PARAM: Final = "q" 

56 

57TRow = TypeVar("TRow") 

58TRow_co = TypeVar("TRow_co", covariant=True) 

59TOut = TypeVar("TOut") 

60 

61_FILTER_OP_ADAPTER: Final[TypeAdapter[FilterOp]] = TypeAdapter(FilterOp) 

62 

63 

64@dataclass(frozen=True, slots=True) 

65class Compare: 

66 """`field <op> value`.""" 

67 

68 field: str 

69 op: ComparisonOp 

70 value: FilterValue 

71 

72 

73@dataclass(frozen=True, slots=True) 

74class Within: 

75 """`field IN (values)`.""" 

76 

77 field: str 

78 values: tuple[FilterValue, ...] 

79 

80 

81@dataclass(frozen=True, slots=True) 

82class IsNull: 

83 """`field IS NULL`, or `IS NOT NULL` when negated.""" 

84 

85 field: str 

86 negated: bool 

87 

88 

89@dataclass(frozen=True, slots=True) 

90class AnyOf: 

91 """Disjunction of its clauses. `?q=` is the only producer. 

92 

93 Holding leaves rather than predicates keeps the disjunction one level deep by type, so 

94 neither the SQL renderer nor an in-memory executor has to walk a tree to evaluate it. 

95 """ 

96 

97 clauses: tuple[Compare, ...] 

98 

99 

100Predicate = Compare | Within | IsNull | AnyOf 

101 

102 

103@dataclass(frozen=True, slots=True) 

104class FilterSpec: 

105 type: FilterType 

106 ops: frozenset[FilterOp] 

107 

108 

109@dataclass(frozen=True, slots=True) 

110class SortKey: 

111 field: str 

112 descending: bool 

113 

114 

115@dataclass(frozen=True, slots=True) 

116class ScopeAll: 

117 """The caller may read every row of the resource.""" 

118 

119 

120@dataclass(frozen=True, slots=True) 

121class ScopeWhere: 

122 """The caller may read the rows matching every predicate in `where`.""" 

123 

124 where: tuple[Predicate, ...] 

125 

126 

127@dataclass(frozen=True, slots=True) 

128class ScopeDenied: 

129 """The caller may read no rows at all, and should be told so rather than shown an empty page.""" 

130 

131 reason: str 

132 

133 

134Scope = ScopeAll | ScopeWhere | ScopeDenied 

135 

136 

137@dataclass(frozen=True, slots=True) 

138class ListSpec(Generic[TRow, TOut]): 

139 resource: str 

140 sortable: frozenset[str] 

141 searchable: frozenset[str] 

142 filters: Mapping[str, FilterSpec] 

143 default_sort: tuple[SortKey, ...] 

144 default_page_size: int 

145 max_page_size: int 

146 scope: Callable[[UserAPIKeyAuth], Scope] 

147 serialize: Callable[[TRow], TOut] 

148 tiebreaker: str 

149 

150 def __post_init__(self) -> None: 

151 """A malformed spec is a programming error at import time, so this raises rather than 

152 returning a problem: there is no request in flight and no caller to answer.""" 

153 if not 1 <= self.default_page_size <= self.max_page_size: 153 ↛ 154line 153 didn't jump to line 154 because the condition on line 153 was never true

154 raise ValueError( 

155 f"{self.resource}: default_page_size must be between 1 and max_page_size " 

156 f"({self.max_page_size}), got {self.default_page_size}. A default above the cap " 

157 f"would serve more rows than the resource allows whenever page_size is omitted." 

158 ) 

159 if not self.tiebreaker: 159 ↛ 160line 159 didn't jump to line 160 because the condition on line 159 was never true

160 raise ValueError(f"{self.resource}: tiebreaker is required; it is the final sort key on every query.") 

161 undeclared: Final = tuple(sorted(frozenset(key.field for key in self.default_sort) - self.sortable)) 

162 if undeclared: 162 ↛ 163line 162 didn't jump to line 163 because the condition on line 162 was never true

163 raise ValueError(f"{self.resource}: default_sort orders by non-sortable field(s): {', '.join(undeclared)}.") 

164 non_text: Final = tuple( 

165 sorted(field for field, spec in self.filters.items() if "contains" in spec.ops and spec.type is not str) 

166 ) 

167 if non_text: 167 ↛ 168line 167 didn't jump to line 168 because the condition on line 167 was never true

168 raise ValueError( 

169 f"{self.resource}: contains renders as ILIKE and is only meaningful on text columns, " 

170 f"but is declared on: {', '.join(non_text)}." 

171 ) 

172 

173 

174@dataclass(frozen=True, slots=True) 

175class QueryPlan: 

176 """`where` is an implicit AND, ordered scope-first; `order` always ends with the spec's tiebreaker.""" 

177 

178 where: tuple[Predicate, ...] 

179 order: tuple[SortKey, ...] 

180 skip: int 

181 take: int 

182 

183 

184class ListExecutor(Protocol[TRow_co]): 

185 """The database half of a list, injected so this module never imports Prisma.""" 

186 

187 async def count(self, where: tuple[Predicate, ...]) -> int: ... 187 ↛ exitline 187 didn't return from function 'count' because

188 

189 async def find_many(self, plan: QueryPlan) -> Sequence[TRow_co]: ... 189 ↛ exitline 189 didn't return from function 'find_many' because

190 

191 

192class FacetExecutor(Protocol): 

193 """The half of a facet that knows the rows. Separate from `ListExecutor` so a SQL 

194 executor is not forced to implement `distinct` to keep serving entity lists.""" 

195 

196 async def distinct(self, field: str, where: tuple[Predicate, ...]) -> Sequence[str]: ... 196 ↛ exitline 196 didn't return from function 'distinct' because

197 

198 

199def order_by_sql(order: tuple[SortKey, ...]) -> str: 

200 """`ORDER BY` body for a plan, NULLS LAST in both directions. 

201 

202 Postgres sorts nulls last ascending but first descending, so an unqualified flip of 

203 the sort direction drags every "Unlimited" row to the top of the table. Every field 

204 reaching here is either a member of `ListSpec.sortable` (caller-supplied sort is 

205 checked against it, `default_sort` at construction) or the spec's `tiebreaker`, so 

206 these are developer-declared column names, never caller-controlled text. 

207 """ 

208 return ", ".join(f'"{key.field}" {"DESC" if key.descending else "ASC"} NULLS LAST' for key in order) 

209 

210 

211def _sql_operator(op: ComparisonOp) -> str: 

212 match op: 

213 case "eq": 

214 return "=" 

215 case "not": 

216 return "<>" 

217 case "gte": 

218 return ">=" 

219 case "lte": 

220 return "<=" 

221 case "gt": 

222 return ">" 

223 case "lt": 

224 return "<" 

225 case "contains": 

226 return "ILIKE" 

227 case _: 

228 assert_never(op) 

229 

230 

231def _placeholder(index: int, value: FilterValue) -> str: 

232 """`$n`, cast when the bind is a datetime. 

233 

234 Binds cross into the query engine as JSON, so a datetime arrives as text and 

235 Postgres refuses `timestamp >= text` outright. Prisma stores DateTime as a naive 

236 `TIMESTAMP(3)` holding UTC, so the bind is read as an instant and then dropped to 

237 naive UTC to match the column, the same cast `/spend/logs/ui` applies. 

238 """ 

239 return f"${index}::timestamptz AT TIME ZONE 'UTC'" if isinstance(value, datetime) else f"${index}" 

240 

241 

242def _render(predicate: Predicate, index: int) -> tuple[str, tuple[object, ...]]: 

243 match predicate: 

244 case IsNull(field=field, negated=negated): 

245 return f'"{field}" IS {"NOT NULL" if negated else "NULL"}', () 

246 case Within(field=field, values=values): 

247 placeholders: Final = ", ".join(_placeholder(index + offset, value) for offset, value in enumerate(values)) 

248 return f'"{field}" IN ({placeholders})', values 

249 case AnyOf(clauses=clauses): 

250 rendered, params = _render_all(clauses, index) 

251 return f"({' OR '.join(rendered)})", params 

252 case Compare(field=field, op="contains", value=value): 

253 return f"\"{field}\" ILIKE ${index} ESCAPE '\\'", (f"%{escape_like(str(value))}%",) 

254 case Compare(field=field, op=op, value=value): 

255 return f'"{field}" {_sql_operator(op)} {_placeholder(index, value)}', (value,) 

256 case _: 

257 assert_never(predicate) 

258 

259 

260def _render_one( 

261 rendered: tuple[tuple[str, ...], tuple[object, ...]], 

262 predicate: Predicate, 

263 first_index: int, 

264) -> tuple[tuple[str, ...], tuple[object, ...]]: 

265 """Append one predicate, numbering it after the binds already consumed.""" 

266 clauses, params = rendered 

267 clause, clause_params = _render(predicate, first_index + len(params)) 

268 return (*clauses, clause), (*params, *clause_params) 

269 

270 

271def _render_all(predicates: tuple[Predicate, ...], index: int) -> tuple[tuple[str, ...], tuple[object, ...]]: 

272 """Render every predicate, numbering placeholders continuously across them. 

273 

274 Folded rather than self-recursive: walking a predicate list is a running index, and 

275 recursing per predicate grew the stack with the filter count for nothing. `_render` 

276 still re-enters here for `AnyOf`, whose clauses are plain `Compare`s from `?q=`, so 

277 that nesting is one level deep and cannot be driven deeper by a caller. 

278 """ 

279 return reduce(partial(_render_one, first_index=index), predicates, ((), ())) 

280 

281 

282def where_sql(where: tuple[Predicate, ...], first_index: int = 1) -> tuple[str, tuple[object, ...]]: 

283 """`WHERE` body and its bind parameters, numbered from `first_index`. 

284 

285 Returns `("", ())` when there is nothing to filter on. Every caller-supplied value 

286 becomes a `$n` placeholder rather than being written into the SQL text; only column 

287 names reach the text, and those come from the spec's own declarations. 

288 """ 

289 clauses, params = _render_all(where, first_index) 

290 return " AND ".join(clauses), params 

291 

292 

293def _problem(slug: str, title: str, status: int, detail: str, allowed: tuple[str, ...] | None = None) -> ProblemDetail: 

294 return ProblemDetail( 

295 type=f"{PROBLEM_TYPE_BASE}{slug}", 

296 title=title, 

297 status=status, 

298 detail=detail, 

299 allowed=sorted(allowed) if allowed is not None else None, 

300 ) 

301 

302 

303def _invalid(detail: str) -> ProblemDetail: 

304 return _problem("invalid-query-parameter", "Invalid query parameter", 400, detail) 

305 

306 

307def _parse_filter_key(name: str) -> tuple[str, FilterOp] | None: 

308 """`filter[max_budget][gte]` -> `("max_budget", "gte")`; bare `filter[status]` -> `("status", "eq")`.""" 

309 if not name.startswith("filter[") or not name.endswith("]"): 

310 return None 

311 field, separator, raw_op = name[len("filter[") : -1].partition("][") 

312 if not separator: 

313 return field, "eq" 

314 try: 

315 return field, _FILTER_OP_ADAPTER.validate_python(raw_op) 

316 except ValidationError: 

317 return None 

318 

319 

320def _is_known_param(spec: ListSpec[TRow, TOut], name: str) -> bool: 

321 if name in (PAGE_PARAM, PAGE_SIZE_PARAM): 

322 return True 

323 if name == SORT_PARAM: 

324 return bool(spec.sortable) 

325 if name == SEARCH_PARAM: 

326 return bool(spec.searchable) 

327 parsed: Final = _parse_filter_key(name) 

328 return parsed is not None and parsed[0] in spec.filters 

329 

330 

331def _allowed_params(spec: ListSpec[TRow, TOut]) -> tuple[str, ...]: 

332 return tuple( 

333 sorted( 

334 (PAGE_PARAM, PAGE_SIZE_PARAM) 

335 + ((SORT_PARAM,) if spec.sortable else ()) 

336 + ((SEARCH_PARAM,) if spec.searchable else ()) 

337 + tuple( 

338 f"filter[{field}]" if op == "eq" else f"filter[{field}][{op}]" 

339 for field, filter_spec in spec.filters.items() 

340 for op in filter_spec.ops 

341 ) 

342 ) 

343 ) 

344 

345 

346def _parse_positive_int(name: str, raw: str) -> int | ProblemDetail: 

347 try: 

348 value: Final = int(raw) 

349 except ValueError: 

350 return _invalid(f"'{name}' must be an integer.") 

351 if value < 1: 

352 return _invalid(f"'{name}' must be 1 or greater.") 

353 return value 

354 

355 

356def _parse_page(params: Mapping[str, str]) -> int | ProblemDetail: 

357 raw: Final = params.get(PAGE_PARAM) 

358 return 1 if raw is None else _parse_positive_int(PAGE_PARAM, raw) 

359 

360 

361def _parse_page_size(spec: ListSpec[TRow, TOut], params: Mapping[str, str]) -> int | ProblemDetail: 

362 raw: Final = params.get(PAGE_SIZE_PARAM) 

363 if raw is None: 363 ↛ 365line 363 didn't jump to line 365 because the condition on line 363 was always true

364 return spec.default_page_size 

365 value: Final = _parse_positive_int(PAGE_SIZE_PARAM, raw) 

366 if isinstance(value, ProblemDetail): 

367 return value 

368 return min(value, spec.max_page_size) 

369 

370 

371def _parse_sort(spec: ListSpec[TRow, TOut], params: Mapping[str, str]) -> tuple[SortKey, ...] | ProblemDetail: 

372 raw: Final = params.get(SORT_PARAM) 

373 if raw is None: 373 ↛ 375line 373 didn't jump to line 375 because the condition on line 373 was always true

374 return spec.default_sort 

375 segments: Final = tuple(segment.strip() for segment in raw.split(",")) 

376 keys = tuple(SortKey(field=segment.removeprefix("-"), descending=segment.startswith("-")) for segment in segments) 

377 rejected: Final = tuple(sorted(frozenset(key.field for key in keys) - spec.sortable)) 

378 if rejected: 

379 return _problem( 

380 "invalid-sort-field", 

381 "Invalid sort field", 

382 400, 

383 f"Cannot sort {spec.resource} by: {', '.join(repr(field) for field in rejected)}.", 

384 tuple(spec.sortable), 

385 ) 

386 # A repeated field cannot change the ordering, but an executor that sorts once per key 

387 # does the work anyway. Rejecting repeats bounds that to the size of `sortable`, which 

388 # matters because an unauthenticated caller can otherwise name one field a thousand times. 

389 fields: Final = tuple(key.field for key in keys) 

390 repeated: Final = tuple(sorted(frozenset(field for field in fields if fields.count(field) > 1))) 

391 if repeated: 

392 return _problem( 

393 "duplicate-sort-field", 

394 "Duplicate sort field", 

395 400, 

396 f"Sort field(s) named more than once: {', '.join(repeated)}. Each may appear once.", 

397 tuple(spec.sortable), 

398 ) 

399 return keys 

400 

401 

402def _to_utc(value: datetime) -> datetime: 

403 return value.replace(tzinfo=timezone.utc) if value.tzinfo is None else value.astimezone(timezone.utc) 

404 

405 

406def _coerce(field: str, op: FilterOp, raw: str, target: FilterType) -> FilterValue | ProblemDetail: 

407 try: 

408 if target is str: 

409 return raw 

410 if target is int: 

411 return int(raw) 

412 if target is float: 

413 return float(raw) 

414 return _to_utc(datetime.fromisoformat(raw[:-1] + "+00:00" if raw.endswith("Z") else raw)) 

415 except ValueError: 

416 return _invalid(f"'filter[{field}][{op}]' is not a valid {target.__name__}: {raw!r}.") 

417 

418 

419def _null_predicate(field: str, raw: str) -> Predicate | ProblemDetail: 

420 if raw.lower() == "true": 

421 return IsNull(field=field, negated=False) 

422 if raw.lower() == "false": 

423 return IsNull(field=field, negated=True) 

424 return _invalid(f"'filter[{field}][is_null]' must be 'true' or 'false'.") 

425 

426 

427def _within_predicate(field: str, raw: str, target: FilterType) -> Predicate | ProblemDetail: 

428 coerced: Final = tuple(_coerce(field, "in", item.strip(), target) for item in raw.split(",")) 

429 problems: Final = tuple(item for item in coerced if isinstance(item, ProblemDetail)) 

430 if problems: 

431 return problems[0] 

432 return Within(field=field, values=tuple(item for item in coerced if not isinstance(item, ProblemDetail))) 

433 

434 

435def _parse_filter(field: str, op: FilterOp, raw: str, filter_spec: FilterSpec) -> Predicate | ProblemDetail: 

436 if op not in filter_spec.ops: 

437 return _problem( 

438 "unsupported-filter-operator", 

439 "Unsupported filter operator", 

440 400, 

441 f"Operator '{op}' is not supported on '{field}'.", 

442 tuple(filter_spec.ops), 

443 ) 

444 if op == "is_null": 

445 return _null_predicate(field, raw) 

446 if op == "in": 

447 return _within_predicate(field, raw, filter_spec.type) 

448 value: Final = _coerce(field, op, raw, filter_spec.type) 

449 if isinstance(value, ProblemDetail): 

450 return value 

451 return Compare(field=field, op=op, value=value) 

452 

453 

454def _parse_filters(spec: ListSpec[TRow, TOut], params: Mapping[str, str]) -> tuple[Predicate, ...] | ProblemDetail: 

455 keys: Final = tuple( 

456 (name, parsed) 

457 for name in sorted(params) 

458 if (parsed := _parse_filter_key(name)) is not None and parsed[0] in spec.filters 

459 ) 

460 parsed = tuple(_parse_filter(field, op, params[name], spec.filters[field]) for name, (field, op) in keys) 

461 problems: Final = tuple(item for item in parsed if isinstance(item, ProblemDetail)) 

462 if problems: 462 ↛ 463line 462 didn't jump to line 463 because the condition on line 462 was never true

463 return problems[0] 

464 return tuple(item for item in parsed if not isinstance(item, ProblemDetail)) 

465 

466 

467def _search_predicate(spec: ListSpec[TRow, TOut], params: Mapping[str, str]) -> Predicate | None: 

468 raw: Final = params.get(SEARCH_PARAM) 

469 if not raw: 469 ↛ 471line 469 didn't jump to line 471 because the condition on line 469 was always true

470 return None 

471 return AnyOf(clauses=tuple(Compare(field=field, op="contains", value=raw) for field in sorted(spec.searchable))) 

472 

473 

474def _scope_predicates(scope: Scope) -> tuple[Predicate, ...] | ProblemDetail: 

475 match scope: 

476 case ScopeAll(): 476 ↛ 478line 476 didn't jump to line 478 because the pattern on line 476 always matched

477 return () 

478 case ScopeWhere(where=where): 

479 return where 

480 case ScopeDenied(reason=reason): 

481 return _problem("forbidden", "Forbidden", 403, reason) 

482 case _: 

483 assert_never(scope) 

484 

485 

486def build_query_plan( 

487 spec: ListSpec[TRow, TOut], 

488 params: Mapping[str, str], 

489 caller: UserAPIKeyAuth, 

490) -> QueryPlan | ProblemDetail: 

491 """Turn query parameters into a plan, or into the problem that explains why they are not one.""" 

492 scope_predicates: Final = _scope_predicates(spec.scope(caller)) 

493 if isinstance(scope_predicates, ProblemDetail): 493 ↛ 494line 493 didn't jump to line 494 because the condition on line 493 was never true

494 return scope_predicates 

495 

496 unknown: Final = tuple(sorted(name for name in params if not _is_known_param(spec, name))) 

497 if unknown: 497 ↛ 498line 497 didn't jump to line 498 because the condition on line 497 was never true

498 return unknown_query_param_problem(unknown=unknown, allowed=_allowed_params(spec)) 

499 

500 page: Final = _parse_page(params) 

501 if isinstance(page, ProblemDetail): 501 ↛ 502line 501 didn't jump to line 502 because the condition on line 501 was never true

502 return page 

503 

504 page_size: Final = _parse_page_size(spec, params) 

505 if isinstance(page_size, ProblemDetail): 505 ↛ 506line 505 didn't jump to line 506 because the condition on line 505 was never true

506 return page_size 

507 

508 sort: Final = _parse_sort(spec, params) 

509 if isinstance(sort, ProblemDetail): 509 ↛ 510line 509 didn't jump to line 510 because the condition on line 509 was never true

510 return sort 

511 

512 filters: Final = _parse_filters(spec, params) 

513 if isinstance(filters, ProblemDetail): 513 ↛ 514line 513 didn't jump to line 514 because the condition on line 513 was never true

514 return filters 

515 

516 search: Final = _search_predicate(spec, params) 

517 return QueryPlan( 

518 # Scope first: conjuncts a caller filter sits behind and cannot replace. 

519 where=scope_predicates + filters + ((search,) if search is not None else ()), 

520 # Ordering by an all-null column without a unique final key lets Postgres return 

521 # the same row on two different pages. 

522 order=sort + (SortKey(field=spec.tiebreaker, descending=False),), 

523 skip=(page - 1) * page_size, 

524 take=page_size, 

525 ) 

526 

527 

528def _facet_allowed_params(spec: ListSpec[TRow, TOut]) -> tuple[str, ...]: 

529 """A facet's values are always ascending, so `sort` is not one of its parameters.""" 

530 return tuple(name for name in _allowed_params(spec) if name != SORT_PARAM) 

531 

532 

533def _facet_where( 

534 spec: ListSpec[TRow, TOut], 

535 params: Mapping[str, str], 

536 caller: UserAPIKeyAuth, 

537) -> tuple[Predicate, ...] | ProblemDetail: 

538 scope_predicates: Final = _scope_predicates(spec.scope(caller)) 

539 if isinstance(scope_predicates, ProblemDetail): 539 ↛ 540line 539 didn't jump to line 540 because the condition on line 539 was never true

540 return scope_predicates 

541 filters: Final = _parse_filters(spec, params) 

542 if isinstance(filters, ProblemDetail): 542 ↛ 543line 542 didn't jump to line 543 because the condition on line 542 was never true

543 return filters 

544 search: Final = _search_predicate(spec, params) 

545 return scope_predicates + filters + ((search,) if search is not None else ()) 

546 

547 

548async def handle_facet( 

549 spec: ListSpec[TRow, TOut], 

550 executor: FacetExecutor, 

551 request: Request, 

552 caller: UserAPIKeyAuth, 

553 field: str, 

554) -> FacetListResponse: 

555 """The distinct values one column takes over a filtered query on a resource. 

556 

557 Carries the parent's parameters so a filter dropdown offers exactly the values the 

558 table can show, and `has_more` rather than a total, which would cost a COUNT(*) over 

559 the whole match set on every keystroke. 

560 """ 

561 params: Final = request.query_params 

562 unknown: Final = tuple(sorted(name for name in params if name == SORT_PARAM or not _is_known_param(spec, name))) 

563 if unknown: 563 ↛ 564line 563 didn't jump to line 564 because the condition on line 563 was never true

564 raise ManagementProblem(unknown_query_param_problem(unknown=unknown, allowed=_facet_allowed_params(spec))) 

565 

566 duplicates: Final = _duplicate_params(request) 

567 if duplicates: 567 ↛ 568line 567 didn't jump to line 568 because the condition on line 567 was never true

568 raise ManagementProblem( 

569 _problem( 

570 "duplicate-query-parameter", 

571 "Duplicate query parameter", 

572 400, 

573 f"Repeated query parameter(s): {', '.join(duplicates)}. Each may appear once; " 

574 f"use a comma-separated list for multiple filter values.", 

575 ) 

576 ) 

577 

578 page: Final = _parse_page(params) 

579 if isinstance(page, ProblemDetail): 579 ↛ 580line 579 didn't jump to line 580 because the condition on line 579 was never true

580 raise ManagementProblem(page) 

581 page_size: Final = _parse_page_size(spec, params) 

582 if isinstance(page_size, ProblemDetail): 582 ↛ 583line 582 didn't jump to line 583 because the condition on line 582 was never true

583 raise ManagementProblem(page_size) 

584 

585 where: Final = _facet_where(spec, params, caller) 

586 if isinstance(where, ProblemDetail): 586 ↛ 587line 586 didn't jump to line 587 because the condition on line 586 was never true

587 raise ManagementProblem(where) 

588 

589 values: Final = await executor.distinct(field, where) 

590 skip: Final = (page - 1) * page_size 

591 window: Final = values[skip : skip + page_size + 1] 

592 has_more: Final = len(window) > page_size 

593 return FacetListResponse( 

594 data=tuple(window[:page_size]), 

595 meta=PageMeta(page=page, page_size=page_size, has_more=has_more), 

596 links=build_page_links(request=request, page=page, has_more=has_more), 

597 ) 

598 

599 

600def _duplicate_params(request: Request) -> tuple[str, ...]: 

601 names: Final = tuple(name for name, _ in request.query_params.multi_items()) 

602 return tuple(sorted(frozenset(name for name in names if names.count(name) > 1))) 

603 

604 

605async def handle_list( 

606 spec: ListSpec[TRow, TOut], 

607 executor: ListExecutor[TRow], 

608 request: Request, 

609 caller: UserAPIKeyAuth, 

610) -> ListResponse[TOut]: 

611 """Plan, execute, count, serialize, envelope. Failures reach the client as RFC 9457 problems.""" 

612 plan: Final = build_query_plan(spec=spec, params=request.query_params, caller=caller) 

613 if isinstance(plan, ProblemDetail): 613 ↛ 614line 613 didn't jump to line 614 because the condition on line 613 was never true

614 raise ManagementProblem(plan) 

615 

616 # Checked here rather than in build_query_plan because a Mapping[str, str] cannot 

617 # represent a repeat: query_params.get() silently keeps the last one, so ?page=1&page=999 

618 # would page from 999 without the caller ever being told which value won. 

619 duplicates: Final = _duplicate_params(request) 

620 if duplicates: 620 ↛ 621line 620 didn't jump to line 621 because the condition on line 620 was never true

621 raise ManagementProblem( 

622 _problem( 

623 "duplicate-query-parameter", 

624 "Duplicate query parameter", 

625 400, 

626 f"Repeated query parameter(s): {', '.join(duplicates)}. Each may appear once; " 

627 f"use a comma-separated list for multiple sort keys or filter values.", 

628 ) 

629 ) 

630 

631 total_count: Final = await executor.count(plan.where) 

632 rows: Final = await executor.find_many(plan) 

633 total_pages: Final = ceil(total_count / plan.take) 

634 page: Final = plan.skip // plan.take + 1 

635 return ListResponse[TOut]( 

636 data=tuple(spec.serialize(row) for row in rows), 

637 meta=ListMeta( 

638 total_count=total_count, 

639 page=page, 

640 page_size=plan.take, 

641 total_pages=total_pages, 

642 ), 

643 links=build_list_links(request=request, page=page, total_pages=total_pages), 

644 )