Skip to content

Commit 4fc3af6

Browse files
Fix pagination schema to match nested response structure
1 parent 1ccd494 commit 4fc3af6

File tree

6 files changed

+116
-34
lines changed

6 files changed

+116
-34
lines changed

.stats.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
configured_endpoints: 9
22
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/nanonets%2Fdocstrange-a418fe45369669cc2d14b549ee010f3a7ee52f6e47fea3489e560d33064be099.yml
33
openapi_spec_hash: 02f7f52faae1eb42188c290c32c25f10
4-
config_hash: 618498b0a12e1535b716981b83e1f760
4+
config_hash: de4c18021060af5ebd13aa207675d379

README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,67 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ
112112

113113
Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`.
114114

115+
## Pagination
116+
117+
List methods in the Docstrange API are paginated.
118+
119+
This library provides auto-paginating iterators with each list response, so you do not have to request successive pages manually:
120+
121+
```python
122+
from docstrange import Docstrange
123+
124+
client = Docstrange()
125+
126+
all_results = []
127+
# Automatically fetches more pages as needed.
128+
for result in client.extract.results.list():
129+
# Do something with result here
130+
all_results.append(result)
131+
print(all_results)
132+
```
133+
134+
Or, asynchronously:
135+
136+
```python
137+
import asyncio
138+
from docstrange import AsyncDocstrange
139+
140+
client = AsyncDocstrange()
141+
142+
143+
async def main() -> None:
144+
all_results = []
145+
# Iterate through items across all pages, issuing requests as needed.
146+
async for result in client.extract.results.list():
147+
all_results.append(result)
148+
print(all_results)
149+
150+
151+
asyncio.run(main())
152+
```
153+
154+
Alternatively, you can use the `.has_next_page()`, `.next_page_info()`, or `.get_next_page()` methods for more granular control working with pages:
155+
156+
```python
157+
first_page = await client.extract.results.list()
158+
if first_page.has_next_page():
159+
print(f"will fetch next page using these details: {first_page.next_page_info()}")
160+
next_page = await first_page.get_next_page()
161+
print(f"number of items we just fetched: {len(next_page.results)}")
162+
163+
# Remove `await` for non-async usage.
164+
```
165+
166+
Or just work directly with the returned data:
167+
168+
```python
169+
first_page = await client.extract.results.list()
170+
for result in first_page.results:
171+
print(result.record_id)
172+
173+
# Remove `await` for non-async usage.
174+
```
175+
115176
## File uploads
116177

117178
Request parameters that correspond to file uploads can be passed as `bytes`, or a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`.

api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ from docstrange.types.extract import ExtractionListResponse, PaginationInfo
3434
Methods:
3535

3636
- <code title="get /api/v1/extract/results/{record_id}">client.extract.results.<a href="./src/docstrange/resources/extract/results.py">retrieve</a>(record_id, \*\*<a href="src/docstrange/types/extract/result_retrieve_params.py">params</a>) -> <a href="./src/docstrange/types/extract_response.py">ExtractResponse</a></code>
37-
- <code title="get /api/v1/extract/results">client.extract.results.<a href="./src/docstrange/resources/extract/results.py">list</a>(\*\*<a href="src/docstrange/types/extract/result_list_params.py">params</a>) -> <a href="./src/docstrange/types/extract/extraction_list_response.py">ExtractionListResponse</a></code>
37+
- <code title="get /api/v1/extract/results">client.extract.results.<a href="./src/docstrange/resources/extract/results.py">list</a>(\*\*<a href="src/docstrange/types/extract/result_list_params.py">params</a>) -> <a href="./src/docstrange/types/extract_response.py">SyncPageNumberPagination[ExtractResponse]</a></code>
3838

3939
# Classify
4040

src/docstrange/pagination.py

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,46 +3,65 @@
33
from typing import List, Generic, TypeVar, Optional, cast
44
from typing_extensions import override
55

6+
from ._models import BaseModel
67
from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage
78

8-
__all__ = ["SyncPageNumberPagination", "AsyncPageNumberPagination"]
9+
__all__ = ["PageNumberPaginationPagination", "SyncPageNumberPagination", "AsyncPageNumberPagination"]
910

1011
_T = TypeVar("_T")
1112

1213

13-
class SyncPageNumberPagination(BaseSyncPage[_T], BasePage[_T], Generic[_T]):
14-
items: List[_T]
15-
total_count: Optional[int] = None
14+
class PageNumberPaginationPagination(BaseModel):
1615
has_next: Optional[bool] = None
1716

