Source code for recurring_ical_events.pages

"""Pagination for recurring ical events.

See

- https://github.com/niccokunzmann/python-recurring-ical-events/issues/211
- https://github.com/niccokunzmann/python-recurring-ical-events/issues/217
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Generic, Iterator, Optional, TypeVar

from recurring_ical_events.util import compare_greater

if TYPE_CHECKING:
    from icalendar import Component

    from recurring_ical_events.occurrence import Occurrence
    from recurring_ical_events.types import Time

T = TypeVar("T")


class _PageBase(Generic[T]):
    """Shared behaviour for :class:`Page` and :class:`OccurrencePage`."""

    def __init__(self, items: list[T], next_page_id: str = ""):
        self._items = items
        self._next_page_id = next_page_id

    def has_next_page(self) -> bool:
        """Wether there is a page following this one."""
        return self._next_page_id != ""

    @property
    def next_page_id(self) -> str:
        """The id of the next page or ``''``."""
        return self._next_page_id

    def is_last(self) -> bool:
        """Wether this is the last page and there is no other page following."""
        return self._next_page_id == ""

    def __len__(self) -> int:
        """The number of items on this page."""
        return len(self._items)

    def __iter__(self) -> Iterator[T]:
        """Return an iterator over the items."""
        return iter(self._items)


[docs] class Page(_PageBase["Component"]): """One page in a series of pages. Examples: Check if the page has components. .. code-block:: python if page: print(f"We have {len(page)} components.") Go though the components: .. code-block:: python for component in page: print(component) """ def __init__(self, components: list[Component], next_page_id: str = ""): """ "Create a new page.""" super().__init__(components, next_page_id) @property def components(self) -> list[Component]: """All the components of this page.""" return self._items
[docs] class OccurrencePage(_PageBase["Occurrence"]): """One page of occurrences in a series of pages. Examples: Check if the page has occurrences. .. code-block:: python if page: print(f"We have {len(page)} occurrences.") Go though the occurrences: .. code-block:: python for occurrence in page: print(occurrence) """ def __init__(self, occurrences: list[Occurrence], next_page_id: str = ""): """Create a new occurrence page.""" super().__init__(occurrences, next_page_id) @property def occurrences(self) -> list[Occurrence]: """All the occurrences of this page.""" return self._items
class _PagesBase(Generic[T]): """Shared iteration state for :class:`Pages` and :class:`OccurrencePages`.""" def __init__( self, occurrence_iterator: Iterator[Occurrence], size: int, stop: Optional[Time] = None, ): self._iterator = occurrence_iterator self._stop = stop self._size = size if self._size <= 0: raise ValueError(f"A page must have at least one item, not {self._size}.") self._next_occurrence: Optional[Occurrence] = None for occurrence in self._iterator: if self._stop is None or compare_greater(self._stop, occurrence.start): self._next_occurrence = occurrence break @property def size(self) -> int: """The maximum number of items per page.""" return self._size def generate_next_page(self) -> T: """Generate the next page. In contrast to ``next(pages)``, this does not raise :class:`StopIteration`. But it works the same: the next page is generated and returned. The last page is empty. """ for page in self: return page return self._empty_page() def _empty_page(self) -> T: """The empty page returned past the end of the iterator.""" raise NotImplementedError def _collect_next_page(self) -> list[Occurrence]: """Pull the next page-worth of occurrences from the source iterator.""" if self._next_occurrence is None: raise StopIteration last_occurrence = self._next_occurrence occurrences = [last_occurrence] for occurrence in self._iterator: if self._stop is not None and compare_greater(occurrence.start, self._stop): break last_occurrence = occurrence if len(occurrences) < self._size: occurrences.append(occurrence) else: break if occurrences[-1] == last_occurrence: self._next_occurrence = None else: self._next_occurrence = last_occurrence return occurrences def _next_page_id_string(self) -> str: """The id of the page following the one we just emitted, or ``''``.""" return ( self._next_occurrence.id.to_string() if self._next_occurrence is not None else "" )
[docs] class Pages(_PagesBase["Page"]): """A pagination configuration to iterate over pages. This is an :class:`Iterator` that returns :class:`Page` objects. """ def __init__( self, occurrence_iterator: Iterator[Occurrence], size: int, stop: Optional[Time] = None, keep_recurrence_attributes: bool = False, # noqa: FBT001 ): """Create a new paginated iterator over components.""" super().__init__(occurrence_iterator, size, stop) self._keep_recurrence_attributes = keep_recurrence_attributes def _empty_page(self) -> Page: return Page([]) def __next__(self) -> Page: """Return the next page.""" occurrences = self._collect_next_page() return Page( [ occurrence.as_component(self._keep_recurrence_attributes) for occurrence in occurrences ], next_page_id=self._next_page_id_string(), ) def __iter__(self) -> Pages: """Return the iterator.""" return self
[docs] class OccurrencePages(_PagesBase["OccurrencePage"]): """A pagination configuration to iterate over pages of occurrences. This is an :class:`Iterator` that returns :class:`OccurrencePage` objects. """ def _empty_page(self) -> OccurrencePage: return OccurrencePage([]) def __next__(self) -> OccurrencePage: """Return the next page.""" occurrences = self._collect_next_page() return OccurrencePage(occurrences, next_page_id=self._next_page_id_string()) def __iter__(self) -> OccurrencePages: """Return the iterator.""" return self
__all__ = ["OccurrencePage", "OccurrencePages", "Page", "Pages"]