python-zendesk-sdk 0.1.3__tar.gz → 0.2.0__tar.gz
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.3 → python_zendesk_sdk-0.2.0}/PKG-INFO +97 -123
- python_zendesk_sdk-0.2.0/README.md +300 -0
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/examples/basic_usage.py +7 -7
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/examples/enriched_tickets.py +7 -6
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/examples/error_handling.py +3 -3
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/examples/help_center.py +23 -16
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/examples/pagination_example.py +4 -4
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/pyproject.toml +1 -1
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/src/zendesk_sdk/__init__.py +28 -3
- python_zendesk_sdk-0.2.0/src/zendesk_sdk/client.py +280 -0
- python_zendesk_sdk-0.2.0/src/zendesk_sdk/clients/__init__.py +25 -0
- python_zendesk_sdk-0.2.0/src/zendesk_sdk/clients/attachments.py +117 -0
- python_zendesk_sdk-0.2.0/src/zendesk_sdk/clients/base.py +108 -0
- python_zendesk_sdk-0.2.0/src/zendesk_sdk/clients/help_center/__init__.py +83 -0
- python_zendesk_sdk-0.2.0/src/zendesk_sdk/clients/help_center/articles.py +259 -0
- python_zendesk_sdk-0.2.0/src/zendesk_sdk/clients/help_center/categories.py +155 -0
- python_zendesk_sdk-0.2.0/src/zendesk_sdk/clients/help_center/sections.py +180 -0
- python_zendesk_sdk-0.2.0/src/zendesk_sdk/clients/organizations.py +66 -0
- python_zendesk_sdk-0.2.0/src/zendesk_sdk/clients/search.py +82 -0
- python_zendesk_sdk-0.2.0/src/zendesk_sdk/clients/tickets.py +461 -0
- python_zendesk_sdk-0.2.0/src/zendesk_sdk/clients/users.py +110 -0
- python_zendesk_sdk-0.2.0/tests/test_client.py +207 -0
- python_zendesk_sdk-0.2.0/tests/test_clients.py +609 -0
- python_zendesk_sdk-0.2.0/tests/test_help_center_client.py +383 -0
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/tests/test_package_import.py +1 -1
- python_zendesk_sdk-0.1.3/README.md +0 -326
- python_zendesk_sdk-0.1.3/src/zendesk_sdk/client.py +0 -895
- python_zendesk_sdk-0.1.3/src/zendesk_sdk/help_center_client.py +0 -528
- python_zendesk_sdk-0.1.3/tests/test_client.py +0 -1015
- python_zendesk_sdk-0.1.3/tests/test_help_center_client.py +0 -571
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/.flake8 +0 -0
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/.github/workflows/publish.yml +0 -0
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/.gitignore +0 -0
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/.python-version +0 -0
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/LICENSE +0 -0
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/src/zendesk_sdk/config.py +0 -0
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/src/zendesk_sdk/exceptions.py +0 -0
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/src/zendesk_sdk/http_client.py +0 -0
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/src/zendesk_sdk/models/__init__.py +0 -0
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/src/zendesk_sdk/models/base.py +0 -0
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/src/zendesk_sdk/models/comment.py +0 -0
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/src/zendesk_sdk/models/enriched_ticket.py +0 -0
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/src/zendesk_sdk/models/help_center.py +0 -0
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/src/zendesk_sdk/models/organization.py +0 -0
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/src/zendesk_sdk/models/ticket.py +0 -0
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/src/zendesk_sdk/models/user.py +0 -0
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/src/zendesk_sdk/pagination.py +0 -0
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/tests/__init__.py +0 -0
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/tests/test_config.py +0 -0
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/tests/test_exceptions.py +0 -0
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/tests/test_http_client.py +0 -0
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/tests/test_models.py +0 -0
- {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/tests/test_pagination.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-zendesk-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Modern Python SDK for Zendesk API
|
|
5
5
|
Project-URL: Homepage, https://github.com/bormog/python-zendesk-sdk
|
|
6
6
|
Project-URL: Repository, https://github.com/bormog/python-zendesk-sdk
|
|
@@ -43,6 +43,7 @@ Modern Python SDK for Zendesk API with async support, full type safety, and comp
|
|
|
43
43
|
|
|
44
44
|
- **Async HTTP Client**: Built on httpx with retry logic, rate limiting, and exponential backoff
|
|
45
45
|
- **Type Safety**: Full Pydantic v2 models for Users, Organizations, Tickets, Comments, and Help Center
|
|
46
|
+
- **Namespace Pattern**: Clean API organization (`client.users`, `client.tickets`, `client.help_center`)
|
|
46
47
|
- **Help Center**: Full CRUD for Categories, Sections, and Articles
|
|
47
48
|
- **Pagination**: Both offset-based and cursor-based pagination support
|
|
48
49
|
- **Search**: Zendesk search API support
|
|
@@ -69,18 +70,18 @@ async def main():
|
|
|
69
70
|
|
|
70
71
|
async with ZendeskClient(config) as client:
|
|
71
72
|
# Get users with pagination
|
|
72
|
-
|
|
73
|
-
users = await
|
|
73
|
+
paginator = await client.users.list(per_page=10)
|
|
74
|
+
users = await paginator.get_page()
|
|
74
75
|
|
|
75
76
|
for user in users:
|
|
76
|
-
print(f"User: {user
|
|
77
|
+
print(f"User: {user['name']} ({user['email']})")
|
|
77
78
|
|
|
78
79
|
# Get specific ticket
|
|
79
|
-
ticket = await client.
|
|
80
|
+
ticket = await client.tickets.get(12345)
|
|
80
81
|
print(f"Ticket: {ticket.subject}")
|
|
81
82
|
|
|
82
83
|
# Search tickets
|
|
83
|
-
results = await client.
|
|
84
|
+
results = await client.search.tickets("status:open priority:high")
|
|
84
85
|
for ticket in results:
|
|
85
86
|
print(f"High priority: {ticket.subject}")
|
|
86
87
|
|
|
@@ -112,32 +113,53 @@ config = ZendeskConfig() # Will load from environment
|
|
|
112
113
|
## API Methods
|
|
113
114
|
|
|
114
115
|
### Users
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
```python
|
|
117
|
+
user = await client.users.get(user_id) # Get user by ID
|
|
118
|
+
paginator = await client.users.list() # List users with pagination
|
|
119
|
+
user = await client.users.by_email(email) # Get user by email
|
|
120
|
+
users = await client.users.search(query) # Search users
|
|
121
|
+
users = await client.users.get_many([id1, id2]) # Get multiple users
|
|
122
|
+
```
|
|
118
123
|
|
|
119
124
|
### Organizations
|
|
120
|
-
|
|
121
|
-
|
|
125
|
+
```python
|
|
126
|
+
org = await client.organizations.get(org_id) # Get organization by ID
|
|
127
|
+
paginator = await client.organizations.list() # List organizations
|
|
128
|
+
orgs = await client.organizations.search(query) # Search organizations
|
|
129
|
+
```
|
|
122
130
|
|
|
123
131
|
### Tickets
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
132
|
+
```python
|
|
133
|
+
ticket = await client.tickets.get(ticket_id) # Get ticket by ID
|
|
134
|
+
paginator = await client.tickets.list() # List tickets
|
|
135
|
+
tickets = await client.tickets.for_user(user_id) # Get user's tickets
|
|
136
|
+
tickets = await client.tickets.for_organization(org_id) # Get org's tickets
|
|
137
|
+
tickets = await client.tickets.search(query) # Search tickets
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Comments (nested under tickets)
|
|
141
|
+
```python
|
|
142
|
+
comments = await client.tickets.comments.list(ticket_id)
|
|
143
|
+
ticket = await client.tickets.comments.add(ticket_id, body, public=False)
|
|
144
|
+
await client.tickets.comments.make_private(ticket_id, comment_id)
|
|
145
|
+
comment = await client.tickets.comments.redact(ticket_id, comment_id, text)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Tags (nested under tickets)
|
|
149
|
+
```python
|
|
150
|
+
tags = await client.tickets.tags.get(ticket_id) # Get tags
|
|
151
|
+
tags = await client.tickets.tags.add(ticket_id, ["vip"]) # Add tags
|
|
152
|
+
tags = await client.tickets.tags.set(ticket_id, ["new"]) # Replace all tags
|
|
153
|
+
tags = await client.tickets.tags.remove(ticket_id, ["old"]) # Remove tags
|
|
154
|
+
```
|
|
128
155
|
|
|
129
156
|
### Enriched Tickets
|
|
130
157
|
|
|
131
158
|
Load tickets with all related data (comments + users) in minimum API requests:
|
|
132
159
|
|
|
133
|
-
- `get_enriched_ticket(ticket_id)` - Get ticket with comments and all users
|
|
134
|
-
- `search_enriched_tickets(query)` - Search tickets with all related data
|
|
135
|
-
- `get_organization_enriched_tickets(org_id)` - Get organization tickets with all data
|
|
136
|
-
- `get_user_enriched_tickets(user_id)` - Get user tickets with all data
|
|
137
|
-
|
|
138
160
|
```python
|
|
139
161
|
# Get ticket with all related data
|
|
140
|
-
enriched = await client.
|
|
162
|
+
enriched = await client.tickets.get_enriched(12345)
|
|
141
163
|
|
|
142
164
|
print(f"Ticket: {enriched.ticket.subject}")
|
|
143
165
|
print(f"Requester: {enriched.requester.name}")
|
|
@@ -148,152 +170,104 @@ for comment in enriched.comments:
|
|
|
148
170
|
print(f"Comment by {author.name}: {comment.body[:50]}...")
|
|
149
171
|
|
|
150
172
|
# Search with all data loaded
|
|
151
|
-
results = await client.
|
|
173
|
+
results = await client.tickets.search_enriched("status:open")
|
|
152
174
|
for item in results:
|
|
153
175
|
print(f"{item.ticket.subject} - {len(item.comments)} comments")
|
|
154
176
|
```
|
|
155
177
|
|
|
156
|
-
### Comments
|
|
157
|
-
- `get_ticket_comments(ticket_id)` - Get comments for a ticket
|
|
158
|
-
- `add_ticket_comment(ticket_id, body, public=False)` - Add a comment (private by default)
|
|
159
|
-
- `make_comment_private(ticket_id, comment_id)` - Convert public comment to internal note
|
|
160
|
-
- `redact_comment_string(ticket_id, comment_id, text)` - Permanently redact text from comment
|
|
161
|
-
|
|
162
|
-
### Tags
|
|
163
|
-
- `get_ticket_tags(ticket_id)` - Get all tags for a ticket
|
|
164
|
-
- `add_ticket_tags(ticket_id, tags)` - Add tags without removing existing ones
|
|
165
|
-
- `set_ticket_tags(ticket_id, tags)` - Replace all tags with a new set
|
|
166
|
-
- `remove_ticket_tags(ticket_id, tags)` - Remove specific tags
|
|
167
|
-
|
|
168
|
-
```python
|
|
169
|
-
# Get current tags
|
|
170
|
-
tags = await client.get_ticket_tags(12345)
|
|
171
|
-
# ["billing", "urgent"]
|
|
172
|
-
|
|
173
|
-
# Add new tags (keeps existing)
|
|
174
|
-
tags = await client.add_ticket_tags(12345, ["vip"])
|
|
175
|
-
# ["billing", "urgent", "vip"]
|
|
176
|
-
|
|
177
|
-
# Replace all tags
|
|
178
|
-
tags = await client.set_ticket_tags(12345, ["support", "priority"])
|
|
179
|
-
# ["support", "priority"]
|
|
180
|
-
|
|
181
|
-
# Remove specific tags
|
|
182
|
-
tags = await client.remove_ticket_tags(12345, ["priority"])
|
|
183
|
-
# ["support"]
|
|
184
|
-
```
|
|
185
|
-
|
|
186
178
|
### Attachments
|
|
187
|
-
- `download_attachment(content_url)` - Download attachment content as bytes
|
|
188
|
-
- `upload_attachment(data, filename, content_type)` - Upload file and get token
|
|
189
|
-
|
|
190
179
|
```python
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
for comment in comments:
|
|
194
|
-
for attachment in comment.attachments or []:
|
|
195
|
-
content = await client.download_attachment(attachment.content_url)
|
|
196
|
-
with open(attachment.file_name, "wb") as f:
|
|
197
|
-
f.write(content)
|
|
198
|
-
|
|
199
|
-
# Upload a file and attach to a comment
|
|
200
|
-
with open("screenshot.png", "rb") as f:
|
|
201
|
-
token = await client.upload_attachment(
|
|
202
|
-
f.read(),
|
|
203
|
-
"screenshot.png",
|
|
204
|
-
"image/png"
|
|
205
|
-
)
|
|
180
|
+
content = await client.attachments.download(content_url) # Download file
|
|
181
|
+
token = await client.attachments.upload(data, filename, content_type) # Upload file
|
|
206
182
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
body="See attached screenshot",
|
|
210
|
-
uploads=[token]
|
|
211
|
-
)
|
|
183
|
+
# Attach to comment
|
|
184
|
+
await client.tickets.comments.add(ticket_id, "See attached", uploads=[token])
|
|
212
185
|
```
|
|
213
186
|
|
|
214
187
|
### Search
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
188
|
+
```python
|
|
189
|
+
paginator = await client.search.all(query) # General search
|
|
190
|
+
tickets = await client.search.tickets(query) # Search tickets
|
|
191
|
+
users = await client.search.users(query) # Search users
|
|
192
|
+
orgs = await client.search.organizations(query) # Search organizations
|
|
193
|
+
```
|
|
219
194
|
|
|
220
195
|
### Help Center
|
|
221
196
|
|
|
222
197
|
Access Help Center (Guide) via `client.help_center` namespace:
|
|
223
198
|
|
|
224
199
|
#### Categories
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
200
|
+
```python
|
|
201
|
+
cat = await client.help_center.categories.get(category_id)
|
|
202
|
+
paginator = await client.help_center.categories.list()
|
|
203
|
+
cat = await client.help_center.categories.create(name, description)
|
|
204
|
+
cat = await client.help_center.categories.update(category_id, name=new_name)
|
|
205
|
+
await client.help_center.categories.delete(category_id, force=True)
|
|
206
|
+
```
|
|
230
207
|
|
|
231
208
|
#### Sections
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
209
|
+
```python
|
|
210
|
+
sec = await client.help_center.sections.get(section_id)
|
|
211
|
+
paginator = await client.help_center.sections.list()
|
|
212
|
+
paginator = await client.help_center.sections.for_category(category_id)
|
|
213
|
+
sec = await client.help_center.sections.create(category_id, name, description)
|
|
214
|
+
sec = await client.help_center.sections.update(section_id, name=new_name)
|
|
215
|
+
await client.help_center.sections.delete(section_id, force=True)
|
|
216
|
+
```
|
|
238
217
|
|
|
239
218
|
#### Articles
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
219
|
+
```python
|
|
220
|
+
art = await client.help_center.articles.get(article_id)
|
|
221
|
+
paginator = await client.help_center.articles.list()
|
|
222
|
+
paginator = await client.help_center.articles.for_section(section_id)
|
|
223
|
+
paginator = await client.help_center.articles.for_category(category_id)
|
|
224
|
+
results = await client.help_center.articles.search(query)
|
|
225
|
+
art = await client.help_center.articles.create(section_id, title, body=html)
|
|
226
|
+
art = await client.help_center.articles.update(article_id, title=new_title)
|
|
227
|
+
await client.help_center.articles.delete(article_id)
|
|
228
|
+
```
|
|
248
229
|
|
|
230
|
+
#### Example
|
|
249
231
|
```python
|
|
250
232
|
async with ZendeskClient(config) as client:
|
|
251
233
|
hc = client.help_center
|
|
252
234
|
|
|
235
|
+
# Get permission_group_id from existing article (required for article creation)
|
|
236
|
+
existing = await (await hc.articles.list(per_page=1)).get_page()
|
|
237
|
+
article_details = await hc.articles.get(existing[0]["id"])
|
|
238
|
+
permission_group_id = article_details.permission_group_id
|
|
239
|
+
|
|
253
240
|
# Create category -> section -> article hierarchy
|
|
254
|
-
category = await hc.
|
|
241
|
+
category = await hc.categories.create(
|
|
255
242
|
name="Product Documentation",
|
|
256
243
|
description="Help articles for our product"
|
|
257
244
|
)
|
|
258
245
|
|
|
259
|
-
section = await hc.
|
|
260
|
-
|
|
261
|
-
|
|
246
|
+
section = await hc.sections.create(
|
|
247
|
+
category.id,
|
|
248
|
+
"Getting Started"
|
|
262
249
|
)
|
|
263
250
|
|
|
264
|
-
article = await hc.
|
|
265
|
-
|
|
251
|
+
article = await hc.articles.create(
|
|
252
|
+
section.id,
|
|
266
253
|
title="Installation Guide",
|
|
267
254
|
body="<h1>Installation</h1><p>Follow these steps...</p>",
|
|
268
|
-
permission_group_id=
|
|
269
|
-
draft=
|
|
255
|
+
permission_group_id=permission_group_id,
|
|
256
|
+
draft=True,
|
|
270
257
|
label_names=["installation", "guide"],
|
|
271
258
|
)
|
|
272
259
|
|
|
273
260
|
# Search articles (useful for AI assistants)
|
|
274
|
-
results = await hc.
|
|
261
|
+
results = await hc.articles.search("password reset")
|
|
275
262
|
for article in results:
|
|
276
263
|
print(f"{article.title}")
|
|
277
264
|
print(f"Snippet: {article.snippet}") # Matching text with <em> tags
|
|
278
265
|
|
|
279
|
-
# Paginate through all articles
|
|
280
|
-
paginator = await hc.get_articles(per_page=50)
|
|
281
|
-
async for article_data in paginator:
|
|
282
|
-
print(article_data["title"])
|
|
283
|
-
|
|
284
|
-
# Update article
|
|
285
|
-
await hc.update_article(
|
|
286
|
-
article.id,
|
|
287
|
-
title="Updated Title",
|
|
288
|
-
promoted=True,
|
|
289
|
-
label_names=["updated", "featured"],
|
|
290
|
-
)
|
|
291
|
-
|
|
292
266
|
# Cascade delete (removes category + all sections + all articles)
|
|
293
|
-
await hc.
|
|
267
|
+
await hc.categories.delete(category.id, force=True)
|
|
294
268
|
```
|
|
295
269
|
|
|
296
|
-
> **Note**: `
|
|
270
|
+
> **Note**: `delete()` for categories and sections requires `force=True` as a safety measure since they cascade delete all child content.
|
|
297
271
|
|
|
298
272
|
## Error Handling
|
|
299
273
|
|
|
@@ -310,7 +284,7 @@ from zendesk_sdk.exceptions import (
|
|
|
310
284
|
|
|
311
285
|
async with ZendeskClient(config) as client:
|
|
312
286
|
try:
|
|
313
|
-
user = await client.
|
|
287
|
+
user = await client.users.get(12345)
|
|
314
288
|
except ZendeskAuthException as e:
|
|
315
289
|
# 401/403 - Authentication failed
|
|
316
290
|
print(f"Auth error: {e.message}")
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# Python Zendesk SDK
|
|
2
|
+
|
|
3
|
+
Modern Python SDK for Zendesk API with async support, full type safety, and comprehensive error handling.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Async HTTP Client**: Built on httpx with retry logic, rate limiting, and exponential backoff
|
|
8
|
+
- **Type Safety**: Full Pydantic v2 models for Users, Organizations, Tickets, Comments, and Help Center
|
|
9
|
+
- **Namespace Pattern**: Clean API organization (`client.users`, `client.tickets`, `client.help_center`)
|
|
10
|
+
- **Help Center**: Full CRUD for Categories, Sections, and Articles
|
|
11
|
+
- **Pagination**: Both offset-based and cursor-based pagination support
|
|
12
|
+
- **Search**: Zendesk search API support
|
|
13
|
+
- **Configuration**: Flexible configuration with environment variable support
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install python-zendesk-sdk
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import asyncio
|
|
25
|
+
from zendesk_sdk import ZendeskClient, ZendeskConfig
|
|
26
|
+
|
|
27
|
+
async def main():
|
|
28
|
+
config = ZendeskConfig(
|
|
29
|
+
subdomain="your-subdomain",
|
|
30
|
+
email="your-email@example.com",
|
|
31
|
+
token="your-api-token",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
async with ZendeskClient(config) as client:
|
|
35
|
+
# Get users with pagination
|
|
36
|
+
paginator = await client.users.list(per_page=10)
|
|
37
|
+
users = await paginator.get_page()
|
|
38
|
+
|
|
39
|
+
for user in users:
|
|
40
|
+
print(f"User: {user['name']} ({user['email']})")
|
|
41
|
+
|
|
42
|
+
# Get specific ticket
|
|
43
|
+
ticket = await client.tickets.get(12345)
|
|
44
|
+
print(f"Ticket: {ticket.subject}")
|
|
45
|
+
|
|
46
|
+
# Search tickets
|
|
47
|
+
results = await client.search.tickets("status:open priority:high")
|
|
48
|
+
for ticket in results:
|
|
49
|
+
print(f"High priority: {ticket.subject}")
|
|
50
|
+
|
|
51
|
+
asyncio.run(main())
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Configuration
|
|
55
|
+
|
|
56
|
+
### Direct instantiation
|
|
57
|
+
```python
|
|
58
|
+
config = ZendeskConfig(
|
|
59
|
+
subdomain="mycompany",
|
|
60
|
+
email="user@example.com",
|
|
61
|
+
token="api_token_here"
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Environment variables
|
|
66
|
+
```bash
|
|
67
|
+
export ZENDESK_SUBDOMAIN=mycompany
|
|
68
|
+
export ZENDESK_EMAIL=user@example.com
|
|
69
|
+
export ZENDESK_TOKEN=api_token_here
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
config = ZendeskConfig() # Will load from environment
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## API Methods
|
|
77
|
+
|
|
78
|
+
### Users
|
|
79
|
+
```python
|
|
80
|
+
user = await client.users.get(user_id) # Get user by ID
|
|
81
|
+
paginator = await client.users.list() # List users with pagination
|
|
82
|
+
user = await client.users.by_email(email) # Get user by email
|
|
83
|
+
users = await client.users.search(query) # Search users
|
|
84
|
+
users = await client.users.get_many([id1, id2]) # Get multiple users
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Organizations
|
|
88
|
+
```python
|
|
89
|
+
org = await client.organizations.get(org_id) # Get organization by ID
|
|
90
|
+
paginator = await client.organizations.list() # List organizations
|
|
91
|
+
orgs = await client.organizations.search(query) # Search organizations
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Tickets
|
|
95
|
+
```python
|
|
96
|
+
ticket = await client.tickets.get(ticket_id) # Get ticket by ID
|
|
97
|
+
paginator = await client.tickets.list() # List tickets
|
|
98
|
+
tickets = await client.tickets.for_user(user_id) # Get user's tickets
|
|
99
|
+
tickets = await client.tickets.for_organization(org_id) # Get org's tickets
|
|
100
|
+
tickets = await client.tickets.search(query) # Search tickets
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Comments (nested under tickets)
|
|
104
|
+
```python
|
|
105
|
+
comments = await client.tickets.comments.list(ticket_id)
|
|
106
|
+
ticket = await client.tickets.comments.add(ticket_id, body, public=False)
|
|
107
|
+
await client.tickets.comments.make_private(ticket_id, comment_id)
|
|
108
|
+
comment = await client.tickets.comments.redact(ticket_id, comment_id, text)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Tags (nested under tickets)
|
|
112
|
+
```python
|
|
113
|
+
tags = await client.tickets.tags.get(ticket_id) # Get tags
|
|
114
|
+
tags = await client.tickets.tags.add(ticket_id, ["vip"]) # Add tags
|
|
115
|
+
tags = await client.tickets.tags.set(ticket_id, ["new"]) # Replace all tags
|
|
116
|
+
tags = await client.tickets.tags.remove(ticket_id, ["old"]) # Remove tags
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Enriched Tickets
|
|
120
|
+
|
|
121
|
+
Load tickets with all related data (comments + users) in minimum API requests:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
# Get ticket with all related data
|
|
125
|
+
enriched = await client.tickets.get_enriched(12345)
|
|
126
|
+
|
|
127
|
+
print(f"Ticket: {enriched.ticket.subject}")
|
|
128
|
+
print(f"Requester: {enriched.requester.name}")
|
|
129
|
+
print(f"Assignee: {enriched.assignee.name if enriched.assignee else 'Unassigned'}")
|
|
130
|
+
|
|
131
|
+
for comment in enriched.comments:
|
|
132
|
+
author = enriched.get_comment_author(comment)
|
|
133
|
+
print(f"Comment by {author.name}: {comment.body[:50]}...")
|
|
134
|
+
|
|
135
|
+
# Search with all data loaded
|
|
136
|
+
results = await client.tickets.search_enriched("status:open")
|
|
137
|
+
for item in results:
|
|
138
|
+
print(f"{item.ticket.subject} - {len(item.comments)} comments")
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Attachments
|
|
142
|
+
```python
|
|
143
|
+
content = await client.attachments.download(content_url) # Download file
|
|
144
|
+
token = await client.attachments.upload(data, filename, content_type) # Upload file
|
|
145
|
+
|
|
146
|
+
# Attach to comment
|
|
147
|
+
await client.tickets.comments.add(ticket_id, "See attached", uploads=[token])
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Search
|
|
151
|
+
```python
|
|
152
|
+
paginator = await client.search.all(query) # General search
|
|
153
|
+
tickets = await client.search.tickets(query) # Search tickets
|
|
154
|
+
users = await client.search.users(query) # Search users
|
|
155
|
+
orgs = await client.search.organizations(query) # Search organizations
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Help Center
|
|
159
|
+
|
|
160
|
+
Access Help Center (Guide) via `client.help_center` namespace:
|
|
161
|
+
|
|
162
|
+
#### Categories
|
|
163
|
+
```python
|
|
164
|
+
cat = await client.help_center.categories.get(category_id)
|
|
165
|
+
paginator = await client.help_center.categories.list()
|
|
166
|
+
cat = await client.help_center.categories.create(name, description)
|
|
167
|
+
cat = await client.help_center.categories.update(category_id, name=new_name)
|
|
168
|
+
await client.help_center.categories.delete(category_id, force=True)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
#### Sections
|
|
172
|
+
```python
|
|
173
|
+
sec = await client.help_center.sections.get(section_id)
|
|
174
|
+
paginator = await client.help_center.sections.list()
|
|
175
|
+
paginator = await client.help_center.sections.for_category(category_id)
|
|
176
|
+
sec = await client.help_center.sections.create(category_id, name, description)
|
|
177
|
+
sec = await client.help_center.sections.update(section_id, name=new_name)
|
|
178
|
+
await client.help_center.sections.delete(section_id, force=True)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
#### Articles
|
|
182
|
+
```python
|
|
183
|
+
art = await client.help_center.articles.get(article_id)
|
|
184
|
+
paginator = await client.help_center.articles.list()
|
|
185
|
+
paginator = await client.help_center.articles.for_section(section_id)
|
|
186
|
+
paginator = await client.help_center.articles.for_category(category_id)
|
|
187
|
+
results = await client.help_center.articles.search(query)
|
|
188
|
+
art = await client.help_center.articles.create(section_id, title, body=html)
|
|
189
|
+
art = await client.help_center.articles.update(article_id, title=new_title)
|
|
190
|
+
await client.help_center.articles.delete(article_id)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
#### Example
|
|
194
|
+
```python
|
|
195
|
+
async with ZendeskClient(config) as client:
|
|
196
|
+
hc = client.help_center
|
|
197
|
+
|
|
198
|
+
# Get permission_group_id from existing article (required for article creation)
|
|
199
|
+
existing = await (await hc.articles.list(per_page=1)).get_page()
|
|
200
|
+
article_details = await hc.articles.get(existing[0]["id"])
|
|
201
|
+
permission_group_id = article_details.permission_group_id
|
|
202
|
+
|
|
203
|
+
# Create category -> section -> article hierarchy
|
|
204
|
+
category = await hc.categories.create(
|
|
205
|
+
name="Product Documentation",
|
|
206
|
+
description="Help articles for our product"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
section = await hc.sections.create(
|
|
210
|
+
category.id,
|
|
211
|
+
"Getting Started"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
article = await hc.articles.create(
|
|
215
|
+
section.id,
|
|
216
|
+
title="Installation Guide",
|
|
217
|
+
body="<h1>Installation</h1><p>Follow these steps...</p>",
|
|
218
|
+
permission_group_id=permission_group_id,
|
|
219
|
+
draft=True,
|
|
220
|
+
label_names=["installation", "guide"],
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Search articles (useful for AI assistants)
|
|
224
|
+
results = await hc.articles.search("password reset")
|
|
225
|
+
for article in results:
|
|
226
|
+
print(f"{article.title}")
|
|
227
|
+
print(f"Snippet: {article.snippet}") # Matching text with <em> tags
|
|
228
|
+
|
|
229
|
+
# Cascade delete (removes category + all sections + all articles)
|
|
230
|
+
await hc.categories.delete(category.id, force=True)
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
> **Note**: `delete()` for categories and sections requires `force=True` as a safety measure since they cascade delete all child content.
|
|
234
|
+
|
|
235
|
+
## Error Handling
|
|
236
|
+
|
|
237
|
+
The SDK provides specific exception classes for different error types:
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
from zendesk_sdk.exceptions import (
|
|
241
|
+
ZendeskAuthException,
|
|
242
|
+
ZendeskHTTPException,
|
|
243
|
+
ZendeskRateLimitException,
|
|
244
|
+
ZendeskTimeoutException,
|
|
245
|
+
ZendeskValidationException,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
async with ZendeskClient(config) as client:
|
|
249
|
+
try:
|
|
250
|
+
user = await client.users.get(12345)
|
|
251
|
+
except ZendeskAuthException as e:
|
|
252
|
+
# 401/403 - Authentication failed
|
|
253
|
+
print(f"Auth error: {e.message}")
|
|
254
|
+
except ZendeskRateLimitException as e:
|
|
255
|
+
# 429 - Rate limit exceeded
|
|
256
|
+
print(f"Rate limited, retry after: {e.retry_after}s")
|
|
257
|
+
except ZendeskHTTPException as e:
|
|
258
|
+
# Other HTTP errors (404, 500, etc.)
|
|
259
|
+
print(f"HTTP {e.status_code}: {e.message}")
|
|
260
|
+
except ZendeskTimeoutException as e:
|
|
261
|
+
# Request timeout
|
|
262
|
+
print(f"Timeout: {e.message}")
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Automatic Retry
|
|
266
|
+
|
|
267
|
+
The SDK automatically retries on:
|
|
268
|
+
- Rate limiting (429) - with respect to `Retry-After` header
|
|
269
|
+
- Server errors (5xx) - with exponential backoff
|
|
270
|
+
- Network errors and timeouts
|
|
271
|
+
|
|
272
|
+
Configure retry behavior:
|
|
273
|
+
```python
|
|
274
|
+
config = ZendeskConfig(
|
|
275
|
+
subdomain="mycompany",
|
|
276
|
+
email="user@example.com",
|
|
277
|
+
token="api_token",
|
|
278
|
+
timeout=30.0, # Request timeout in seconds
|
|
279
|
+
max_retries=3, # Number of retry attempts
|
|
280
|
+
)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Examples
|
|
284
|
+
|
|
285
|
+
See the `examples/` directory for complete usage examples:
|
|
286
|
+
- `basic_usage.py` - Basic configuration and API operations
|
|
287
|
+
- `pagination_example.py` - Working with paginated results
|
|
288
|
+
- `error_handling.py` - Error handling patterns
|
|
289
|
+
- `enriched_tickets.py` - Loading tickets with related data
|
|
290
|
+
- `help_center.py` - Help Center categories, sections, and articles
|
|
291
|
+
|
|
292
|
+
## Requirements
|
|
293
|
+
|
|
294
|
+
- Python 3.8+
|
|
295
|
+
- httpx
|
|
296
|
+
- pydantic >=2.0
|
|
297
|
+
|
|
298
|
+
## License
|
|
299
|
+
|
|
300
|
+
MIT License
|
|
@@ -29,32 +29,32 @@ async def main() -> None:
|
|
|
29
29
|
# Use async context manager for proper resource cleanup
|
|
30
30
|
async with ZendeskClient(config) as client:
|
|
31
31
|
# Get a single user
|
|
32
|
-
user = await client.
|
|
32
|
+
user = await client.users.get(12345)
|
|
33
33
|
print(f"User: {user.name} ({user.email})")
|
|
34
34
|
|
|
35
35
|
# Find user by email
|
|
36
|
-
user_by_email = await client.
|
|
36
|
+
user_by_email = await client.users.by_email("user@example.com")
|
|
37
37
|
if user_by_email:
|
|
38
38
|
print(f"Found user: {user_by_email.name}")
|
|
39
39
|
|
|
40
40
|
# Get a single ticket
|
|
41
|
-
ticket = await client.
|
|
41
|
+
ticket = await client.tickets.get(12345)
|
|
42
42
|
print(f"Ticket: {ticket.subject} (status: {ticket.status})")
|
|
43
43
|
|
|
44
44
|
# Get ticket comments
|
|
45
|
-
comments = await client.
|
|
45
|
+
comments = await client.tickets.comments.list(12345)
|
|
46
46
|
print(f"Ticket has {len(comments)} comments")
|
|
47
47
|
|
|
48
48
|
# Get organization
|
|
49
|
-
org = await client.
|
|
49
|
+
org = await client.organizations.get(123)
|
|
50
50
|
print(f"Organization: {org.name}")
|
|
51
51
|
|
|
52
52
|
# Search for tickets
|
|
53
|
-
open_tickets = await client.
|
|
53
|
+
open_tickets = await client.search.tickets("status:open")
|
|
54
54
|
print(f"Found {len(open_tickets)} open tickets")
|
|
55
55
|
|
|
56
56
|
# Search for users
|
|
57
|
-
users = await client.
|
|
57
|
+
users = await client.search.users("role:admin")
|
|
58
58
|
print(f"Found {len(users)} admin users")
|
|
59
59
|
|
|
60
60
|
|