python-zendesk-sdk 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,296 @@
1
+ """Pagination utilities for Zendesk API."""
2
+
3
+ import logging
4
+ from abc import ABC, abstractmethod
5
+ from typing import Any, AsyncIterator, Dict, Generic, List, Optional, TypeVar
6
+
7
+ from .exceptions import ZendeskPaginationException
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ T = TypeVar("T")
12
+
13
+
14
+ class PaginationInfo:
15
+ """Information about pagination state."""
16
+
17
+ def __init__(
18
+ self,
19
+ page: Optional[int] = None,
20
+ per_page: Optional[int] = None,
21
+ count: Optional[int] = None,
22
+ next_page: Optional[str] = None,
23
+ previous_page: Optional[str] = None,
24
+ has_more: Optional[bool] = None,
25
+ ) -> None:
26
+ self.page = page
27
+ self.per_page = per_page
28
+ self.count = count
29
+ self.next_page = next_page
30
+ self.previous_page = previous_page
31
+ self.has_more = has_more
32
+
33
+ @classmethod
34
+ def from_response(cls, response: Dict[str, Any]) -> "PaginationInfo":
35
+ """Create pagination info from API response."""
36
+ return cls(
37
+ page=response.get("page"),
38
+ per_page=response.get("per_page"),
39
+ count=response.get("count"),
40
+ next_page=response.get("next_page"),
41
+ previous_page=response.get("previous_page"),
42
+ has_more=response.get("has_more"),
43
+ )
44
+
45
+ def __repr__(self) -> str:
46
+ return (
47
+ f"PaginationInfo(page={self.page}, per_page={self.per_page}, count={self.count}, has_more={self.has_more})"
48
+ )
49
+
50
+
51
+ class Paginator(ABC, Generic[T]):
52
+ """Abstract base class for paginators."""
53
+
54
+ def __init__(
55
+ self, http_client: Any, path: str, params: Optional[Dict[str, Any]] = None, per_page: int = 100
56
+ ) -> None:
57
+ self.http_client = http_client
58
+ self.path = path
59
+ self.params = params or {}
60
+ self.per_page = per_page
61
+ self._current_page = 1
62
+ self._pagination_info: Optional[PaginationInfo] = None
63
+
64
+ @abstractmethod
65
+ async def _fetch_page(self, page_params: Dict[str, Any]) -> Dict[str, Any]:
66
+ """Fetch a single page of data."""
67
+ pass
68
+
69
+ @abstractmethod
70
+ def _extract_items(self, response: Dict[str, Any]) -> List[T]:
71
+ """Extract items from API response."""
72
+ pass
73
+
74
+ @abstractmethod
75
+ def _update_pagination_state(self, response: Dict[str, Any]) -> bool:
76
+ """Update pagination state and return True if more pages available."""
77
+ pass
78
+
79
+ async def get_page(self, page: Optional[int] = None) -> List[T]:
80
+ """Get a specific page of items."""
81
+ if page is not None:
82
+ self._current_page = page
83
+
84
+ page_params = self._build_page_params()
85
+ response = await self._fetch_page(page_params)
86
+ self._update_pagination_state(response)
87
+
88
+ return self._extract_items(response)
89
+
90
+ def _build_page_params(self) -> Dict[str, Any]:
91
+ """Build parameters for current page request."""
92
+ params = self.params.copy()
93
+ params.update(self._get_page_params())
94
+ return params
95
+
96
+ @abstractmethod
97
+ def _get_page_params(self) -> Dict[str, Any]:
98
+ """Get page-specific parameters."""
99
+ pass
100
+
101
+ @property
102
+ def pagination_info(self) -> Optional[PaginationInfo]:
103
+ """Get current pagination information."""
104
+ return self._pagination_info
105
+
106
+ async def __aiter__(self) -> AsyncIterator[T]:
107
+ """Async iterator over all items across all pages."""
108
+ self._current_page = 1
109
+
110
+ while True:
111
+ try:
112
+ items = await self.get_page()
113
+ for item in items:
114
+ yield item
115
+
116
+ # Check if there are more pages
117
+ if not self._has_more_pages():
118
+ break
119
+
120
+ self._advance_to_next_page()
121
+
122
+ except Exception as e:
123
+ raise ZendeskPaginationException(
124
+ f"Error during pagination: {str(e)}", {"page": self._current_page, "per_page": self.per_page}
125
+ )
126
+
127
+ @abstractmethod
128
+ def _has_more_pages(self) -> bool:
129
+ """Check if there are more pages available."""
130
+ pass
131
+
132
+ @abstractmethod
133
+ def _advance_to_next_page(self) -> None:
134
+ """Advance to next page."""
135
+ pass
136
+
137
+
138
+ class OffsetPaginator(Paginator[T]):
139
+ """Offset-based paginator using page and per_page parameters."""
140
+
141
+ async def _fetch_page(self, page_params: Dict[str, Any]) -> Dict[str, Any]:
142
+ """Fetch page using HTTP client."""
143
+ return await self.http_client.get(self.path, params=page_params)
144
+
145
+ def _extract_items(self, response: Dict[str, Any]) -> List[T]:
146
+ """Extract items from response. Override in subclasses."""
147
+ # This is a generic implementation - subclasses should override
148
+ # to extract specific item types (users, tickets, etc.)
149
+ return response.get("items", [])
150
+
151
+ def _update_pagination_state(self, response: Dict[str, Any]) -> bool:
152
+ """Update pagination state from response."""
153
+ self._pagination_info = PaginationInfo.from_response(response)
154
+ return self._has_more_pages()
155
+
156
+ def _get_page_params(self) -> Dict[str, Any]:
157
+ """Get offset-based page parameters."""
158
+ return {"page": self._current_page, "per_page": self.per_page}
159
+
160
+ def _has_more_pages(self) -> bool:
161
+ """Check if more pages available using count and current page."""
162
+ if not self._pagination_info:
163
+ return False
164
+
165
+ if self._pagination_info.has_more is not None:
166
+ return self._pagination_info.has_more
167
+
168
+ # Calculate based on count if available
169
+ if self._pagination_info.count is not None:
170
+ total_pages = (self._pagination_info.count + self.per_page - 1) // self.per_page
171
+ return self._current_page < total_pages
172
+
173
+ # Fallback: assume more pages if we got a full page
174
+ return True
175
+
176
+ def _advance_to_next_page(self) -> None:
177
+ """Move to next page."""
178
+ self._current_page += 1
179
+
180
+
181
+ class CursorPaginator(Paginator[T]):
182
+ """Cursor-based paginator for large datasets."""
183
+
184
+ def __init__(
185
+ self, http_client: Any, path: str, params: Optional[Dict[str, Any]] = None, per_page: int = 100
186
+ ) -> None:
187
+ super().__init__(http_client, path, params, per_page)
188
+ self._next_cursor: Optional[str] = None
189
+ self._has_started = False
190
+
191
+ async def _fetch_page(self, page_params: Dict[str, Any]) -> Dict[str, Any]:
192
+ """Fetch page using HTTP client."""
193
+ return await self.http_client.get(self.path, params=page_params)
194
+
195
+ def _extract_items(self, response: Dict[str, Any]) -> List[T]:
196
+ """Extract items from response. Override in subclasses."""
197
+ return response.get("items", [])
198
+
199
+ def _update_pagination_state(self, response: Dict[str, Any]) -> bool:
200
+ """Update cursor-based pagination state."""
201
+ self._pagination_info = PaginationInfo.from_response(response)
202
+
203
+ # Update cursor for next page
204
+ self._next_cursor = response.get("next_cursor") or response.get("after_cursor")
205
+
206
+ # Some APIs use different field names
207
+ if not self._next_cursor:
208
+ links = response.get("links", {})
209
+ if "next" in links:
210
+ # Extract cursor from next URL if needed
211
+ self._next_cursor = str(links["next"])
212
+
213
+ self._has_started = True
214
+ return self._has_more_pages()
215
+
216
+ def _get_page_params(self) -> Dict[str, Any]:
217
+ """Get cursor-based page parameters."""
218
+ params = {"per_page": self.per_page}
219
+
220
+ if self._next_cursor and self._has_started:
221
+ params["cursor"] = self._next_cursor # type: ignore[assignment]
222
+
223
+ return params
224
+
225
+ def _has_more_pages(self) -> bool:
226
+ """Check if more pages available using cursor."""
227
+ if not self._has_started:
228
+ return True
229
+
230
+ if self._pagination_info and self._pagination_info.has_more is not None:
231
+ return self._pagination_info.has_more
232
+
233
+ # If we have a next cursor, there are more pages
234
+ return self._next_cursor is not None
235
+
236
+ def _advance_to_next_page(self) -> None:
237
+ """Cursor advancement is handled in _update_pagination_state."""
238
+ pass
239
+
240
+
241
+ class ZendeskPaginator:
242
+ """Factory for creating Zendesk-specific paginators."""
243
+
244
+ @staticmethod
245
+ def create_users_paginator(http_client: Any, per_page: int = 100) -> OffsetPaginator[Dict[str, Any]]:
246
+ """Create paginator for users endpoint."""
247
+
248
+ class UsersPaginator(OffsetPaginator[Dict[str, Any]]):
249
+ def _extract_items(self, response: Dict[str, Any]) -> List[Dict[str, Any]]:
250
+ return response.get("users", [])
251
+
252
+ return UsersPaginator(http_client, "users.json", per_page=per_page)
253
+
254
+ @staticmethod
255
+ def create_tickets_paginator(http_client: Any, per_page: int = 100) -> OffsetPaginator[Dict[str, Any]]:
256
+ """Create paginator for tickets endpoint."""
257
+
258
+ class TicketsPaginator(OffsetPaginator[Dict[str, Any]]):
259
+ def _extract_items(self, response: Dict[str, Any]) -> List[Dict[str, Any]]:
260
+ return response.get("tickets", [])
261
+
262
+ return TicketsPaginator(http_client, "tickets.json", per_page=per_page)
263
+
264
+ @staticmethod
265
+ def create_organizations_paginator(http_client: Any, per_page: int = 100) -> OffsetPaginator[Dict[str, Any]]:
266
+ """Create paginator for organizations endpoint."""
267
+
268
+ class OrganizationsPaginator(OffsetPaginator[Dict[str, Any]]):
269
+ def _extract_items(self, response: Dict[str, Any]) -> List[Dict[str, Any]]:
270
+ return response.get("organizations", [])
271
+
272
+ return OrganizationsPaginator(http_client, "organizations.json", per_page=per_page)
273
+
274
+ @staticmethod
275
+ def create_search_paginator(http_client: Any, query: str, per_page: int = 100) -> OffsetPaginator[Dict[str, Any]]:
276
+ """Create paginator for search endpoint."""
277
+
278
+ class SearchPaginator(OffsetPaginator[Dict[str, Any]]):
279
+ def _extract_items(self, response: Dict[str, Any]) -> List[Dict[str, Any]]:
280
+ return response.get("results", [])
281
+
282
+ return SearchPaginator(http_client, "search.json", params={"query": query}, per_page=per_page)
283
+
284
+ @staticmethod
285
+ def create_incremental_paginator(
286
+ http_client: Any, resource_type: str, start_time: int
287
+ ) -> CursorPaginator[Dict[str, Any]]:
288
+ """Create cursor-based paginator for incremental exports."""
289
+
290
+ class IncrementalPaginator(CursorPaginator[Dict[str, Any]]):
291
+ def _extract_items(self, response: Dict[str, Any]) -> List[Dict[str, Any]]:
292
+ return response.get(resource_type, [])
293
+
294
+ path = f"incremental/{resource_type}.json"
295
+ params = {"start_time": start_time}
296
+ return IncrementalPaginator(http_client, path, params=params)