chain.py 2.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
  1. """The chain a vote record carries: which verification run wrote it, and what it left pending."""
  2. from __future__ import annotations
  3. from typing import TypedDict
  4. from .strictjson import JsonMap, is_int, is_list, is_map
  5. class Chain(TypedDict):
  6. """A scan's position after a run: the run, the next free report id, the ranks pending."""
  7. shard: int
  8. next_id: int
  9. pending: list[list[int]]
  10. retry: list[int]
  11. def _positive(raw: JsonMap, key: str) -> int:
  12. """A field holding an integer from 1."""
  13. value = raw.get(key)
  14. if not is_int(value) or value < 1:
  15. msg = f"field {key!r} is not a positive integer"
  16. raise ValueError(msg)
  17. return value
  18. def _ranks(raw: object, key: str) -> list[int]:
  19. """A field holding a list of ranks (integers from 1)."""
  20. if is_list(raw):
  21. ranks = [n for n in raw if is_int(n) and n >= 1]
  22. if len(ranks) == len(raw):
  23. return ranks
  24. msg = f"field {key!r} is not a list of ranks"
  25. raise ValueError(msg)
  26. def _span(raw: object) -> list[int]:
  27. """One `[from, to]` entry of `pending`."""
  28. span = _ranks(raw, "pending")
  29. if len(span) != 2 or span[0] > span[1]:
  30. msg = "field 'pending' is not a list of [from, to] rank ranges"
  31. raise ValueError(msg)
  32. return span
  33. def chain_of(raw: object) -> Chain:
  34. """The chain `raw` spells; ValueError naming the field when it is not one."""
  35. if not is_map(raw):
  36. msg = "is not an object"
  37. raise ValueError(msg)
  38. pending = raw.get("pending")
  39. if not is_list(pending):
  40. msg = "field 'pending' is not a list of [from, to] rank ranges"
  41. raise ValueError(msg)
  42. return {
  43. "shard": _positive(raw, "shard"),
  44. "next_id": _positive(raw, "next_id"),
  45. "pending": [_span(entry) for entry in pending],
  46. "retry": _ranks(raw.get("retry"), "retry"),
  47. }
  48. def pending_ranks(chain: Chain) -> list[int]:
  49. """Every rank the chain leaves to the next run, ascending."""
  50. spanned = {n for start, end in chain["pending"] for n in range(start, end + 1)}
  51. return sorted(spanned | set(chain["retry"]))