From 50c868fe118c8f0b9ad2844f57705239b915383c Mon Sep 17 00:00:00 2001 From: Brook Date: Mon, 28 Sep 2026 10:16:13 -0700 Subject: [PATCH 1/4] Remove dataset and episode offset pagination Remove the offset argument and legacy pagination handling from the five dataset and episode list APIs in DC-1789. Keep limit as the cursor page size and update tests and documentation for cursor-only continuation.\n\nSet the package version to 0.20.1. --- foxglove/client/api.py | 27 ++++++--------------------- foxglove/client/pagination.py | 10 +--------- pyproject.toml | 2 +- tests/test_datasets.py | 2 +- tests/test_episodes.py | 4 ++-- tests/test_pagination.py | 17 +++-------------- uv.lock | 2 +- 7 files changed, 15 insertions(+), 49 deletions(-) diff --git a/foxglove/client/api.py b/foxglove/client/api.py index ca72466..1c54d5e 100644 --- a/foxglove/client/api.py +++ b/foxglove/client/api.py @@ -1210,13 +1210,12 @@ def get_datasets( sort_by: Optional[str] = None, sort_order: Optional[str] = None, limit: Optional[int] = None, - offset: Optional[int] = None, cursor: Optional[str] = None, ): """Return a Page of datasets; name is a case-insensitive substring filter. - ``limit`` is the page size. Use ``cursor`` for continuation; ``offset`` is - deprecated. Use ``page.auto_paging_iter()`` to traverse all matching items. + ``limit`` is the page size. Use ``cursor`` for continuation or + ``page.auto_paging_iter()`` to traverse all matching items. """ return self._get_page( "/v1/datasets", @@ -1228,7 +1227,6 @@ def get_datasets( "sortBy": camelize(sort_by), "sortOrder": sort_order, "limit": limit, - "offset": offset, "cursor": cursor, } ), @@ -1242,8 +1240,6 @@ def _get_page( params: Dict[str, Any], collection: Optional[str] = None, ) -> Page[T]: - if params.get("cursor") is not None and params.get("offset", 0) != 0: - raise ValueError("cursor cannot be combined with a nonzero offset") response = self.__session.get(self.__url__(path), params=params) result = json_or_raise(response) items = result[collection] if collection else result @@ -1258,7 +1254,6 @@ def fetch_page(cursor: str) -> Page[T]: next_cursor=response.headers.get("fg-pagination-next-cursor"), previous_cursor=response.headers.get("fg-pagination-previous-cursor"), fetch_page=fetch_page, - legacy_offset=params.get("offset", 0) != 0, ) def get_dataset(self, *, dataset_id: str): @@ -1296,7 +1291,6 @@ def get_dataset_episodes( sort_by: Optional[str] = None, sort_order: Optional[str] = None, limit: Optional[int] = None, - offset: Optional[int] = None, cursor: Optional[str] = None, start: Optional[datetime.datetime] = None, end: Optional[datetime.datetime] = None, @@ -1307,7 +1301,7 @@ def get_dataset_episodes( """Return a Page of the latest committed or initial editable membership. Supply ``start`` and ``end`` together to filter overlapping episode windows. - ``limit`` is page size; ``cursor`` continues a page and ``offset`` is deprecated. + ``limit`` is page size and ``cursor`` continues a page. Use ``page.auto_paging_iter()`` to traverse all matching episodes. """ return self._get_dataset_episodes( @@ -1316,7 +1310,6 @@ def get_dataset_episodes( sort_by=sort_by, sort_order=sort_order, limit=limit, - offset=offset, cursor=cursor, start=start, end=end, @@ -1351,11 +1344,10 @@ def get_dataset_versions( sort_order: Optional[str] = None, limit: Optional[int] = None, cursor: Optional[str] = None, - offset: Optional[int] = None, ): """Return a Page of committed versions and the current editable version. - ``limit`` is page size; ``cursor`` continues a page and ``offset`` is deprecated. + ``limit`` is page size and ``cursor`` continues a page. Use ``page.auto_paging_iter()`` to traverse all versions. """ return self._get_page( @@ -1367,7 +1359,6 @@ def get_dataset_versions( "sortOrder": sort_order, "limit": limit, "cursor": cursor, - "offset": offset, } ), ) @@ -1387,7 +1378,6 @@ def get_dataset_version_episodes( sort_by: Optional[str] = None, sort_order: Optional[str] = None, limit: Optional[int] = None, - offset: Optional[int] = None, cursor: Optional[str] = None, start: Optional[datetime.datetime] = None, end: Optional[datetime.datetime] = None, @@ -1398,7 +1388,7 @@ def get_dataset_version_episodes( """Return a Page of episode membership in a specific version. Supply ``start`` and ``end`` together to filter overlapping episode windows. - ``limit`` is page size; ``cursor`` continues a page and ``offset`` is deprecated. + ``limit`` is page size and ``cursor`` continues a page. Use ``page.auto_paging_iter()`` to traverse all matching episodes. """ return self._get_dataset_episodes( @@ -1407,7 +1397,6 @@ def get_dataset_version_episodes( sort_by=sort_by, sort_order=sort_order, limit=limit, - offset=offset, cursor=cursor, start=start, end=end, @@ -1424,7 +1413,6 @@ def _get_dataset_episodes( sort_by: Optional[str], sort_order: Optional[str], limit: Optional[int], - offset: Optional[int], cursor: Optional[str], start: Optional[datetime.datetime], end: Optional[datetime.datetime], @@ -1446,7 +1434,6 @@ def _get_dataset_episodes( "sortBy": camelize(sort_by), "sortOrder": sort_order, "limit": limit, - "offset": offset, "cursor": cursor, "start": start.astimezone().isoformat() if start else None, "end": end.astimezone().isoformat() if end else None, @@ -1576,14 +1563,13 @@ def get_episodes( sort_by: Optional[str] = None, sort_order: Optional[str] = None, limit: Optional[int] = None, - offset: Optional[int] = None, cursor: Optional[str] = None, include_recordings: bool = False, ): """Return a Page of episodes, optionally with recording details. Supply ``start`` and ``end`` together to filter overlapping episode windows. - ``limit`` is page size; ``cursor`` continues a page and ``offset`` is deprecated. + ``limit`` is page size and ``cursor`` continues a page. Use ``page.auto_paging_iter()`` to traverse all matching episodes. """ _validate_episode_range(start, end) @@ -1605,7 +1591,6 @@ def get_episodes( "sortBy": camelize(sort_by), "sortOrder": sort_order, "limit": limit, - "offset": offset, "cursor": cursor, "include": "recordings" if include_recordings else None, } diff --git a/foxglove/client/pagination.py b/foxglove/client/pagination.py index 0c87d7e..a700858 100644 --- a/foxglove/client/pagination.py +++ b/foxglove/client/pagination.py @@ -18,25 +18,17 @@ def __init__( next_cursor: Optional[str] = None, previous_cursor: Optional[str] = None, fetch_page: Callable[[str], "Page[T]"], - legacy_offset: bool = False, ): self.items = items self.next_cursor = next_cursor self.previous_cursor = previous_cursor self._fetch_page = fetch_page - self._legacy_offset = legacy_offset def auto_paging_iter(self) -> Iterator[T]: """Yield this page, then fetch further pages on demand in display order. - Starting from a nonzero deprecated offset is unsupported: those responses - do not contain continuation cursors. Request the first page or a cursor - page instead. Errors fetching subsequent pages propagate to the caller. + Errors fetching subsequent pages propagate to the caller. """ - if self._legacy_offset: - raise ValueError( - "Automatic pagination requires cursor pagination, not a nonzero offset" - ) page = self while True: yield from page.items diff --git a/pyproject.toml b/pyproject.toml index 7efef40..c089e78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta" [project] name = "foxglove-client" -version = "0.20.0" +version = "0.20.1" description = "Client library for the Foxglove API." readme = "README.md" requires-python = ">=3.7" diff --git a/tests/test_datasets.py b/tests/test_datasets.py index 289a1fe..26c2539 100644 --- a/tests/test_datasets.py +++ b/tests/test_datasets.py @@ -96,7 +96,7 @@ def test_dataset_metadata_methods(): episode_ids=["ep_1"], ) datasets = client.get_datasets( - project_id="prj_1", sort_by="updated_at", limit=10, offset=2 + project_id="prj_1", sort_by="updated_at", limit=10, cursor="next" ) fetched = client.get_dataset(dataset_id="ds_1") updated = client.update_dataset(dataset_id="ds_1", description=None) diff --git a/tests/test_episodes.py b/tests/test_episodes.py index 36ff513..bb0abff 100644 --- a/tests/test_episodes.py +++ b/tests/test_episodes.py @@ -87,7 +87,7 @@ def test_get_episodes_maps_response_and_filters(): sort_by="start_time", sort_order="asc", limit=10, - offset=20, + cursor="next", include_recordings=True, ) @@ -105,7 +105,7 @@ def test_get_episodes_maps_response_and_filters(): "sortBy": "startTime", "sortOrder": "asc", "limit": "10", - "offset": "20", + "cursor": "next", "include": "recordings", } diff --git a/tests/test_pagination.py b/tests/test_pagination.py index ef36d50..67d35c0 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -117,21 +117,10 @@ def test_pages_are_lazy_and_preserve_query( @pytest.mark.parametrize("method,kwargs,path,collection,factory,filters", LIST_CASES) -@responses.activate -def test_offset_rules(method, kwargs, path, collection, factory, filters): +def test_offset_is_not_accepted(method, kwargs, path, collection, factory, filters): get_page = getattr(Client("test"), method) - with pytest.raises(ValueError, match="nonzero offset"): - get_page(**kwargs, cursor="cursor", offset=1) - assert not responses.calls - responses.add( - responses.GET, api_url(path), json={collection: []} if collection else [] - ) - page = get_page(**kwargs, offset=1) - with pytest.raises(ValueError, match="nonzero offset"): - list(page.auto_paging_iter()) - page = get_page(**kwargs, offset=0, cursor="cursor", limit=0) - assert list(page.auto_paging_iter()) == [] - assert page.next_cursor is None + with pytest.raises(TypeError, match="unexpected keyword argument 'offset'"): + get_page(**kwargs, offset=1) @responses.activate diff --git a/uv.lock b/uv.lock index 3b20949..f9b9805 100644 --- a/uv.lock +++ b/uv.lock @@ -877,7 +877,7 @@ wheels = [ [[package]] name = "foxglove-client" -version = "0.20.0" +version = "0.20.1" source = { editable = "." } dependencies = [ { name = "arrow", version = "1.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, From 4233f578fe4632b83d571351b8ed7a88ee88856e Mon Sep 17 00:00:00 2001 From: Brook Date: Mon, 28 Sep 2026 10:35:37 -0700 Subject: [PATCH 2/4] Bump package version to 0.21.0 --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c089e78..b70fdd6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta" [project] name = "foxglove-client" -version = "0.20.1" +version = "0.21.0" description = "Client library for the Foxglove API." readme = "README.md" requires-python = ">=3.7" diff --git a/uv.lock b/uv.lock index f9b9805..c85aad1 100644 --- a/uv.lock +++ b/uv.lock @@ -877,7 +877,7 @@ wheels = [ [[package]] name = "foxglove-client" -version = "0.20.1" +version = "0.21.0" source = { editable = "." } dependencies = [ { name = "arrow", version = "1.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, From 6184fcb1ce916a1f6c662a44370a1931444b066c Mon Sep 17 00:00:00 2001 From: Brook Date: Mon, 28 Sep 2026 10:41:44 -0700 Subject: [PATCH 3/4] Clarify cursor pagination docstrings --- foxglove/client/api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/foxglove/client/api.py b/foxglove/client/api.py index 1c54d5e..c911306 100644 --- a/foxglove/client/api.py +++ b/foxglove/client/api.py @@ -1301,7 +1301,7 @@ def get_dataset_episodes( """Return a Page of the latest committed or initial editable membership. Supply ``start`` and ``end`` together to filter overlapping episode windows. - ``limit`` is page size and ``cursor`` continues a page. + Use ``cursor`` to advance pages and ``limit`` to set the size of the page. Use ``page.auto_paging_iter()`` to traverse all matching episodes. """ return self._get_dataset_episodes( @@ -1347,7 +1347,7 @@ def get_dataset_versions( ): """Return a Page of committed versions and the current editable version. - ``limit`` is page size and ``cursor`` continues a page. + Use ``cursor`` to advance pages and ``limit`` to set the size of the page. Use ``page.auto_paging_iter()`` to traverse all versions. """ return self._get_page( @@ -1388,7 +1388,7 @@ def get_dataset_version_episodes( """Return a Page of episode membership in a specific version. Supply ``start`` and ``end`` together to filter overlapping episode windows. - ``limit`` is page size and ``cursor`` continues a page. + Use ``cursor`` to advance pages and ``limit`` to set the size of the page. Use ``page.auto_paging_iter()`` to traverse all matching episodes. """ return self._get_dataset_episodes( @@ -1569,7 +1569,7 @@ def get_episodes( """Return a Page of episodes, optionally with recording details. Supply ``start`` and ``end`` together to filter overlapping episode windows. - ``limit`` is page size and ``cursor`` continues a page. + Use ``cursor`` to advance pages and ``limit`` to set the size of the page. Use ``page.auto_paging_iter()`` to traverse all matching episodes. """ _validate_episode_range(start, end) From 614eed626cb6c47b518abc53e17dbc8fcdafbda5 Mon Sep 17 00:00:00 2001 From: Brook Date: Mon, 28 Sep 2026 10:44:12 -0700 Subject: [PATCH 4/4] Align dataset pagination documentation --- foxglove/client/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/foxglove/client/api.py b/foxglove/client/api.py index c911306..7373bc4 100644 --- a/foxglove/client/api.py +++ b/foxglove/client/api.py @@ -1214,8 +1214,8 @@ def get_datasets( ): """Return a Page of datasets; name is a case-insensitive substring filter. - ``limit`` is the page size. Use ``cursor`` for continuation or - ``page.auto_paging_iter()`` to traverse all matching items. + Use ``cursor`` to advance pages and ``limit`` to set the size of the page. + Use ``page.auto_paging_iter()`` to traverse all matching items. """ return self._get_page( "/v1/datasets",