python-zendesk-sdk 0.10.0__tar.gz → 0.12.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.10.0 → python_zendesk_sdk-0.12.0}/PKG-INFO +17 -2
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/README.md +16 -1
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/examples/enriched_tickets.py +18 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/examples/groups.py +20 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/pyproject.toml +1 -1
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/__init__.py +3 -1
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/clients/groups.py +66 -1
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/clients/tickets.py +55 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/models/__init__.py +2 -0
- python_zendesk_sdk-0.12.0/src/zendesk_sdk/models/group_membership.py +31 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/pagination.py +27 -1
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/tests/test_clients.py +93 -1
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/tests/test_package_import.py +1 -1
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/.flake8 +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/.github/workflows/publish.yml +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/.gitignore +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/.python-version +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/LICENSE +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/context7.json +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/examples/basic_usage.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/examples/caching.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/examples/error_handling.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/examples/help_center.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/examples/organizations.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/examples/pagination_example.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/examples/search.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/examples/users.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/client.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/clients/__init__.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/clients/attachments.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/clients/base.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/clients/help_center/__init__.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/clients/help_center/articles.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/clients/help_center/categories.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/clients/help_center/sections.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/clients/organizations.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/clients/search.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/clients/ticket_fields.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/clients/users.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/config.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/exceptions.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/http_client.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/models/base.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/models/comment.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/models/enriched_ticket.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/models/group.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/models/help_center.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/models/organization.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/models/search.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/models/ticket.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/models/user.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/tests/__init__.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/tests/test_client.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/tests/test_config.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/tests/test_exceptions.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/tests/test_help_center_client.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/tests/test_http_client.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/tests/test_models.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/tests/test_pagination.py +0 -0
- {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/tests/test_search_query_config.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.12.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
|
|
@@ -266,6 +266,17 @@ count = await client.groups.count() # Get total number of groups
|
|
|
266
266
|
paginator = client.groups.list() # List all groups (paginator)
|
|
267
267
|
paginator = client.groups.list_assignable() # List assignable groups (paginator)
|
|
268
268
|
|
|
269
|
+
# Memberships — find which agents belong to a group
|
|
270
|
+
paginator = client.groups.list_memberships() # All memberships (paginator)
|
|
271
|
+
paginator = client.groups.list_group_members(group_id) # Members of specific group (paginator)
|
|
272
|
+
membership = await client.groups.get_membership(membership_id) # Get specific membership
|
|
273
|
+
|
|
274
|
+
# Example: get all agents in a group
|
|
275
|
+
members = await client.groups.list_group_members(group_id).collect()
|
|
276
|
+
for m in members:
|
|
277
|
+
user = await client.users.get(m.user_id)
|
|
278
|
+
print(f" {user.name} (default={m.default})")
|
|
279
|
+
|
|
269
280
|
# Create
|
|
270
281
|
group = await client.groups.create(
|
|
271
282
|
name="Support Team",
|
|
@@ -289,6 +300,7 @@ await client.groups.delete(group_id)
|
|
|
289
300
|
```python
|
|
290
301
|
# Read
|
|
291
302
|
ticket = await client.tickets.get(ticket_id) # Get ticket by ID
|
|
303
|
+
tickets = await client.tickets.get_many([id1, id2]) # Get multiple tickets (batch)
|
|
292
304
|
paginator = client.tickets.list() # List tickets (paginator)
|
|
293
305
|
paginator = client.tickets.for_user(user_id) # User's tickets (paginator)
|
|
294
306
|
paginator = client.tickets.for_organization(org_id) # Org's tickets (paginator)
|
|
@@ -358,9 +370,12 @@ Load tickets with all related data (comments, users, field definitions) in minim
|
|
|
358
370
|
```python
|
|
359
371
|
from zendesk_sdk import SearchQueryConfig
|
|
360
372
|
|
|
361
|
-
# Get ticket with all related data
|
|
373
|
+
# Get single ticket with all related data
|
|
362
374
|
enriched = await client.tickets.get_enriched(12345)
|
|
363
375
|
|
|
376
|
+
# Batch: get multiple enriched tickets (much faster than calling get_enriched in a loop)
|
|
377
|
+
enriched_list = await client.tickets.get_many_enriched([12345, 12346, 12347])
|
|
378
|
+
|
|
364
379
|
print(f"Ticket: {enriched.ticket.subject}")
|
|
365
380
|
print(f"Requester: {enriched.requester.name}")
|
|
366
381
|
print(f"Assignee: {enriched.assignee.name if enriched.assignee else 'Unassigned'}")
|
|
@@ -228,6 +228,17 @@ count = await client.groups.count() # Get total number of groups
|
|
|
228
228
|
paginator = client.groups.list() # List all groups (paginator)
|
|
229
229
|
paginator = client.groups.list_assignable() # List assignable groups (paginator)
|
|
230
230
|
|
|
231
|
+
# Memberships — find which agents belong to a group
|
|
232
|
+
paginator = client.groups.list_memberships() # All memberships (paginator)
|
|
233
|
+
paginator = client.groups.list_group_members(group_id) # Members of specific group (paginator)
|
|
234
|
+
membership = await client.groups.get_membership(membership_id) # Get specific membership
|
|
235
|
+
|
|
236
|
+
# Example: get all agents in a group
|
|
237
|
+
members = await client.groups.list_group_members(group_id).collect()
|
|
238
|
+
for m in members:
|
|
239
|
+
user = await client.users.get(m.user_id)
|
|
240
|
+
print(f" {user.name} (default={m.default})")
|
|
241
|
+
|
|
231
242
|
# Create
|
|
232
243
|
group = await client.groups.create(
|
|
233
244
|
name="Support Team",
|
|
@@ -251,6 +262,7 @@ await client.groups.delete(group_id)
|
|
|
251
262
|
```python
|
|
252
263
|
# Read
|
|
253
264
|
ticket = await client.tickets.get(ticket_id) # Get ticket by ID
|
|
265
|
+
tickets = await client.tickets.get_many([id1, id2]) # Get multiple tickets (batch)
|
|
254
266
|
paginator = client.tickets.list() # List tickets (paginator)
|
|
255
267
|
paginator = client.tickets.for_user(user_id) # User's tickets (paginator)
|
|
256
268
|
paginator = client.tickets.for_organization(org_id) # Org's tickets (paginator)
|
|
@@ -320,9 +332,12 @@ Load tickets with all related data (comments, users, field definitions) in minim
|
|
|
320
332
|
```python
|
|
321
333
|
from zendesk_sdk import SearchQueryConfig
|
|
322
334
|
|
|
323
|
-
# Get ticket with all related data
|
|
335
|
+
# Get single ticket with all related data
|
|
324
336
|
enriched = await client.tickets.get_enriched(12345)
|
|
325
337
|
|
|
338
|
+
# Batch: get multiple enriched tickets (much faster than calling get_enriched in a loop)
|
|
339
|
+
enriched_list = await client.tickets.get_many_enriched([12345, 12346, 12347])
|
|
340
|
+
|
|
326
341
|
print(f"Ticket: {enriched.ticket.subject}")
|
|
327
342
|
print(f"Requester: {enriched.requester.name}")
|
|
328
343
|
print(f"Assignee: {enriched.assignee.name if enriched.assignee else 'Unassigned'}")
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
This example demonstrates:
|
|
4
4
|
- Loading tickets with all related data (comments, users, field definitions)
|
|
5
|
+
- Batch loading multiple enriched tickets with get_many_enriched()
|
|
5
6
|
- Using EnrichedTicket for efficient data access
|
|
6
7
|
- Accessing custom field values with human-readable names
|
|
7
8
|
- Minimizing API requests with batch loading
|
|
@@ -81,6 +82,23 @@ async def main() -> None:
|
|
|
81
82
|
else:
|
|
82
83
|
print(f" - Unknown: {body_preview}")
|
|
83
84
|
|
|
85
|
+
# ==================== Batch enriched tickets ====================
|
|
86
|
+
|
|
87
|
+
# Load multiple tickets with all related data at once
|
|
88
|
+
# Much more efficient than calling get_enriched() in a loop:
|
|
89
|
+
# - 1 API call for all tickets (show_many)
|
|
90
|
+
# - 1 API call for all users (show_many)
|
|
91
|
+
# - 1 API call for field definitions
|
|
92
|
+
# - N parallel API calls for comments (one per ticket)
|
|
93
|
+
ticket_ids = [12345, 12346, 12347]
|
|
94
|
+
enriched_list = await client.tickets.get_many_enriched(ticket_ids)
|
|
95
|
+
|
|
96
|
+
print(f"\n--- Batch loaded {len(enriched_list)} enriched tickets ---")
|
|
97
|
+
for item in enriched_list:
|
|
98
|
+
print(f" #{item.ticket.id}: {item.ticket.subject}")
|
|
99
|
+
print(f" Requester: {item.requester.name if item.requester else 'N/A'}")
|
|
100
|
+
print(f" Comments: {len(item.comments)}")
|
|
101
|
+
|
|
84
102
|
# ==================== Search with enrichment ====================
|
|
85
103
|
|
|
86
104
|
# Search for tickets and load all related data
|
|
@@ -6,6 +6,7 @@ This example demonstrates:
|
|
|
6
6
|
- Updating groups
|
|
7
7
|
- Deleting groups
|
|
8
8
|
- Listing assignable groups
|
|
9
|
+
- Group memberships (list members of a group)
|
|
9
10
|
"""
|
|
10
11
|
|
|
11
12
|
import asyncio
|
|
@@ -97,6 +98,25 @@ async def main() -> None:
|
|
|
97
98
|
await client.groups.delete(detailed_group.id)
|
|
98
99
|
print(f"Deleted group: {detailed_group.id}")
|
|
99
100
|
|
|
101
|
+
# ==================== Membership Operations ====================
|
|
102
|
+
|
|
103
|
+
print("\n=== Membership Operations ===")
|
|
104
|
+
|
|
105
|
+
# List all memberships across all groups
|
|
106
|
+
print("All memberships:")
|
|
107
|
+
async for membership in client.groups.list_memberships(limit=10):
|
|
108
|
+
print(f" User {membership.user_id} -> Group {membership.group_id} (default={membership.default})")
|
|
109
|
+
|
|
110
|
+
# List members of a specific group
|
|
111
|
+
print("\nMembers of first group:")
|
|
112
|
+
first_group = groups[0] if groups else None
|
|
113
|
+
if first_group:
|
|
114
|
+
members = await client.groups.list_group_members(first_group.id).collect()
|
|
115
|
+
for m in members:
|
|
116
|
+
user = await client.users.get(m.user_id)
|
|
117
|
+
default_str = " [DEFAULT]" if m.default else ""
|
|
118
|
+
print(f" {user.name}{default_str}")
|
|
119
|
+
|
|
100
120
|
# ==================== Caching Example ====================
|
|
101
121
|
|
|
102
122
|
print("\n=== Caching Example ===")
|
|
@@ -5,7 +5,7 @@ This package provides a clean, async-first interface to the Zendesk API
|
|
|
5
5
|
with full type safety and comprehensive error handling.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
__version__ = "0.
|
|
8
|
+
__version__ = "0.12.0"
|
|
9
9
|
|
|
10
10
|
from .client import ZendeskClient
|
|
11
11
|
from .clients import (
|
|
@@ -38,6 +38,7 @@ from .models import (
|
|
|
38
38
|
Category,
|
|
39
39
|
EnrichedTicket,
|
|
40
40
|
Group,
|
|
41
|
+
GroupMembership,
|
|
41
42
|
PasswordRequirements,
|
|
42
43
|
SearchQueryConfig,
|
|
43
44
|
SearchType,
|
|
@@ -76,6 +77,7 @@ __all__ = [
|
|
|
76
77
|
"ArticlesClient",
|
|
77
78
|
# Models
|
|
78
79
|
"Group",
|
|
80
|
+
"GroupMembership",
|
|
79
81
|
"EnrichedTicket",
|
|
80
82
|
"TicketField",
|
|
81
83
|
"Category",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional
|
|
4
4
|
|
|
5
|
-
from ..models import Group
|
|
5
|
+
from ..models import Group, GroupMembership
|
|
6
6
|
from ..pagination import ZendeskPaginator
|
|
7
7
|
from .base import BaseClient
|
|
8
8
|
|
|
@@ -300,3 +300,68 @@ class GroupsClient(BaseClient):
|
|
|
300
300
|
"""
|
|
301
301
|
await self._delete(f"groups/{group_id}.json")
|
|
302
302
|
return True
|
|
303
|
+
|
|
304
|
+
# ==================== Membership Operations ====================
|
|
305
|
+
|
|
306
|
+
def list_memberships(self, per_page: int = 100, limit: Optional[int] = None) -> "Paginator[GroupMembership]":
|
|
307
|
+
"""Get paginated list of all group memberships.
|
|
308
|
+
|
|
309
|
+
Returns all group membership records across the entire account.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
per_page: Number of memberships per API request (max 100).
|
|
313
|
+
limit: Maximum total memberships to return. None for no limit.
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
Paginator[GroupMembership] for iterating through all memberships
|
|
317
|
+
|
|
318
|
+
Example:
|
|
319
|
+
async for membership in client.groups.list_memberships():
|
|
320
|
+
print(f"User {membership.user_id} in Group {membership.group_id}")
|
|
321
|
+
"""
|
|
322
|
+
return ZendeskPaginator.create_group_memberships_paginator(self._http, per_page=per_page, limit=limit)
|
|
323
|
+
|
|
324
|
+
def list_group_members(
|
|
325
|
+
self, group_id: int, per_page: int = 100, limit: Optional[int] = None
|
|
326
|
+
) -> "Paginator[GroupMembership]":
|
|
327
|
+
"""Get paginated list of memberships for a specific group.
|
|
328
|
+
|
|
329
|
+
Returns membership records for agents in the given group.
|
|
330
|
+
Use this to find out which agents belong to a group.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
group_id: The group's ID
|
|
334
|
+
per_page: Number of memberships per API request (max 100).
|
|
335
|
+
limit: Maximum total memberships to return. None for no limit.
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
Paginator[GroupMembership] for iterating through group's memberships
|
|
339
|
+
|
|
340
|
+
Example:
|
|
341
|
+
# List all agents in a group
|
|
342
|
+
async for membership in client.groups.list_group_members(12345):
|
|
343
|
+
print(f"Agent {membership.user_id}, default={membership.default}")
|
|
344
|
+
|
|
345
|
+
# Collect all members
|
|
346
|
+
members = await client.groups.list_group_members(12345).collect()
|
|
347
|
+
user_ids = [m.user_id for m in members]
|
|
348
|
+
"""
|
|
349
|
+
return ZendeskPaginator.create_group_memberships_by_group_paginator(
|
|
350
|
+
self._http, group_id, per_page=per_page, limit=limit
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
async def get_membership(self, membership_id: int) -> GroupMembership:
|
|
354
|
+
"""Get a specific group membership by ID.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
membership_id: The membership's ID
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
GroupMembership object
|
|
361
|
+
|
|
362
|
+
Example:
|
|
363
|
+
membership = await client.groups.get_membership(99999)
|
|
364
|
+
print(f"User {membership.user_id} in Group {membership.group_id}")
|
|
365
|
+
"""
|
|
366
|
+
response = await self._get(f"group_memberships/{membership_id}.json")
|
|
367
|
+
return GroupMembership(**response["group_membership"])
|
|
@@ -375,6 +375,32 @@ class TicketsClient(BaseClient):
|
|
|
375
375
|
response = await self._get(f"tickets/{ticket_id}.json")
|
|
376
376
|
return Ticket(**response["ticket"])
|
|
377
377
|
|
|
378
|
+
async def get_many(self, ticket_ids: List[int]) -> Dict[int, Ticket]:
|
|
379
|
+
"""Fetch multiple tickets by IDs.
|
|
380
|
+
|
|
381
|
+
Uses show_many endpoint for efficiency (max 100 IDs per request).
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
ticket_ids: List of ticket IDs to fetch
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
Dictionary mapping ticket_id to Ticket object
|
|
388
|
+
"""
|
|
389
|
+
if not ticket_ids:
|
|
390
|
+
return {}
|
|
391
|
+
|
|
392
|
+
unique_ids = list(set(ticket_ids))[:100]
|
|
393
|
+
ids_param = ",".join(str(tid) for tid in unique_ids)
|
|
394
|
+
|
|
395
|
+
response = await self._get(f"tickets/show_many.json?ids={ids_param}")
|
|
396
|
+
|
|
397
|
+
tickets: Dict[int, Ticket] = {}
|
|
398
|
+
for ticket_data in response.get("tickets", []):
|
|
399
|
+
ticket = Ticket(**ticket_data)
|
|
400
|
+
if ticket.id is not None:
|
|
401
|
+
tickets[ticket.id] = ticket
|
|
402
|
+
return tickets
|
|
403
|
+
|
|
378
404
|
def list(self, per_page: int = 100, limit: Optional[int] = None) -> "Paginator[Ticket]":
|
|
379
405
|
"""Get paginated list of all tickets in the account.
|
|
380
406
|
|
|
@@ -844,6 +870,35 @@ class TicketsClient(BaseClient):
|
|
|
844
870
|
ticket_users = self._extract_users_from_response(response)
|
|
845
871
|
return await self._build_enriched_ticket(ticket, ticket_users, fields)
|
|
846
872
|
|
|
873
|
+
async def get_many_enriched(self, ticket_ids: List[int]) -> List[EnrichedTicket]:
|
|
874
|
+
"""Get multiple tickets with all related data: comments, users, and field definitions.
|
|
875
|
+
|
|
876
|
+
Batch-loads tickets, users, and fields in parallel, then fetches comments
|
|
877
|
+
for each ticket. Much more efficient than calling get_enriched() in a loop.
|
|
878
|
+
|
|
879
|
+
Args:
|
|
880
|
+
ticket_ids: List of ticket IDs to fetch (max 100)
|
|
881
|
+
|
|
882
|
+
Returns:
|
|
883
|
+
List of EnrichedTicket objects
|
|
884
|
+
"""
|
|
885
|
+
if not ticket_ids:
|
|
886
|
+
return []
|
|
887
|
+
|
|
888
|
+
# Fetch tickets and fields in parallel (independent calls)
|
|
889
|
+
tickets_dict, fields = await asyncio.gather(
|
|
890
|
+
self.get_many(ticket_ids),
|
|
891
|
+
self._fetch_fields(),
|
|
892
|
+
)
|
|
893
|
+
if not tickets_dict:
|
|
894
|
+
return []
|
|
895
|
+
|
|
896
|
+
tickets = list(tickets_dict.values())
|
|
897
|
+
user_ids = self._collect_user_ids_from_tickets(tickets)
|
|
898
|
+
ticket_users = await self._fetch_users_batch(user_ids)
|
|
899
|
+
|
|
900
|
+
return await self._build_enriched_tickets(tickets, ticket_users, fields)
|
|
901
|
+
|
|
847
902
|
async def search_enriched(
|
|
848
903
|
self,
|
|
849
904
|
query: Union[str, SearchQueryConfig],
|
|
@@ -4,6 +4,7 @@ from .base import ZendeskModel
|
|
|
4
4
|
from .comment import Comment, CommentAttachment, CommentMetadata, CommentVia
|
|
5
5
|
from .enriched_ticket import EnrichedTicket
|
|
6
6
|
from .group import Group
|
|
7
|
+
from .group_membership import GroupMembership
|
|
7
8
|
from .help_center import Article, Category, Section
|
|
8
9
|
from .organization import Organization, OrganizationField, OrganizationSubscription
|
|
9
10
|
from .search import (
|
|
@@ -43,6 +44,7 @@ __all__ = [
|
|
|
43
44
|
"PasswordRequirements",
|
|
44
45
|
# Group models
|
|
45
46
|
"Group",
|
|
47
|
+
"GroupMembership",
|
|
46
48
|
# Organization models
|
|
47
49
|
"Organization",
|
|
48
50
|
"OrganizationField",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Group Membership model for Zendesk API."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
|
|
8
|
+
from .base import ZendeskModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GroupMembership(ZendeskModel):
|
|
12
|
+
"""Zendesk Group Membership model.
|
|
13
|
+
|
|
14
|
+
Represents the association between a user (agent) and a group.
|
|
15
|
+
Agents can be members of multiple groups, and one group is marked
|
|
16
|
+
as their default group for ticket assignment.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# Read-only fields
|
|
20
|
+
id: Optional[int] = Field(None, description="Automatically assigned when creating memberships")
|
|
21
|
+
url: Optional[str] = Field(None, description="The API url of the membership")
|
|
22
|
+
user_id: int = Field(..., description="The ID of the agent")
|
|
23
|
+
group_id: int = Field(..., description="The ID of the group")
|
|
24
|
+
default: Optional[bool] = Field(None, description="If true, tickets assigned directly to the agent use this group")
|
|
25
|
+
created_at: Optional[datetime] = Field(None, description="The time the membership was created")
|
|
26
|
+
updated_at: Optional[datetime] = Field(None, description="The time of the last update")
|
|
27
|
+
|
|
28
|
+
def __str__(self) -> str:
|
|
29
|
+
"""Human-readable string representation."""
|
|
30
|
+
default_str = " (default)" if self.default else ""
|
|
31
|
+
return f"User {self.user_id} -> Group {self.group_id}{default_str} (id={self.id})"
|
|
@@ -5,7 +5,7 @@ from abc import ABC, abstractmethod
|
|
|
5
5
|
from typing import Any, AsyncIterator, Dict, Generic, List, Optional, TypeVar
|
|
6
6
|
|
|
7
7
|
from .exceptions import ZendeskPaginationException
|
|
8
|
-
from .models import Article, Category, Comment, Group, Organization, Section, Ticket, TicketField, User
|
|
8
|
+
from .models import Article, Category, Comment, Group, GroupMembership, Organization, Section, Ticket, TicketField, User
|
|
9
9
|
|
|
10
10
|
logger = logging.getLogger(__name__)
|
|
11
11
|
|
|
@@ -557,6 +557,32 @@ class ZendeskPaginator:
|
|
|
557
557
|
|
|
558
558
|
return AssignableGroupsPaginator(http_client, "groups/assignable.json", per_page=per_page, limit=limit)
|
|
559
559
|
|
|
560
|
+
@staticmethod
|
|
561
|
+
def create_group_memberships_paginator(
|
|
562
|
+
http_client: Any, per_page: int = 100, limit: Optional[int] = None
|
|
563
|
+
) -> OffsetPaginator[GroupMembership]:
|
|
564
|
+
"""Create paginator for all group memberships endpoint."""
|
|
565
|
+
|
|
566
|
+
class GroupMembershipsPaginator(OffsetPaginator[GroupMembership]):
|
|
567
|
+
def _extract_items(self, response: Dict[str, Any]) -> List[GroupMembership]:
|
|
568
|
+
return [GroupMembership(**m) for m in response.get("group_memberships", [])]
|
|
569
|
+
|
|
570
|
+
return GroupMembershipsPaginator(http_client, "group_memberships.json", per_page=per_page, limit=limit)
|
|
571
|
+
|
|
572
|
+
@staticmethod
|
|
573
|
+
def create_group_memberships_by_group_paginator(
|
|
574
|
+
http_client: Any, group_id: int, per_page: int = 100, limit: Optional[int] = None
|
|
575
|
+
) -> OffsetPaginator[GroupMembership]:
|
|
576
|
+
"""Create paginator for memberships of a specific group."""
|
|
577
|
+
|
|
578
|
+
class GroupMembershipsByGroupPaginator(OffsetPaginator[GroupMembership]):
|
|
579
|
+
def _extract_items(self, response: Dict[str, Any]) -> List[GroupMembership]:
|
|
580
|
+
return [GroupMembership(**m) for m in response.get("group_memberships", [])]
|
|
581
|
+
|
|
582
|
+
return GroupMembershipsByGroupPaginator(
|
|
583
|
+
http_client, f"groups/{group_id}/memberships.json", per_page=per_page, limit=limit
|
|
584
|
+
)
|
|
585
|
+
|
|
560
586
|
@staticmethod
|
|
561
587
|
def create_ticket_fields_paginator(
|
|
562
588
|
http_client: Any, per_page: int = 100, limit: Optional[int] = None
|
|
@@ -13,7 +13,7 @@ from zendesk_sdk.clients import (
|
|
|
13
13
|
TicketsClient,
|
|
14
14
|
UsersClient,
|
|
15
15
|
)
|
|
16
|
-
from zendesk_sdk.models import Comment, Organization, Ticket, User
|
|
16
|
+
from zendesk_sdk.models import Comment, EnrichedTicket, Organization, Ticket, User
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class TestUsersClient:
|
|
@@ -960,6 +960,98 @@ class TestTicketsClient:
|
|
|
960
960
|
assert result is True
|
|
961
961
|
mock_delete.assert_called_once_with("tickets/12345.json")
|
|
962
962
|
|
|
963
|
+
@pytest.mark.asyncio
|
|
964
|
+
async def test_get_many(self):
|
|
965
|
+
"""Test batch get tickets by IDs."""
|
|
966
|
+
client = self.get_client()
|
|
967
|
+
response_data = {
|
|
968
|
+
"tickets": [
|
|
969
|
+
{"id": 1, "subject": "Ticket 1", "status": "open", "created_at": "2023-01-01T00:00:00Z"},
|
|
970
|
+
{"id": 2, "subject": "Ticket 2", "status": "pending", "created_at": "2023-01-02T00:00:00Z"},
|
|
971
|
+
{"id": 3, "subject": "Ticket 3", "status": "solved", "created_at": "2023-01-03T00:00:00Z"},
|
|
972
|
+
]
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
with patch.object(client, "_get", new_callable=AsyncMock) as mock_get:
|
|
976
|
+
mock_get.return_value = response_data
|
|
977
|
+
|
|
978
|
+
result = await client.get_many([1, 2, 3])
|
|
979
|
+
|
|
980
|
+
assert len(result) == 3
|
|
981
|
+
assert isinstance(result[1], Ticket)
|
|
982
|
+
assert result[1].subject == "Ticket 1"
|
|
983
|
+
assert result[2].subject == "Ticket 2"
|
|
984
|
+
assert result[3].subject == "Ticket 3"
|
|
985
|
+
mock_get.assert_called_once()
|
|
986
|
+
call_url = mock_get.call_args[0][0]
|
|
987
|
+
assert call_url.startswith("tickets/show_many.json?ids=")
|
|
988
|
+
|
|
989
|
+
@pytest.mark.asyncio
|
|
990
|
+
async def test_get_many_empty(self):
|
|
991
|
+
"""Test batch get tickets with empty list."""
|
|
992
|
+
client = self.get_client()
|
|
993
|
+
|
|
994
|
+
result = await client.get_many([])
|
|
995
|
+
|
|
996
|
+
assert result == {}
|
|
997
|
+
|
|
998
|
+
@pytest.mark.asyncio
|
|
999
|
+
async def test_get_many_deduplicates(self):
|
|
1000
|
+
"""Test batch get tickets deduplicates IDs."""
|
|
1001
|
+
client = self.get_client()
|
|
1002
|
+
response_data = {
|
|
1003
|
+
"tickets": [
|
|
1004
|
+
{"id": 1, "subject": "Ticket 1", "status": "open", "created_at": "2023-01-01T00:00:00Z"},
|
|
1005
|
+
]
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
with patch.object(client, "_get", new_callable=AsyncMock) as mock_get:
|
|
1009
|
+
mock_get.return_value = response_data
|
|
1010
|
+
|
|
1011
|
+
result = await client.get_many([1, 1, 1])
|
|
1012
|
+
|
|
1013
|
+
assert len(result) == 1
|
|
1014
|
+
call_url = mock_get.call_args[0][0]
|
|
1015
|
+
assert "ids=1" in call_url
|
|
1016
|
+
|
|
1017
|
+
@pytest.mark.asyncio
|
|
1018
|
+
async def test_get_many_enriched(self):
|
|
1019
|
+
"""Test batch get enriched tickets."""
|
|
1020
|
+
client = self.get_client()
|
|
1021
|
+
|
|
1022
|
+
tickets_dict = {
|
|
1023
|
+
1: Ticket(id=1, subject="T1", status="open", requester_id=100, created_at="2023-01-01T00:00:00Z"),
|
|
1024
|
+
2: Ticket(id=2, subject="T2", status="open", requester_id=200, created_at="2023-01-02T00:00:00Z"),
|
|
1025
|
+
}
|
|
1026
|
+
mock_users = {100: User(id=100, name="User A"), 200: User(id=200, name="User B")}
|
|
1027
|
+
mock_enriched = [
|
|
1028
|
+
EnrichedTicket(ticket=tickets_dict[1], comments=[], users={100: mock_users[100]}, fields={}),
|
|
1029
|
+
EnrichedTicket(ticket=tickets_dict[2], comments=[], users={200: mock_users[200]}, fields={}),
|
|
1030
|
+
]
|
|
1031
|
+
|
|
1032
|
+
with (
|
|
1033
|
+
patch.object(client, "get_many", new_callable=AsyncMock, return_value=tickets_dict) as mock_get_many,
|
|
1034
|
+
patch.object(client, "_fetch_users_batch", new_callable=AsyncMock, return_value=mock_users),
|
|
1035
|
+
patch.object(client, "_fetch_fields", new_callable=AsyncMock, return_value={}),
|
|
1036
|
+
patch.object(client, "_build_enriched_tickets", new_callable=AsyncMock, return_value=mock_enriched),
|
|
1037
|
+
):
|
|
1038
|
+
result = await client.get_many_enriched([1, 2])
|
|
1039
|
+
|
|
1040
|
+
assert len(result) == 2
|
|
1041
|
+
assert isinstance(result[0], EnrichedTicket)
|
|
1042
|
+
assert result[0].ticket.id == 1
|
|
1043
|
+
assert result[1].ticket.id == 2
|
|
1044
|
+
mock_get_many.assert_called_once_with([1, 2])
|
|
1045
|
+
|
|
1046
|
+
@pytest.mark.asyncio
|
|
1047
|
+
async def test_get_many_enriched_empty(self):
|
|
1048
|
+
"""Test batch get enriched tickets with empty list."""
|
|
1049
|
+
client = self.get_client()
|
|
1050
|
+
|
|
1051
|
+
result = await client.get_many_enriched([])
|
|
1052
|
+
|
|
1053
|
+
assert result == []
|
|
1054
|
+
|
|
963
1055
|
|
|
964
1056
|
class TestCommentsClient:
|
|
965
1057
|
"""Test cases for CommentsClient."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/clients/attachments.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/clients/organizations.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/clients/ticket_fields.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/models/enriched_ticket.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/models/help_center.py
RENAMED
|
File without changes
|
{python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/models/organization.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|