17+
total_count: Optional[int] = None
18+
19+
20+
class SyncPageNumberPagination(BaseSyncPage[_T], BasePage[_T], Generic[_T]):
21+
results: List[_T]
22+
pagination: Optional[PageNumberPaginationPagination] = None
23+
1824
@override
1925
def _get_page_items(self) -> List[_T]:
20-
items = self.items
21-
if not items:
26+
results = self.results
27+
if not results:
2228
return []
23-
return items
29+
return results
2430

2531
@override
2632
def next_page_info(self) -> Optional[PageInfo]:
2733
last_page = cast("int | None", self._options.params.get("page")) or 1
2834

35+
total_pages = None
36+
if self.pagination is not None:
37+
if self.pagination.total_count is not None:
38+
total_pages = self.pagination.total_count
39+
if total_pages is not None and last_page >= total_pages:
40+
return None
41+
2942
return PageInfo(params={"page": last_page + 1})
3043

3144

3245
class AsyncPageNumberPagination(BaseAsyncPage[_T], BasePage[_T], Generic[_T]):
33-
items: List[_T]
34-
total_count: Optional[int] = None
35-
has_next: Optional[bool] = None
46+
results: List[_T]
47+
pagination: Optional[PageNumberPaginationPagination] = None
3648

3749
@override
3850
def _get_page_items(self) -> List[_T]:
39-
items = self.items
40-
if not items:
51+
results = self.results
52+
if not results:
4153
return []
42-
return items
54+
return results
4355

4456
@override
4557
def next_page_info(self) -> Optional[PageInfo]:
4658
last_page = cast("int | None", self._options.params.get("page")) or 1
4759

60+
total_pages = None
61+
if self.pagination is not None:
62+
if self.pagination.total_count is not None:
63+
total_pages = self.pagination.total_count
64+
if total_pages is not None and last_page >= total_pages:
65+
return None
66+
4867
return PageInfo(params={"page": last_page + 1})

src/docstrange/resources/extract/results.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616
async_to_raw_response_wrapper,
1717
async_to_streamed_response_wrapper,
1818
)
19-
from ..._base_client import make_request_options
19+
from ...pagination import SyncPageNumberPagination, AsyncPageNumberPagination
20+
from ..._base_client import AsyncPaginator, make_request_options
2021
from ...types.extract import result_list_params, result_retrieve_params
2122
from ...types.extract_response import ExtractResponse
22-
from ...types.extract.extraction_list_response import ExtractionListResponse
2323

2424
__all__ = ["ResultsResource", "AsyncResultsResource"]
2525

