pagination.py 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. """Utilities for client-side handling of "relay-style" GraphQL pagination.
  2. For formal specs and definitions, see https://relay.dev/graphql/connections.htm.
  3. """
  4. from __future__ import annotations
  5. from collections.abc import Iterator
  6. from typing import Generic, Literal, Optional, TypeVar
  7. from pydantic import NonNegativeInt
  8. from .base import GQLResult
  9. NodeT = TypeVar("NodeT")
  10. """A generic type variable for a GraphQL relay node."""
  11. class PageInfo(GQLResult):
  12. """Pagination metadata returned by the server for a single page of results."""
  13. typename__: Literal["PageInfo"] = "PageInfo"
  14. end_cursor: Optional[str]
  15. """Opaque token marking the end of this page and the start of the next page."""
  16. has_next_page: bool
  17. """True if more results exist beyond this page."""
  18. class Edge(GQLResult, Generic[NodeT]):
  19. """A wrapper around a single result item in a paginated response.
  20. In relay-style pagination, individual items are wrapped in "edges" which can
  21. carry additional metadata, e.g., per-item cursors. This base implementation
  22. only exposes the `node` (the actual result item, like a GraphQL `Run` or `Project`).
  23. """
  24. node: NodeT
  25. """The actual result item."""
  26. class Connection(GQLResult, Generic[NodeT]):
  27. """A page of results from the response of a paginated GraphQL query.
  28. This follows the "Relay Connection" specification, which is a standard
  29. way to paginate large result sets in GraphQL. Instead of returning all
  30. results at once, the server returns one page at a time. Each "page" is
  31. represented by a `Connection` object that includes:
  32. - A list of `edges`, each wrapping a single result item (`node`).
  33. - A `page_info` object with metadata for fetching subsequent pages.
  34. - Optionally, a `total_count` of all results (not just this page).
  35. """
  36. edges: list[Edge[NodeT]]
  37. """The items in this page, each wrapped in an `Edge`."""
  38. page_info: PageInfo
  39. """Pagination metadata, e.g. `end_cursor`, `has_next_page`."""
  40. total_count: Optional[NonNegativeInt] = None
  41. """Total number of results across all pages, if available."""
  42. def nodes(self) -> Iterator[NodeT]:
  43. """Returns an iterator over the nodes in the connection."""
  44. return (node for edge in self.edges if (node := edge.node))
  45. @property
  46. def has_next(self) -> bool:
  47. """Returns True if there are more pages to fetch."""
  48. return self.page_info.has_next_page
  49. @property
  50. def next_cursor(self) -> Optional[str]:
  51. """The cursor value to pass as the `after` arg in the next page request."""
  52. return self.page_info.end_cursor
  53. class ConnectionWithTotal(Connection[NodeT], Generic[NodeT]):
  54. """A `Connection` where the `totalCount` field must be present.
  55. Use this INSTEAD of `Connection` when the paginated query is expected
  56. to return a finite `totalCount` field, i.e. when `totalCount` is:
  57. - explicitly requested in the GraphQL query
  58. - non-nullable in the GraphQL schema
  59. """
  60. total_count: NonNegativeInt
  61. """Total number of results across all pages (required, not optional)."""