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.
Files changed (53) hide show
  1. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/PKG-INFO +97 -123
  2. python_zendesk_sdk-0.2.0/README.md +300 -0
  3. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/examples/basic_usage.py +7 -7
  4. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/examples/enriched_tickets.py +7 -6
  5. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/examples/error_handling.py +3 -3
  6. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/examples/help_center.py +23 -16
  7. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/examples/pagination_example.py +4 -4
  8. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/pyproject.toml +1 -1
  9. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/src/zendesk_sdk/__init__.py +28 -3
  10. python_zendesk_sdk-0.2.0/src/zendesk_sdk/client.py +280 -0
  11. python_zendesk_sdk-0.2.0/src/zendesk_sdk/clients/__init__.py +25 -0
  12. python_zendesk_sdk-0.2.0/src/zendesk_sdk/clients/attachments.py +117 -0
  13. python_zendesk_sdk-0.2.0/src/zendesk_sdk/clients/base.py +108 -0
  14. python_zendesk_sdk-0.2.0/src/zendesk_sdk/clients/help_center/__init__.py +83 -0
  15. python_zendesk_sdk-0.2.0/src/zendesk_sdk/clients/help_center/articles.py +259 -0
  16. python_zendesk_sdk-0.2.0/src/zendesk_sdk/clients/help_center/categories.py +155 -0
  17. python_zendesk_sdk-0.2.0/src/zendesk_sdk/clients/help_center/sections.py +180 -0
  18. python_zendesk_sdk-0.2.0/src/zendesk_sdk/clients/organizations.py +66 -0
  19. python_zendesk_sdk-0.2.0/src/zendesk_sdk/clients/search.py +82 -0
  20. python_zendesk_sdk-0.2.0/src/zendesk_sdk/clients/tickets.py +461 -0
  21. python_zendesk_sdk-0.2.0/src/zendesk_sdk/clients/users.py +110 -0
  22. python_zendesk_sdk-0.2.0/tests/test_client.py +207 -0
  23. python_zendesk_sdk-0.2.0/tests/test_clients.py +609 -0
  24. python_zendesk_sdk-0.2.0/tests/test_help_center_client.py +383 -0
  25. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/tests/test_package_import.py +1 -1
  26. python_zendesk_sdk-0.1.3/README.md +0 -326
  27. python_zendesk_sdk-0.1.3/src/zendesk_sdk/client.py +0 -895
  28. python_zendesk_sdk-0.1.3/src/zendesk_sdk/help_center_client.py +0 -528
  29. python_zendesk_sdk-0.1.3/tests/test_client.py +0 -1015
  30. python_zendesk_sdk-0.1.3/tests/test_help_center_client.py +0 -571
  31. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/.flake8 +0 -0
  32. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/.github/workflows/publish.yml +0 -0
  33. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/.gitignore +0 -0
  34. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/.python-version +0 -0
  35. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/LICENSE +0 -0
  36. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/src/zendesk_sdk/config.py +0 -0
  37. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/src/zendesk_sdk/exceptions.py +0 -0
  38. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/src/zendesk_sdk/http_client.py +0 -0
  39. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/src/zendesk_sdk/models/__init__.py +0 -0
  40. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/src/zendesk_sdk/models/base.py +0 -0
  41. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/src/zendesk_sdk/models/comment.py +0 -0
  42. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/src/zendesk_sdk/models/enriched_ticket.py +0 -0
  43. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/src/zendesk_sdk/models/help_center.py +0 -0
  44. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/src/zendesk_sdk/models/organization.py +0 -0
  45. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/src/zendesk_sdk/models/ticket.py +0 -0
  46. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/src/zendesk_sdk/models/user.py +0 -0
  47. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/src/zendesk_sdk/pagination.py +0 -0
  48. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/tests/__init__.py +0 -0
  49. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/tests/test_config.py +0 -0
  50. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/tests/test_exceptions.py +0 -0
  51. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/tests/test_http_client.py +0 -0
  52. {python_zendesk_sdk-0.1.3 → python_zendesk_sdk-0.2.0}/tests/test_models.py +0 -0
  53. {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.1.3
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
- users_paginator = await client.get_users(per_page=10)
73
- users = await users_paginator.get_page()
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.name} ({user.email})")
77
+ print(f"User: {user['name']} ({user['email']})")
77
78
 
