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.
Files changed (60) hide show
  1. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/PKG-INFO +17 -2
  2. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/README.md +16 -1
  3. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/examples/enriched_tickets.py +18 -0
  4. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/examples/groups.py +20 -0
  5. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/pyproject.toml +1 -1
  6. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/__init__.py +3 -1
  7. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/clients/groups.py +66 -1
  8. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/clients/tickets.py +55 -0
  9. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/models/__init__.py +2 -0
  10. python_zendesk_sdk-0.12.0/src/zendesk_sdk/models/group_membership.py +31 -0
  11. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/pagination.py +27 -1
  12. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/tests/test_clients.py +93 -1
  13. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/tests/test_package_import.py +1 -1
  14. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/.flake8 +0 -0
  15. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/.github/workflows/publish.yml +0 -0
  16. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/.gitignore +0 -0
  17. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/.python-version +0 -0
  18. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/LICENSE +0 -0
  19. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/context7.json +0 -0
  20. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/examples/basic_usage.py +0 -0
  21. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/examples/caching.py +0 -0
  22. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/examples/error_handling.py +0 -0
  23. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/examples/help_center.py +0 -0
  24. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/examples/organizations.py +0 -0
  25. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/examples/pagination_example.py +0 -0
  26. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/examples/search.py +0 -0
  27. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/examples/users.py +0 -0
  28. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/client.py +0 -0
  29. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/clients/__init__.py +0 -0
  30. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/clients/attachments.py +0 -0
  31. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/clients/base.py +0 -0
  32. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/clients/help_center/__init__.py +0 -0
  33. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/clients/help_center/articles.py +0 -0
  34. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/clients/help_center/categories.py +0 -0
  35. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/clients/help_center/sections.py +0 -0
  36. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/clients/organizations.py +0 -0
  37. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/clients/search.py +0 -0
  38. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/clients/ticket_fields.py +0 -0
  39. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/clients/users.py +0 -0
  40. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/config.py +0 -0
  41. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/exceptions.py +0 -0
  42. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/http_client.py +0 -0
  43. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/models/base.py +0 -0
  44. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/models/comment.py +0 -0
  45. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/models/enriched_ticket.py +0 -0
  46. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/models/group.py +0 -0
  47. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/models/help_center.py +0 -0
  48. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/models/organization.py +0 -0
  49. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/models/search.py +0 -0
  50. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/models/ticket.py +0 -0
  51. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/src/zendesk_sdk/models/user.py +0 -0
  52. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/tests/__init__.py +0 -0
  53. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/tests/test_client.py +0 -0
  54. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/tests/test_config.py +0 -0
  55. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/tests/test_exceptions.py +0 -0
  56. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/tests/test_help_center_client.py +0 -0
  57. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/tests/test_http_client.py +0 -0
  58. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/tests/test_models.py +0 -0
  59. {python_zendesk_sdk-0.10.0 → python_zendesk_sdk-0.12.0}/tests/test_pagination.py +0 -0
  60. {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.10.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 ===")
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-zendesk-sdk"
3
- version = "0.10.0"
3
+ version = "0.12.0"
4
4
  description = "Modern Python SDK for Zendesk API"
5
5
  authors = [
6
6
  {name = "bormog"}
@@ -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.10.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."""
@@ -9,7 +9,7 @@ class TestPackageImport:
9
9
  import zendesk_sdk
10
10
 
11
11
  assert hasattr(zendesk_sdk, "__version__")
12
- assert zendesk_sdk.__version__ == "0.10.0"
12
+ assert zendesk_sdk.__version__ == "0.12.0"
13
13
 
14
14
  def test_client_import(self):
15
15
  """Test importing ZendeskClient."""