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.
- python_zendesk_sdk-0.1.0.dist-info/METADATA +218 -0
- python_zendesk_sdk-0.1.0.dist-info/RECORD +16 -0
- python_zendesk_sdk-0.1.0.dist-info/WHEEL +4 -0
- python_zendesk_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- zendesk_sdk/__init__.py +28 -0
- zendesk_sdk/client.py +321 -0
- zendesk_sdk/config.py +111 -0
- zendesk_sdk/exceptions.py +178 -0
- zendesk_sdk/http_client.py +256 -0
- zendesk_sdk/models/__init__.py +40 -0
- zendesk_sdk/models/base.py +50 -0
- zendesk_sdk/models/comment.py +62 -0
- zendesk_sdk/models/organization.py +59 -0
- zendesk_sdk/models/ticket.py +169 -0
- zendesk_sdk/models/user.py +107 -0
- zendesk_sdk/pagination.py +296 -0
|
@@ -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)
|