@@ -100,7 +100,7 @@ def list(
100100
extra_query: Query | None = None,
101101
extra_body: Body | None = None,
102102
timeout: float | httpx.Timeout | None | NotGiven = not_given,
103-
) -> ExtractionListResponse:
103+
) -> SyncPageNumberPagination[ExtractResponse]:
104104
"""
105105
List all extraction jobs for the authenticated user (paginated).
106106
@@ -113,8 +113,9 @@ def list(
113113
114114
timeout: Override the client-level default timeout for this request, in seconds
115115
"""
116-
return self._get(
116+
return self._get_api_list(
117117
"/api/v1/extract/results",
118+
page=SyncPageNumberPagination[ExtractResponse],
118119
options=make_request_options(
119120
extra_headers=extra_headers,
120121
extra_query=extra_query,
@@ -130,7 +131,7 @@ def list(
130131
result_list_params.ResultListParams,
131132
),
132133
),
133-
cast_to=ExtractionListResponse,
134+
model=ExtractResponse,
134135
)
135136

136137

@@ -196,7 +197,7 @@ async def retrieve(
196197
cast_to=ExtractResponse,
197198
)
198199

199-
async def list(
200+
def list(
200201
self,
201202
*,
202203
page: int | Omit = omit,
@@ -210,7 +211,7 @@ async def list(
210211
extra_query: Query | None = None,
211212
extra_body: Body | None = None,
212213
timeout: float | httpx.Timeout | None | NotGiven = not_given,
213-
) -> ExtractionListResponse:
214+
) -> AsyncPaginator[ExtractResponse, AsyncPageNumberPagination[ExtractResponse]]:
214215
"""
215216
List all extraction jobs for the authenticated user (paginated).
216217
@@ -223,14 +224,15 @@ async def list(
223224
224225
timeout: Override the client-level default timeout for this request, in seconds
225226
"""
226-
return await self._get(
227+
return self._get_api_list(
227228
"/api/v1/extract/results",
229+
page=AsyncPageNumberPagination[ExtractResponse],
228230
options=make_request_options(
229231
extra_headers=extra_headers,
230232
extra_query=extra_query,
231233
extra_body=extra_body,
232234
timeout=timeout,
233-
query=await async_maybe_transform(
235+
query=maybe_transform(
234236
{
235237
"page": page,
236238
"page_size": page_size,
@@ -240,7 +242,7 @@ async def list(
240242
result_list_params.ResultListParams,
241243
),
242244
),
243-
cast_to=ExtractionListResponse,
245+
model=ExtractResponse,
244246
)
245247

246248

tests/api_resources/extract/test_results.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from docstrange import Docstrange, AsyncDocstrange
1111
from tests.utils import assert_matches_type
1212
from docstrange.types import ExtractResponse
13-
from docstrange.types.extract import ExtractionListResponse
13+
from docstrange.pagination import SyncPageNumberPagination, AsyncPageNumberPagination
1414

1515
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
1616

@@ -73,7 +73,7 @@ def test_path_params_retrieve(self, client: Docstrange) -> None:
7373
@parametrize
7474
def test_method_list(self, client: Docstrange) -> None:
7575
result = client.extract.results.list()
76-
assert_matches_type(ExtractionListResponse, result, path=["response"])
76+
assert_matches_type(SyncPageNumberPagination[ExtractResponse], result, path=["response"])
7777

7878
@pytest.mark.skip(reason="Prism tests are disabled")
7979
@parametrize
@@ -84,7 +84,7 @@ def test_method_list_with_all_params(self, client: Docstrange) -> None:
8484
sort_by="created_at",
8585
sort_order="asc",
8686
)
87-
assert_matches_type(ExtractionListResponse, result, path=["response"])
87+
assert_matches_type(SyncPageNumberPagination[ExtractResponse], result, path=["response"])
8888

8989
@pytest.mark.skip(reason="Prism tests are disabled")
9090
@parametrize
@@ -94,7 +94,7 @@ def test_raw_response_list(self, client: Docstrange) -> None:
9494
assert response.is_closed is True
9595
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
9696
result = response.parse()
97-
assert_matches_type(ExtractionListResponse, result, path=["response"])
97+
assert_matches_type(SyncPageNumberPagination[ExtractResponse], result, path=["response"])
9898

9999
@pytest.mark.skip(reason="Prism tests are disabled")
100100
@parametrize
@@ -104,7 +104,7 @@ def test_streaming_response_list(self, client: Docstrange) -> None:
104104
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
105105

106106
result = response.parse()
107-
assert_matches_type(ExtractionListResponse, result, path=["response"])
107+
assert_matches_type(SyncPageNumberPagination[ExtractResponse], result, path=["response"])
108108

109109
assert cast(Any, response.is_closed) is True
110110

@@ -169,7 +169,7 @@ async def test_path_params_retrieve(self, async_client: AsyncDocstrange) -> None
169169
@parametrize
170170
async def test_method_list(self, async_client: AsyncDocstrange) -> None:
171171
result = await async_client.extract.results.list()
172-
assert_matches_type(ExtractionListResponse, result, path=["response"])
172+
assert_matches_type(AsyncPageNumberPagination[ExtractResponse], result, path=["response"])
173173

174174
@pytest.mark.skip(reason="Prism tests are disabled")
175175
@parametrize
@@ -180,7 +180,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncDocstrange)
180180
sort_by="created_at",
181181
sort_order="asc",
182182
)
183-
assert_matches_type(ExtractionListResponse, result, path=["response"])
183+
assert_matches_type(AsyncPageNumberPagination[ExtractResponse], result, path=["response"])
184184

185185
@pytest.mark.skip(reason="Prism tests are disabled")
186186
@parametrize
@@ -190,7 +190,7 @@ async def test_raw_response_list(self, async_client: AsyncDocstrange) -> None:
190190
assert response.is_closed is True
191191
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
192192
result = await response.parse()
193-
assert_matches_type(ExtractionListResponse, result, path=["response"])
193+
assert_matches_type(AsyncPageNumberPagination[ExtractResponse], result, path=["response"])
194194

195195
@pytest.mark.skip(reason="Prism tests are disabled")
196196
@parametrize
@@ -200,6 +200,6 @@ async def test_streaming_response_list(self, async_client: AsyncDocstrange) -> N
200200
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
201201

202202
result = await response.parse()
203-
assert_matches_type(ExtractionListResponse, result, path=["response"])
203+
assert_matches_type(AsyncPageNumberPagination[ExtractResponse], result, path=["response"])
204204

205205
assert cast(Any, response.is_closed) is True

0 commit comments

Comments
 (0)