Skip to content

Commit 9da55a9

Browse files
clauderazor-x
authored andcommitted
fix: Read pagination from the response instead of a client hook
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y1RzepycXEYA3LStfjt8cY
1 parent e0f0805 commit 9da55a9

23 files changed

Lines changed: 417 additions & 210 deletions

codegen/layouts/partials/route-method.hbs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@
3535
return None
3636
{{else if (isListType returnType)}}
3737

38-
return [{{fromDict (listItemType returnType)}}(item) for item in unwrap_list(res, "{{returnPath.[0]}}", "{{path}}")]
38+
return {{#if hasPagination}}PaginatedList(
39+
[{{fromDict (listItemType returnType)}}(item) for item in unwrap_list(res, "{{returnPath.[0]}}", "{{path}}")],
40+
pagination=res.get("pagination"),
41+
){{else}}[{{fromDict (listItemType returnType)}}(item) for item in unwrap_list(res, "{{returnPath.[0]}}", "{{path}}")]{{/if}}
3942
{{else}}
4043

4144
return {{fromDict returnType}}(unwrap(res, "{{returnPath.[0]}}", "{{path}}"))

codegen/layouts/route.hbs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ from ..response import unwrap
2020
{{#if importUnwrapList}}
2121
from ..response import unwrap_list
2222
{{/if}}
23+
{{#if importPaginatedList}}
24+
from ..pagination import PaginatedList
25+
{{/if}}
2326

2427

2528
{{> abstract-route-class abstractClass}}

codegen/lib/layouts/route.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export interface RouteLayoutContext {
6464
importNull: boolean
6565
importUnwrap: boolean
6666
importUnwrapList: boolean
67+
importPaginatedList: boolean
6768
methods: MethodLayoutContext[]
6869
}
6970

@@ -154,6 +155,8 @@ export const setRouteLayoutContext = (cls: ClassModel): RouteLayoutContext => {
154155
returnPath.length > 0 && returnType.startsWith('List['),
155156
)
156157

158+
const importPaginatedList = methods.some(({ hasPagination }) => hasPagination)
159+
157160
const showPass =
158161
cls.methods.length === 0 && cls.childClassIdentifiers.length === 0
159162

@@ -198,6 +201,7 @@ export const setRouteLayoutContext = (cls: ClassModel): RouteLayoutContext => {
198201
importNull,
199202
importUnwrap,
200203
importUnwrapList,
204+
importPaginatedList,
201205
methods,
202206
}
203207
}

seam/pagination.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from typing import Any, Dict, List, Optional
2+
3+
14
class Pagination:
25
def __init__(
36
self,
@@ -8,3 +11,16 @@ def __init__(
811
self.has_next_page = has_next_page
912
self.next_page_cursor = next_page_cursor
1013
self.next_page_url = next_page_url
14+
15+
16+
class PaginatedList(List[Any]):
17+
"""A list of results that carries the response's pagination envelope.
18+
19+
Behaves exactly like the plain list it replaces; the paginator reads
20+
the ``pagination`` attribute instead of intercepting the response
21+
through a client-wide event hook.
22+
"""
23+
24+
def __init__(self, items: List[Any], pagination: Optional[Dict[str, Any]] = None):
25+
super().__init__(items)
26+
self.pagination = pagination

seam/paginator.py

Lines changed: 21 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@
88
Optional,
99
Tuple,
1010
)
11-
from json import JSONDecodeError
12-
from httpx import Response
1311
from .client import AsyncSeamHttpClient, SeamHttpClient
12+
from .exceptions import SeamHttpInvalidResponseError
1413
from .pagination import Pagination
1514

1615

@@ -22,15 +21,29 @@ def parse_pagination(pagination: Dict[str, Any]) -> Pagination:
2221
)
2322

2423

24+
def read_pagination(data: Any, request: Callable) -> Pagination:
25+
"""Read the pagination envelope a paginated route attaches to its result."""
26+
27+
pagination = getattr(data, "pagination", None)
28+
29+
if not isinstance(pagination, dict):
30+
path = getattr(request, "__seam_path__", "this endpoint")
31+
raise SeamHttpInvalidResponseError(
32+
path,
33+
"pagination",
34+
f"got {type(pagination).__name__} instead of a pagination object",
35+
)
36+
37+
return parse_pagination(pagination)
38+
39+
2540
class SeamPaginator:
2641
"""
2742
Handles pagination for API list endpoints.
2843
2944
Iterates through pages of results returned by a callable function.
3045
"""
3146

32-
_FIRST_PAGE = "FIRST_PAGE"
33-
3447
def __init__(
3548
self,
3649
client: SeamHttpClient,
@@ -48,19 +61,12 @@ def __init__(
4861
self._request = request
4962
self.client = client
5063
self._params = params or {}
51-
self._pagination_cache: Dict[str, Pagination] = {}
5264

5365
def first_page(self) -> Tuple[List[Any], Pagination | None]:
5466
"""Fetches the first page of results."""
55-
self.client.event_hooks["response"].append(
56-
lambda response: self._cache_pagination(response, self._FIRST_PAGE)
57-
)
5867
data = self._request(**self._params)
59-
self.client.event_hooks["response"].pop()
6068

61-
pagination = self._pagination_cache.get(self._FIRST_PAGE)
62-
63-
return data, pagination
69+
return data, read_pagination(data, self._request)
6470

6571
def next_page(
6672
self, next_page_cursor: str, /
@@ -74,15 +80,9 @@ def next_page(
7480
"page_cursor": next_page_cursor,
7581
}
7682

77-
self.client.event_hooks["response"].append(
78-
lambda response: self._cache_pagination(response, next_page_cursor)
79-
)
8083
data = self._request(**params)
81-
self.client.event_hooks["response"].pop()
8284

83-
pagination = self._pagination_cache.get(next_page_cursor)
84-
85-
return data, pagination
85+
return data, read_pagination(data, self._request)
8686

8787
def flatten_to_list(self) -> List[Any]:
8888
"""Fetches all pages and returns all items as a single list."""
@@ -110,18 +110,6 @@ def flatten(self) -> Generator[Any, None, None]:
110110
if current_items:
111111
yield from current_items
112112

113-
def _cache_pagination(self, response: Response, page_key: str) -> None:
114-
"""Extracts pagination dict from response, creates Pagination object, and caches it."""
115-
try:
116-
# httpx response hooks fire before the response body is read.
117-
response.read()
118-
pagination = response.json().get("pagination", {})
119-
except JSONDecodeError:
120-
pagination = {}
121-
122-
if isinstance(pagination, dict):
123-
self._pagination_cache[page_key] = parse_pagination(pagination)
124-
125113

126114
class AsyncSeamPaginator:
127115
"""
@@ -130,8 +118,6 @@ class AsyncSeamPaginator:
130118
Iterates through pages of results returned by an awaitable function.
131119
"""
132120

133-
_FIRST_PAGE = "FIRST_PAGE"
134-
135121
def __init__(
136122
self,
137123
client: AsyncSeamHttpClient,
@@ -149,21 +135,12 @@ def __init__(
149135
self._request = request
150136
self.client = client
151137
self._params = params or {}
152-
self._pagination_cache: Dict[str, Pagination] = {}
153138

154139
async def first_page(self) -> Tuple[List[Any], Pagination | None]:
155140
"""Fetches the first page of results."""
156-
157-
async def cache_pagination(response: Response) -> None:
158-
await self._cache_pagination(response, self._FIRST_PAGE)
159-
160-
self.client.event_hooks["response"].append(cache_pagination)
161141
data = await self._request(**self._params)
162-
self.client.event_hooks["response"].pop()
163-
164-
pagination = self._pagination_cache.get(self._FIRST_PAGE)
165142

166-
return data, pagination
143+
return data, read_pagination(data, self._request)
167144

168145
async def next_page(
169146
self, next_page_cursor: str, /
@@ -177,16 +154,9 @@ async def next_page(
177154
"page_cursor": next_page_cursor,
178155
}
179156

180-
async def cache_pagination(response: Response) -> None:
181-
await self._cache_pagination(response, next_page_cursor)
182-
183-
self.client.event_hooks["response"].append(cache_pagination)
184157
data = await self._request(**params)
185-
self.client.event_hooks["response"].pop()
186158

187-
pagination = self._pagination_cache.get(next_page_cursor)
188-
189-
return data, pagination
159+
return data, read_pagination(data, self._request)
190160

191161
async def flatten_to_list(self) -> List[Any]:
192162
"""Fetches all pages and returns all items as a single list."""
@@ -217,15 +187,3 @@ async def flatten(self) -> AsyncGenerator[Any, None]:
217187
)
218188
for item in current_items or []:
219189
yield item
220-
221-
async def _cache_pagination(self, response: Response, page_key: str) -> None:
222-
"""Extracts pagination dict from response, creates Pagination object, and caches it."""
223-
try:
224-
# httpx response hooks fire before the response body is read.
225-
await response.aread()
226-
pagination = response.json().get("pagination", {})
227-
except JSONDecodeError:
228-
pagination = {}
229-
230-
if isinstance(pagination, dict):
231-
self._pagination_cache[page_key] = parse_pagination(pagination)

seam/routes/access_codes.py

Lines changed: 15 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

seam/routes/access_codes_unmanaged.py

Lines changed: 19 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

seam/routes/access_grants.py

Lines changed: 15 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)