-
Notifications
You must be signed in to change notification settings - Fork 45
feat(pagination): add cursor-based pagination + client APIs #112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 4 commits
15c8f54
77066dd
82e2787
27fa4f8
b05fa86
a1cedde
e92077f
7aba7a6
da5587e
a9fd25e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -6,16 +6,17 @@ | |||||
| import copy | ||||||
| import mimetypes | ||||||
| import warnings | ||||||
| from typing import Any, Dict, List, Optional, Union, Iterator, Callable, TYPE_CHECKING | ||||||
| from typing import Any, Dict, List, Optional, Union, Iterator, Callable, TYPE_CHECKING, Tuple | ||||||
|
|
||||||
| from . import utils | ||||||
| from . import feedreader | ||||||
| from . import exceptions as exp | ||||||
| from .resource import Resource | ||||||
| from .types import ( | ||||||
| Json, Document, Row, BulkItem, ServerInfo, DatabaseInfo, | ||||||
| Json, Document, Row, BulkItem, ServerInfo, DatabaseInfo, | ||||||
| ChangeResult, ViewResult, Credentials, AuthMethod, DocId, Rev | ||||||
| ) | ||||||
| from .pagination import view_pages, mango_pages, ViewRows, MangoDocs, PageSize | ||||||
|
|
||||||
| # Type alias for feed reader parameter | ||||||
| FeedReader = Union[Callable[[Dict[str, Any]], None], feedreader.BaseFeedReader] | ||||||
|
|
@@ -855,3 +856,79 @@ def changes_list(self, **kwargs): | |||||
|
|
||||||
| (resp, result) = self.resource("_changes").get(params=kwargs) | ||||||
| return result['last_seq'], result['results'] | ||||||
|
|
||||||
| def find(self, selector: Dict[str, Any], **kwargs: Any) -> Iterator[Document]: | ||||||
| """ | ||||||
| Execute a Mango query using the _find endpoint. | ||||||
|
|
||||||
| :param selector: Mango query selector | ||||||
| :param kwargs: Additional query parameters (limit, bookmark, etc.) | ||||||
| :returns: Iterator of documents matching the selector | ||||||
| """ | ||||||
| params = copy.copy(kwargs) | ||||||
| params['selector'] = selector | ||||||
|
|
||||||
| data = utils.force_bytes(json.dumps(params)) | ||||||
| (resp, result) = self.resource.post("_find", data=data) | ||||||
|
|
||||||
| if result is None or 'docs' not in result: | ||||||
| return iter([]) | ||||||
|
|
||||||
| for doc in result['docs']: | ||||||
| yield doc | ||||||
|
|
||||||
| def view_pages(self, design_and_view: str, page_size: PageSize, params: Optional[Dict[str, Any]] = None) -> Iterator[ViewRows]: | ||||||
| """ | ||||||
| Paginate through CouchDB view results with automatic cursor management. | ||||||
|
|
||||||
| This method provides convenient pagination for view queries without manual | ||||||
| skip parameter management. It automatically handles startkey and startkey_docid | ||||||
| for stable pagination. | ||||||
|
|
||||||
| :param design_and_view: View name (e.g., "design/view") | ||||||
| :param page_size: Number of rows per page | ||||||
| :param params: Additional query parameters | ||||||
| :returns: Iterator yielding lists of rows for each page | ||||||
|
|
||||||
| .. versionadded: 1.17 | ||||||
| """ | ||||||
| path = utils._path_from_name(design_and_view, '_view') | ||||||
|
|
||||||
| def fetch_view(params_dict: Dict[str, Any]) -> Tuple[Any, Optional[Dict[str, Any]]]: | ||||||
| data = None | ||||||
| if "keys" in params_dict: | ||||||
| data_dict = {"keys": params_dict.pop('keys')} | ||||||
| data = utils.force_bytes(json.dumps(data_dict)) | ||||||
|
|
||||||
| encoded_params = utils.encode_view_options(params_dict) | ||||||
|
|
||||||
| if data: | ||||||
| (resp, result) = self.resource(*path).post(params=encoded_params, data=data) | ||||||
| else: | ||||||
| (resp, result) = self.resource(*path).get(params=encoded_params) | ||||||
|
|
||||||
| return resp, result | ||||||
|
|
||||||
| return view_pages(fetch_view, design_and_view, page_size, params) | ||||||
|
|
||||||
| def mango_pages(self, selector: Dict[str, Any], page_size: PageSize, params: Optional[Dict[str, Any]] = None) -> Iterator[MangoDocs]: | ||||||
| """ | ||||||
| Paginate through Mango query results with automatic bookmark management. | ||||||
|
|
||||||
| This method provides convenient pagination for Mango queries without manual | ||||||
| bookmark parameter management. It automatically handles the bookmark cursor | ||||||
| for stable pagination. | ||||||
|
|
||||||
| :param selector: Mango query selector | ||||||
| :param page_size: Number of documents per page | ||||||
| :param params: Additional query parameters | ||||||
| :returns: Iterator yielding lists of documents for each page | ||||||
|
|
||||||
| .. versionadded: 1.17 | ||||||
|
||||||
| .. versionadded: 1.17 | |
| .. versionadded:: 1.17 |
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,153 @@ | ||||||||||||||||
| # -*- coding: utf-8 -*- | ||||||||||||||||
|
|
||||||||||||||||
| """ | ||||||||||||||||
| Pagination utilities for CouchDB views and Mango queries. | ||||||||||||||||
|
|
||||||||||||||||
| This module provides convenient pagination functionality for both CouchDB views | ||||||||||||||||
| and Mango queries, handling the complexity of cursor-based pagination internally. | ||||||||||||||||
| """ | ||||||||||||||||
|
|
||||||||||||||||
| from typing import Any, Dict, List, Iterator, Optional, Callable, Union, Tuple | ||||||||||||||||
| import json | ||||||||||||||||
| import copy | ||||||||||||||||
|
|
||||||||||||||||
| from . import utils | ||||||||||||||||
| from .types import Row, Document, Json, ViewRows, MangoDocs, PageSize | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| def view_pages( | ||||||||||||||||
| fetch: Callable[[Dict[str, Any]], Tuple[Any, Optional[Dict[str, Any]]]], | ||||||||||||||||
| view: str, | ||||||||||||||||
| page_size: PageSize, | ||||||||||||||||
| params: Optional[Dict[str, Any]] = None | ||||||||||||||||
| ) -> Iterator[ViewRows]: | ||||||||||||||||
| """ | ||||||||||||||||
| Paginate through CouchDB view results using startkey/startkey_docid cursor. | ||||||||||||||||
|
|
||||||||||||||||
| This function handles the complexity of CouchDB view pagination by automatically | ||||||||||||||||
| managing startkey and startkey_docid parameters for stable pagination. | ||||||||||||||||
|
|
||||||||||||||||
| :param fetch: Function that makes the actual HTTP request and returns (response, result) | ||||||||||||||||
| :param view: View name (e.g., "design/view") | ||||||||||||||||
| :param page_size: Number of rows per page | ||||||||||||||||
| :param params: Additional query parameters | ||||||||||||||||
| :returns: Iterator yielding lists of rows for each page | ||||||||||||||||
| """ | ||||||||||||||||
| if params is None: | ||||||||||||||||
| params = {} | ||||||||||||||||
|
|
||||||||||||||||
| # Create a copy to avoid modifying the original | ||||||||||||||||
| query_params = copy.deepcopy(params) | ||||||||||||||||
| query_params['limit'] = page_size + 1 # Request one extra to detect if there are more pages | ||||||||||||||||
|
|
||||||||||||||||
| # Track pagination state | ||||||||||||||||
| startkey = None | ||||||||||||||||
| startkey_docid = None | ||||||||||||||||
| skip = 0 | ||||||||||||||||
|
|
||||||||||||||||
| while True: | ||||||||||||||||
| # Build current page parameters | ||||||||||||||||
| current_params = copy.deepcopy(query_params) | ||||||||||||||||
|
|
||||||||||||||||
| if startkey is not None: | ||||||||||||||||
| current_params['startkey'] = startkey | ||||||||||||||||
| current_params['startkey_docid'] = startkey_docid | ||||||||||||||||
| current_params['skip'] = skip | ||||||||||||||||
|
|
||||||||||||||||
| # Encode view parameters properly | ||||||||||||||||
| current_params = _encode_view_params(current_params) | ||||||||||||||||
|
|
||||||||||||||||
| # Make the request | ||||||||||||||||
| response, result = fetch(current_params) | ||||||||||||||||
|
|
||||||||||||||||
| if result is None or 'rows' not in result: | ||||||||||||||||
| break | ||||||||||||||||
|
|
||||||||||||||||
| rows = result['rows'] | ||||||||||||||||
|
|
||||||||||||||||
| # If we got fewer rows than requested, this is the last page | ||||||||||||||||
| if len(rows) <= page_size: | ||||||||||||||||
| if rows: # Only yield if there are rows | ||||||||||||||||
| yield rows | ||||||||||||||||
| break | ||||||||||||||||
|
|
||||||||||||||||
| # We got more rows than page_size, so there are more pages | ||||||||||||||||
| # Yield current page (excluding the extra row) | ||||||||||||||||
| current_page = rows[:page_size] | ||||||||||||||||
| yield current_page | ||||||||||||||||
|
|
||||||||||||||||
| # Set up for next page using the last row as cursor | ||||||||||||||||
| last_row = rows[page_size - 1] | ||||||||||||||||
| startkey = last_row['key'] | ||||||||||||||||
| startkey_docid = last_row['id'] | ||||||||||||||||
|
Comment on lines
+89
to
+90
|
||||||||||||||||
| skip = 1 # Skip the row we used as cursor | ||||||||||||||||
|
||||||||||||||||
| skip = 1 # Skip the row we used as cursor | |
| skip = 1 # Skip the row used as the cursor to avoid returning it again (prevents duplicate results in cursor-based pagination) |
Outdated
Copilot
AI
Sep 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The _prepare_mango_data function is defined but never used in the code. Consider removing it or adding a comment explaining its intended future use.
| def _prepare_mango_data(selector: Dict[str, Any], params: Dict[str, Any]) -> bytes: | |
| """Prepare Mango query data for HTTP request.""" | |
| data_dict = { | |
| 'selector': selector, | |
| **params | |
| } | |
| return utils.force_bytes(json.dumps(data_dict)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The docstring format should be
.. versionadded:: 1.17with double colons, not single colon.