diff --git a/.github/workflows/build-test-macos-util.yml b/.github/workflows/build-test-macos-util.yml index 537b277e35d2..5c87f47fee8a 100644 --- a/.github/workflows/build-test-macos-util.yml +++ b/.github/workflows/build-test-macos-util.yml @@ -79,7 +79,7 @@ jobs: - name: Test util code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 4 -m "not benchmark" tests/util/test_chunks.py tests/util/test_full_block_utils.py tests/util/test_lock_queue.py tests/util/test_network_protocol_files.py tests/util/test_struct_stream.py + venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 4 -m "not benchmark" tests/util/test_chunks.py tests/util/test_full_block_utils.py tests/util/test_lock_queue.py tests/util/test_network_protocol_files.py tests/util/test_paginator.py tests/util/test_struct_stream.py - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-util.yml b/.github/workflows/build-test-ubuntu-util.yml index 6ac03033d9c2..01dd211b874e 100644 --- a/.github/workflows/build-test-ubuntu-util.yml +++ b/.github/workflows/build-test-ubuntu-util.yml @@ -78,7 +78,7 @@ jobs: - name: Test util code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 4 -m "not benchmark" -p no:monitor tests/util/test_chunks.py tests/util/test_full_block_utils.py tests/util/test_lock_queue.py tests/util/test_network_protocol_files.py tests/util/test_struct_stream.py + venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 4 -m "not benchmark" -p no:monitor tests/util/test_chunks.py tests/util/test_full_block_utils.py tests/util/test_lock_queue.py tests/util/test_network_protocol_files.py tests/util/test_paginator.py tests/util/test_struct_stream.py - name: Process coverage data run: | diff --git a/chia/util/paginator.py b/chia/util/paginator.py new file mode 100644 index 000000000000..69bcce5377f4 --- /dev/null +++ b/chia/util/paginator.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +import dataclasses +from math import ceil +from typing import Sequence + + +class InvalidPageSizeLimit(Exception): + def __init__(self, page_size_limit: int) -> None: + super().__init__(f"Page size limit must be one or more, not: {page_size_limit}") + + +class InvalidPageSizeError(Exception): + def __init__(self, page_size: int, page_size_limit: int) -> None: + super().__init__(f"Invalid page size {page_size}. Must be between: 1 and {page_size_limit}") + + +class PageOutOfBoundsError(Exception): + def __init__(self, page_size: int, max_page_size: int) -> None: + super().__init__(f"Page {page_size} out of bounds. Available pages: 0-{max_page_size}") + + +@dataclasses.dataclass +class Paginator: + _source: Sequence[object] + _page_size: int + + @classmethod + def create(cls, source: Sequence[object], page_size: int, page_size_limit: int = 100) -> Paginator: + if page_size_limit < 1: + raise InvalidPageSizeLimit(page_size_limit) + if page_size > page_size_limit: + raise InvalidPageSizeError(page_size, page_size_limit) + return cls(source, page_size) + + def page_size(self) -> int: + return self._page_size + + def page_count(self) -> int: + return max(1, ceil(len(self._source) / self._page_size)) + + def get_page(self, page: int) -> Sequence[object]: + if page < 0 or page >= self.page_count(): + raise PageOutOfBoundsError(page, self.page_count() - 1) + offset = page * self._page_size + return self._source[offset : offset + self._page_size] diff --git a/tests/util/test_paginator.py b/tests/util/test_paginator.py new file mode 100644 index 000000000000..9ffb4814e6bf --- /dev/null +++ b/tests/util/test_paginator.py @@ -0,0 +1,70 @@ +from math import ceil +from typing import List, Type + +import pytest + +from chia.util.paginator import InvalidPageSizeError, InvalidPageSizeLimit, PageOutOfBoundsError, Paginator + + +@pytest.mark.parametrize( + "source, page_size, page_size_limit", + [([], 1, 1), ([1], 1, 2), ([1, 2], 2, 2), ([], 10, 100), ([1, 2, 10], 1000, 1000)], +) +def test_constructor_valid_inputs(source: List[int], page_size: int, page_size_limit: int) -> None: + paginator: Paginator = Paginator.create(source, page_size, page_size_limit) + assert paginator.page_size() == page_size + assert paginator.page_count() == 1 + assert paginator.get_page(0) == source + + +@pytest.mark.parametrize( + "page_size, page_size_limit, exception", + [ + (5, -1, InvalidPageSizeLimit), + (5, 0, InvalidPageSizeLimit), + (2, 1, InvalidPageSizeError), + (100, 1, InvalidPageSizeError), + (1001, 1000, InvalidPageSizeError), + ], +) +def test_constructor_invalid_inputs(page_size: int, page_size_limit: int, exception: Type[Exception]) -> None: + with pytest.raises(exception): + Paginator.create([], page_size, page_size_limit) + + +def test_page_count() -> None: + for page_size in range(1, 10): + for i in range(0, 10): + assert Paginator.create(range(0, i), page_size).page_count() == max(1, ceil(i / page_size)) + + +@pytest.mark.parametrize( + "length, page_size, page, expected_data", + [ + (17, 5, 0, [0, 1, 2, 3, 4]), + (17, 5, 1, [5, 6, 7, 8, 9]), + (17, 5, 2, [10, 11, 12, 13, 14]), + (17, 5, 3, [15, 16]), + (3, 4, 0, [0, 1, 2]), + (3, 3, 0, [0, 1, 2]), + (3, 2, 0, [0, 1]), + (3, 2, 1, [2]), + (3, 1, 0, [0]), + (3, 1, 1, [1]), + (3, 1, 2, [2]), + (2, 2, 0, [0, 1]), + (2, 1, 0, [0]), + (2, 1, 1, [1]), + (1, 2, 0, [0]), + (0, 2, 0, []), + (0, 10, 0, []), + ], +) +def test_get_page_valid(length: int, page: int, page_size: int, expected_data: List[int]) -> None: + assert Paginator.create(list(range(0, length)), page_size).get_page(page) == expected_data + + +@pytest.mark.parametrize("page", [-1000, -10, -1, 5, 10, 1000]) +def test_get_page_invalid(page: int) -> None: + with pytest.raises(PageOutOfBoundsError): + Paginator.create(range(0, 17), 5).get_page(page)