From 1df5ba1b3be8dbaf79e9994eb9b0b168f997edc5 Mon Sep 17 00:00:00 2001 From: MauriceMohr Date: Thu, 4 Jun 2026 18:12:33 +0200 Subject: [PATCH 1/2] Simplify bounty sorting key selection --- app/bounty_sorting.py | 58 +++++++++++++---------- tests/test_bounty_sorting.py | 90 ++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 25 deletions(-) create mode 100644 tests/test_bounty_sorting.py diff --git a/app/bounty_sorting.py b/app/bounty_sorting.py index dbf2f151..3957b92e 100644 --- a/app/bounty_sorting.py +++ b/app/bounty_sorting.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Callable from decimal import Decimal from typing import Any @@ -15,6 +16,7 @@ } BOUNTY_SORT_OPTIONS = tuple(BOUNTY_SORT_LABELS) BOUNTY_SORT_ERROR = f"sort must be one of: {', '.join(BOUNTY_SORT_OPTIONS)}" +BountySortKey = Callable[[dict[str, Any]], Any] def normalize_bounty_sort(sort: str | None) -> str: @@ -29,30 +31,36 @@ def normalize_bounty_sort(sort: str | None) -> str: return normalized_sort +def _newest_sort_key(bounty: dict[str, Any]) -> int: + return int(bounty["id"]) + + +def _reward_sort_key(bounty: dict[str, Any]) -> tuple[Decimal, int]: + return Decimal(str(bounty["reward_mrwk"])), int(bounty["id"]) + + +def _available_sort_key(bounty: dict[str, Any]) -> tuple[Decimal, int]: + return ( + Decimal(str(bounty.get("effective_available_mrwk", bounty["available_mrwk"]))), + int(bounty["id"]), + ) + + +def _awards_sort_key(bounty: dict[str, Any]) -> tuple[int, int]: + return ( + int(bounty.get("effective_awards_remaining", bounty["awards_remaining"])), + int(bounty["id"]), + ) + + +BOUNTY_SORT_KEYS: dict[str, BountySortKey] = { + "newest": _newest_sort_key, + "reward": _reward_sort_key, + "available": _available_sort_key, + "awards": _awards_sort_key, +} + + def sort_bounties(bounties: list[dict[str, Any]], sort: str | None) -> list[dict[str, Any]]: normalized_sort = normalize_bounty_sort(sort) - if normalized_sort == "newest": - return sorted(bounties, key=lambda bounty: int(bounty["id"]), reverse=True) - if normalized_sort == "reward": - return sorted( - bounties, - key=lambda bounty: (Decimal(str(bounty["reward_mrwk"])), int(bounty["id"])), - reverse=True, - ) - if normalized_sort == "available": - return sorted( - bounties, - key=lambda bounty: ( - Decimal(str(bounty.get("effective_available_mrwk", bounty["available_mrwk"]))), - int(bounty["id"]), - ), - reverse=True, - ) - return sorted( - bounties, - key=lambda bounty: ( - int(bounty.get("effective_awards_remaining", bounty["awards_remaining"])), - int(bounty["id"]), - ), - reverse=True, - ) + return sorted(bounties, key=BOUNTY_SORT_KEYS[normalized_sort], reverse=True) diff --git a/tests/test_bounty_sorting.py b/tests/test_bounty_sorting.py new file mode 100644 index 00000000..afb2f737 --- /dev/null +++ b/tests/test_bounty_sorting.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +import pytest + +from app.bounty_sorting import BOUNTY_SORT_ERROR, normalize_bounty_sort, sort_bounties + + +def _bounty_row( + bounty_id: int, + *, + reward_mrwk: str, + available_mrwk: str, + awards_remaining: int, + effective_available_mrwk: str | None = None, + effective_awards_remaining: int | None = None, +) -> dict[str, object]: + return { + "id": bounty_id, + "reward_mrwk": reward_mrwk, + "available_mrwk": available_mrwk, + "effective_available_mrwk": effective_available_mrwk or available_mrwk, + "awards_remaining": awards_remaining, + "effective_awards_remaining": ( + effective_awards_remaining + if effective_awards_remaining is not None + else awards_remaining + ), + } + + +def _ids(rows: list[dict[str, object]]) -> list[int]: + return [int(row["id"]) for row in rows] + + +@pytest.mark.parametrize( + ("raw_sort", "expected"), + [ + (None, "newest"), + ("", "newest"), + (" reward ", "reward"), + ("AVAILABLE", "available"), + ("awards", "awards"), + ], +) +def test_normalize_bounty_sort(raw_sort: str | None, expected: str) -> None: + assert normalize_bounty_sort(raw_sort) == expected + + +def test_normalize_bounty_sort_rejects_invalid_values() -> None: + with pytest.raises(ValueError, match=BOUNTY_SORT_ERROR): + normalize_bounty_sort("oldest") + + +def test_normalize_bounty_sort_rejects_control_characters() -> None: + with pytest.raises(ValueError, match="sort must not contain control characters"): + normalize_bounty_sort("\x85reward") + + +def test_sort_bounties_preserves_supported_orders() -> None: + rows = [ + _bounty_row( + 1, + reward_mrwk="10", + available_mrwk="100", + awards_remaining=10, + effective_available_mrwk="20", + effective_awards_remaining=2, + ), + _bounty_row( + 2, + reward_mrwk="25", + available_mrwk="25", + awards_remaining=1, + effective_available_mrwk="25", + effective_awards_remaining=1, + ), + _bounty_row( + 3, + reward_mrwk="25", + available_mrwk="75", + awards_remaining=3, + effective_available_mrwk="75", + effective_awards_remaining=3, + ), + ] + + assert _ids(sort_bounties(rows, None)) == [3, 2, 1] + assert _ids(sort_bounties(rows, "reward")) == [3, 2, 1] + assert _ids(sort_bounties(rows, "available")) == [3, 2, 1] + assert _ids(sort_bounties(rows, "awards")) == [3, 1, 2] From cdff05b4b8f17e8173334af9b498fbb7d3809173 Mon Sep 17 00:00:00 2001 From: MauriceMohr Date: Thu, 4 Jun 2026 18:20:43 +0200 Subject: [PATCH 2/2] Document bounty sorting helpers --- app/bounty_sorting.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/bounty_sorting.py b/app/bounty_sorting.py index 3957b92e..f60690e2 100644 --- a/app/bounty_sorting.py +++ b/app/bounty_sorting.py @@ -20,6 +20,7 @@ def normalize_bounty_sort(sort: str | None) -> str: + """Return a supported bounty sort mode, defaulting blank values to newest.""" raw_sort = sort or "" if contains_control_character(raw_sort): raise ValueError("sort must not contain control characters") @@ -32,14 +33,17 @@ def normalize_bounty_sort(sort: str | None) -> str: def _newest_sort_key(bounty: dict[str, Any]) -> int: + """Sort newest bounties first by descending internal id.""" return int(bounty["id"]) def _reward_sort_key(bounty: dict[str, Any]) -> tuple[Decimal, int]: + """Sort by per-award reward, then newest bounty id.""" return Decimal(str(bounty["reward_mrwk"])), int(bounty["id"]) def _available_sort_key(bounty: dict[str, Any]) -> tuple[Decimal, int]: + """Sort by effective remaining MRWK pool, then newest bounty id.""" return ( Decimal(str(bounty.get("effective_available_mrwk", bounty["available_mrwk"]))), int(bounty["id"]), @@ -47,6 +51,7 @@ def _available_sort_key(bounty: dict[str, Any]) -> tuple[Decimal, int]: def _awards_sort_key(bounty: dict[str, Any]) -> tuple[int, int]: + """Sort by effective remaining award slots, then newest bounty id.""" return ( int(bounty.get("effective_awards_remaining", bounty["awards_remaining"])), int(bounty["id"]), @@ -62,5 +67,6 @@ def _awards_sort_key(bounty: dict[str, Any]) -> tuple[int, int]: def sort_bounties(bounties: list[dict[str, Any]], sort: str | None) -> list[dict[str, Any]]: + """Return bounties ordered by the normalized public sort mode.""" normalized_sort = normalize_bounty_sort(sort) return sorted(bounties, key=BOUNTY_SORT_KEYS[normalized_sort], reverse=True)