78
79
  # Get specific ticket
79
- ticket = await client.get_ticket(ticket_id=12345)
80
+ ticket = await client.tickets.get(12345)
80
81
  print(f"Ticket: {ticket.subject}")
81
82
 
82
83
  # Search tickets
83
- results = await client.search_tickets("status:open priority:high")
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
- - `get_users()` - List users with pagination
116
- - `get_user(user_id)` - Get user by ID
117
- - `get_user_by_email(email)` - Get user by email
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
- - `get_organizations()` - List organizations with pagination
121
- - `get_organization(organization_id)` - Get organization by ID
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
- - `get_tickets()` - List tickets with pagination
125
- - `get_ticket(ticket_id)` - Get ticket by ID
126
- - `get_user_tickets(user_id)` - Get tickets for a user
127
- - `get_organization_tickets(organization_id)` - Get tickets for an organization
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.get_enriched_ticket(12345)
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.search_enriched_tickets("status:open")
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
- # Download an attachment from a comment
192
- comments = await client.get_ticket_comments(12345)
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
- await client.add_ticket_comment(
208
- ticket_id=12345,
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
- - `search(query)` - General search
216
- - `search_users(query)` - Search users
217
- - `search_tickets(query)` - Search tickets
218
- - `search_organizations(query)` - Search organizations
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
- - `get_categories()` - List categories with pagination
226
- - `get_category(category_id)` - Get category by ID
227
- - `create_category(name, description, position)` - Create category
228
- - `update_category(category_id, ...)` - Update category
229
- - `delete_category(category_id, force=True)` - Delete category (cascade deletes sections/articles)
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
- - `get_sections()` - List all sections with pagination
233
- - `get_category_sections(category_id)` - List sections in a category
234
- - `get_section(section_id)` - Get section by ID
235
- - `create_section(category_id, name, description, position)` - Create section
236
- - `update_section(section_id, ...)` - Update section
237
- - `delete_section(section_id, force=True)` - Delete section (cascade deletes articles)
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
- - `get_articles()` - List all articles with pagination
241
- - `get_section_articles(section_id)` - List articles in a section
242
- - `get_category_articles(category_id)` - List articles in a category
243
- - `get_article(article_id)` - Get article by ID
244
- - `create_article(section_id, title, body, ...)` - Create article
245
- - `update_article(article_id, ...)` - Update article
246
- - `delete_article(article_id)` - Delete article
247
- - `search_articles(query, ...)` - Full-text search with snippets
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.create_category(
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.create_section(
260
- category_id=category.id,
261
- name="Getting Started"
246
+ section = await hc.sections.create(
247
+ category.id,
248
+ "Getting Started"
262
249
  )
263
250
 
264
- article = await hc.create_article(
265
- section_id=section.id,
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=252606, # Required
269
- draft=False, # Publish immediately
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.search_articles("password reset")
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.delete_category(category.id, force=True)
267
+ await hc.categories.delete(category.id, force=True)
294
268
  ```
295
269
 
296
- > **Note**: `delete_category()` and `delete_section()` require `force=True` as a safety measure since they cascade delete all child content.
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.get_user(user_id=12345)
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.get_user(user_id=12345)
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.get_user_by_email("user@example.com")
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.get_ticket(ticket_id=12345)
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.get_ticket_comments(ticket_id=12345)
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.get_organization(org_id=123)
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.search_tickets("status:open")
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.search_users("role:admin")
57
+ users = await client.search.users("role:admin")
58
58
  print(f"Found {len(users)} admin users")
59
59
 
60
60