python-zendesk-sdk 0.11.0__tar.gz → 0.13.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.11.0 → python_zendesk_sdk-0.13.0}/.gitignore +2 -1
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/PKG-INFO +53 -3
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/README.md +52 -2
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/examples/groups.py +20 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/pyproject.toml +1 -1
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/src/zendesk_sdk/__init__.py +3 -1
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/src/zendesk_sdk/clients/groups.py +66 -1
- python_zendesk_sdk-0.13.0/src/zendesk_sdk/config.py +175 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/src/zendesk_sdk/http_client.py +51 -2
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/src/zendesk_sdk/models/__init__.py +2 -0
- python_zendesk_sdk-0.13.0/src/zendesk_sdk/models/group_membership.py +31 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/src/zendesk_sdk/pagination.py +27 -1
- python_zendesk_sdk-0.13.0/tests/test_config.py +216 -0
- python_zendesk_sdk-0.13.0/tests/test_http_client.py +368 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/tests/test_package_import.py +1 -1
- python_zendesk_sdk-0.11.0/src/zendesk_sdk/config.py +0 -126
- python_zendesk_sdk-0.11.0/tests/test_config.py +0 -92
- python_zendesk_sdk-0.11.0/tests/test_http_client.py +0 -180
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/.flake8 +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/.github/workflows/publish.yml +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/.python-version +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/LICENSE +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/context7.json +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/examples/basic_usage.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/examples/caching.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/examples/enriched_tickets.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/examples/error_handling.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/examples/help_center.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/examples/organizations.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/examples/pagination_example.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/examples/search.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/examples/users.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/src/zendesk_sdk/client.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/src/zendesk_sdk/clients/__init__.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/src/zendesk_sdk/clients/attachments.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/src/zendesk_sdk/clients/base.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/src/zendesk_sdk/clients/help_center/__init__.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/src/zendesk_sdk/clients/help_center/articles.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/src/zendesk_sdk/clients/help_center/categories.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/src/zendesk_sdk/clients/help_center/sections.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/src/zendesk_sdk/clients/organizations.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/src/zendesk_sdk/clients/search.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/src/zendesk_sdk/clients/ticket_fields.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/src/zendesk_sdk/clients/tickets.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/src/zendesk_sdk/clients/users.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/src/zendesk_sdk/exceptions.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/src/zendesk_sdk/models/base.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/src/zendesk_sdk/models/comment.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/src/zendesk_sdk/models/enriched_ticket.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/src/zendesk_sdk/models/group.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/src/zendesk_sdk/models/help_center.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/src/zendesk_sdk/models/organization.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/src/zendesk_sdk/models/search.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/src/zendesk_sdk/models/ticket.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/src/zendesk_sdk/models/user.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/tests/__init__.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/tests/test_client.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/tests/test_clients.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/tests/test_exceptions.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/tests/test_help_center_client.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/tests/test_models.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.0}/tests/test_pagination.py +0 -0
- {python_zendesk_sdk-0.11.0 → python_zendesk_sdk-0.13.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.13.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
|
|
@@ -68,6 +68,7 @@ Modern Python SDK for Zendesk API, designed for automation and AI agents.
|
|
|
68
68
|
- [Search](#search)
|
|
69
69
|
- [Help Center](#help-center)
|
|
70
70
|
- [Error Handling](#error-handling)
|
|
71
|
+
- [Proactive Rate Limiting](#proactive-rate-limiting)
|
|
71
72
|
- [Caching](#caching)
|
|
72
73
|
- [Examples](#examples)
|
|
73
74
|
|
|
@@ -102,6 +103,8 @@ Zendesk has a powerful REST API, but using it directly is painful:
|
|
|
102
103
|
- **Caching**: TTL-based caching for users, organizations, and Help Center
|
|
103
104
|
- **Help Center**: Full CRUD for Categories, Sections, and Articles
|
|
104
105
|
- **Async HTTP**: Built on httpx with retry logic, rate limiting, exponential backoff
|
|
106
|
+
- **Proactive Rate Limiting**: Monitors `X-Rate-Limit-Remaining` and throttles before hitting limits
|
|
107
|
+
- **Authentication**: Token auth (Basic Auth) and OAuth (Bearer token)
|
|
105
108
|
- **Configuration**: Environment variables or direct instantiation
|
|
106
109
|
|
|
107
110
|
## Installation
|
|
@@ -141,20 +144,33 @@ asyncio.run(main())
|
|
|
141
144
|
|
|
142
145
|
## Configuration
|
|
143
146
|
|
|
144
|
-
###
|
|
147
|
+
### Token Authentication
|
|
145
148
|
```python
|
|
146
149
|
config = ZendeskConfig(
|
|
147
150
|
subdomain="mycompany",
|
|
148
151
|
email="user@example.com",
|
|
149
|
-
token="api_token_here"
|
|
152
|
+
token="api_token_here",
|
|
153
|
+
)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### OAuth Authentication
|
|
157
|
+
```python
|
|
158
|
+
config = ZendeskConfig(
|
|
159
|
+
subdomain="mycompany",
|
|
160
|
+
oauth_token="your_oauth_token",
|
|
150
161
|
)
|
|
151
162
|
```
|
|
152
163
|
|
|
153
164
|
### Environment variables
|
|
154
165
|
```bash
|
|
166
|
+
# Token auth
|
|
155
167
|
export ZENDESK_SUBDOMAIN=mycompany
|
|
156
168
|
export ZENDESK_EMAIL=user@example.com
|
|
157
169
|
export ZENDESK_TOKEN=api_token_here
|
|
170
|
+
|
|
171
|
+
# Or OAuth
|
|
172
|
+
export ZENDESK_SUBDOMAIN=mycompany
|
|
173
|
+
export ZENDESK_OAUTH_TOKEN=your_oauth_token
|
|
158
174
|
```
|
|
159
175
|
|
|
160
176
|
```python
|
|
@@ -266,6 +282,17 @@ count = await client.groups.count() # Get total number of groups
|
|
|
266
282
|
paginator = client.groups.list() # List all groups (paginator)
|
|
267
283
|
paginator = client.groups.list_assignable() # List assignable groups (paginator)
|
|
268
284
|
|
|
285
|
+
# Memberships — find which agents belong to a group
|
|
286
|
+
paginator = client.groups.list_memberships() # All memberships (paginator)
|
|
287
|
+
paginator = client.groups.list_group_members(group_id) # Members of specific group (paginator)
|
|
288
|
+
membership = await client.groups.get_membership(membership_id) # Get specific membership
|
|
289
|
+
|
|
290
|
+
# Example: get all agents in a group
|
|
291
|
+
members = await client.groups.list_group_members(group_id).collect()
|
|
292
|
+
for m in members:
|
|
293
|
+
user = await client.users.get(m.user_id)
|
|
294
|
+
print(f" {user.name} (default={m.default})")
|
|
295
|
+
|
|
269
296
|
# Create
|
|
270
297
|
group = await client.groups.create(
|
|
271
298
|
name="Support Team",
|
|
@@ -645,6 +672,29 @@ config = ZendeskConfig(
|
|
|
645
672
|
)
|
|
646
673
|
```
|
|
647
674
|
|
|
675
|
+
### Proactive Rate Limiting
|
|
676
|
+
|
|
677
|
+
By default, the SDK reacts to rate limiting after hitting a 429 response. With proactive rate limiting, the SDK reads the `X-Rate-Limit-Remaining` header from every response and starts throttling **before** hitting the limit:
|
|
678
|
+
|
|
679
|
+
```python
|
|
680
|
+
config = ZendeskConfig(
|
|
681
|
+
subdomain="mycompany",
|
|
682
|
+
email="user@example.com",
|
|
683
|
+
token="api_token",
|
|
684
|
+
proactive_ratelimit=50, # Start throttling when < 50 requests remaining
|
|
685
|
+
proactive_ratelimit_request_interval=10, # Wait 10s between requests when throttling
|
|
686
|
+
)
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
When `X-Rate-Limit-Remaining` drops below the threshold, the SDK pauses between requests to let the quota recover. Once remaining goes back above the threshold, requests resume at full speed.
|
|
690
|
+
|
|
691
|
+
| Parameter | Default | Description |
|
|
692
|
+
|-----------|---------|-------------|
|
|
693
|
+
| `proactive_ratelimit` | None (disabled) | Threshold to start throttling |
|
|
694
|
+
| `proactive_ratelimit_request_interval` | 10 | Seconds to wait between requests when throttling |
|
|
695
|
+
|
|
696
|
+
Zendesk typically allows 400 requests per minute. A threshold of 40-60 provides a good safety margin.
|
|
697
|
+
|
|
648
698
|
## Caching
|
|
649
699
|
|
|
650
700
|
The SDK includes built-in caching for frequently accessed resources. Caching is enabled by default and can be configured or disabled.
|
|
@@ -30,6 +30,7 @@ Modern Python SDK for Zendesk API, designed for automation and AI agents.
|
|
|
30
30
|
- [Search](#search)
|
|
31
31
|
- [Help Center](#help-center)
|
|
32
32
|
- [Error Handling](#error-handling)
|
|
33
|
+
- [Proactive Rate Limiting](#proactive-rate-limiting)
|
|
33
34
|
- [Caching](#caching)
|
|
34
35
|
- [Examples](#examples)
|
|
35
36
|
|
|
@@ -64,6 +65,8 @@ Zendesk has a powerful REST API, but using it directly is painful:
|
|
|
64
65
|
- **Caching**: TTL-based caching for users, organizations, and Help Center
|
|
65
66
|
- **Help Center**: Full CRUD for Categories, Sections, and Articles
|
|
66
67
|
- **Async HTTP**: Built on httpx with retry logic, rate limiting, exponential backoff
|
|
68
|
+
- **Proactive Rate Limiting**: Monitors `X-Rate-Limit-Remaining` and throttles before hitting limits
|
|
69
|
+
- **Authentication**: Token auth (Basic Auth) and OAuth (Bearer token)
|
|
67
70
|
- **Configuration**: Environment variables or direct instantiation
|
|
68
71
|
|
|
69
72
|
## Installation
|
|
@@ -103,20 +106,33 @@ asyncio.run(main())
|
|
|
103
106
|
|
|
104
107
|
## Configuration
|
|
105
108
|
|
|
106
|
-
###
|
|
109
|
+
### Token Authentication
|
|
107
110
|
```python
|
|
108
111
|
config = ZendeskConfig(
|
|
109
112
|
subdomain="mycompany",
|
|
110
113
|
email="user@example.com",
|
|
111
|
-
token="api_token_here"
|
|
114
|
+
token="api_token_here",
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### OAuth Authentication
|
|
119
|
+
```python
|
|
120
|
+
config = ZendeskConfig(
|
|
121
|
+
subdomain="mycompany",
|
|
122
|
+
oauth_token="your_oauth_token",
|
|
112
123
|
)
|
|
113
124
|
```
|
|
114
125
|
|
|
115
126
|
### Environment variables
|
|
116
127
|
```bash
|
|
128
|
+
# Token auth
|
|
117
129
|
export ZENDESK_SUBDOMAIN=mycompany
|
|
118
130
|
export ZENDESK_EMAIL=user@example.com
|
|
119
131
|
export ZENDESK_TOKEN=api_token_here
|
|
132
|
+
|
|
133
|
+
# Or OAuth
|
|
134
|
+
export ZENDESK_SUBDOMAIN=mycompany
|
|
135
|
+
export ZENDESK_OAUTH_TOKEN=your_oauth_token
|
|
120
136
|
```
|
|
121
137
|
|
|
122
138
|
```python
|
|
@@ -228,6 +244,17 @@ count = await client.groups.count() # Get total number of groups
|
|
|
228
244
|
paginator = client.groups.list() # List all groups (paginator)
|
|
229
245
|
paginator = client.groups.list_assignable() # List assignable groups (paginator)
|
|
230
246
|
|
|
247
|
+
# Memberships — find which agents belong to a group
|
|
248
|
+
paginator = client.groups.list_memberships() # All memberships (paginator)
|
|
249
|
+
paginator = client.groups.list_group_members(group_id) # Members of specific group (paginator)
|
|
250
|
+
membership = await client.groups.get_membership(membership_id) # Get specific membership
|
|
251
|
+
|
|
252
|
+
# Example: get all agents in a group
|
|
253
|
+
members = await client.groups.list_group_members(group_id).collect()
|
|
254
|
+
for m in members:
|
|
255
|
+
user = await client.users.get(m.user_id)
|
|
256
|
+
print(f" {user.name} (default={m.default})")
|
|
257
|
+
|
|
231
258
|
# Create
|
|
232
259
|
group = await client.groups.create(
|
|
233
260
|
name="Support Team",
|
|
@@ -607,6 +634,29 @@ config = ZendeskConfig(
|
|
|
607
634
|
)
|
|
608
635
|
```
|
|
609
636
|
|
|
637
|
+
### Proactive Rate Limiting
|
|
638
|
+
|
|
639
|
+
By default, the SDK reacts to rate limiting after hitting a 429 response. With proactive rate limiting, the SDK reads the `X-Rate-Limit-Remaining` header from every response and starts throttling **before** hitting the limit:
|
|
640
|
+
|
|
641
|
+
```python
|
|
642
|
+
config = ZendeskConfig(
|
|
643
|
+
subdomain="mycompany",
|
|
644
|
+
email="user@example.com",
|
|
645
|
+
token="api_token",
|
|
646
|
+
proactive_ratelimit=50, # Start throttling when < 50 requests remaining
|
|
647
|
+
proactive_ratelimit_request_interval=10, # Wait 10s between requests when throttling
|
|
648
|
+
)
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
When `X-Rate-Limit-Remaining` drops below the threshold, the SDK pauses between requests to let the quota recover. Once remaining goes back above the threshold, requests resume at full speed.
|
|
652
|
+
|
|
653
|
+
| Parameter | Default | Description |
|
|
654
|
+
|-----------|---------|-------------|
|
|
655
|
+
| `proactive_ratelimit` | None (disabled) | Threshold to start throttling |
|
|
656
|
+
| `proactive_ratelimit_request_interval` | 10 | Seconds to wait between requests when throttling |
|
|
657
|
+
|
|
658
|
+
Zendesk typically allows 400 requests per minute. A threshold of 40-60 provides a good safety margin.
|
|
659
|
+
|
|
610
660
|
## Caching
|
|
611
661
|
|
|
612
662
|
The SDK includes built-in caching for frequently accessed resources. Caching is enabled by default and can be configured or disabled.
|
|
@@ -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.13.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"])
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Configuration management for Zendesk SDK."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field, computed_field, field_validator, model_validator
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CacheConfig(BaseModel):
|
|
10
|
+
"""Cache configuration for Zendesk SDK.
|
|
11
|
+
|
|
12
|
+
Controls TTL (time-to-live) and max size for different resource caches.
|
|
13
|
+
Set enabled=False to disable caching entirely.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
enabled: bool = Field(default=True, description="Enable/disable caching")
|
|
17
|
+
|
|
18
|
+
# Users cache
|
|
19
|
+
user_ttl: int = Field(default=300, description="User cache TTL in seconds (default: 5 min)", ge=0)
|
|
20
|
+
user_maxsize: int = Field(default=1000, description="Max cached users", ge=1)
|
|
21
|
+
|
|
22
|
+
# Organizations cache
|
|
23
|
+
org_ttl: int = Field(default=600, description="Organization cache TTL in seconds (default: 10 min)", ge=0)
|
|
24
|
+
org_maxsize: int = Field(default=500, description="Max cached organizations", ge=1)
|
|
25
|
+
|
|
26
|
+
# Groups cache
|
|
27
|
+
group_ttl: int = Field(default=600, description="Group cache TTL in seconds (default: 10 min)", ge=0)
|
|
28
|
+
group_maxsize: int = Field(default=500, description="Max cached groups", ge=1)
|
|
29
|
+
|
|
30
|
+
# Help Center cache
|
|
31
|
+
article_ttl: int = Field(default=900, description="Article cache TTL in seconds (default: 15 min)", ge=0)
|
|
32
|
+
article_maxsize: int = Field(default=500, description="Max cached articles", ge=1)
|
|
33
|
+
|
|
34
|
+
category_ttl: int = Field(default=1800, description="Category cache TTL in seconds (default: 30 min)", ge=0)
|
|
35
|
+
category_maxsize: int = Field(default=200, description="Max cached categories", ge=1)
|
|
36
|
+
|
|
37
|
+
section_ttl: int = Field(default=1800, description="Section cache TTL in seconds (default: 30 min)", ge=0)
|
|
38
|
+
section_maxsize: int = Field(default=200, description="Max cached sections", ge=1)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ZendeskConfig(BaseModel):
|
|
42
|
+
"""Configuration for Zendesk API client.
|
|
43
|
+
|
|
44
|
+
Supports two authentication methods (mutually exclusive):
|
|
45
|
+
- Token auth: email + token (Basic Auth)
|
|
46
|
+
- OAuth: oauth_token (Bearer token)
|
|
47
|
+
|
|
48
|
+
Environment variables: ZENDESK_SUBDOMAIN, ZENDESK_EMAIL, ZENDESK_TOKEN, ZENDESK_OAUTH_TOKEN.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
subdomain: str = Field(
|
|
52
|
+
...,
|
|
53
|
+
description="Zendesk subdomain (e.g., 'mycompany' for mycompany.zendesk.com)",
|
|
54
|
+
min_length=1,
|
|
55
|
+
)
|
|
56
|
+
email: Optional[str] = Field(
|
|
57
|
+
default=None,
|
|
58
|
+
description="User email for token authentication",
|
|
59
|
+
)
|
|
60
|
+
token: Optional[str] = Field(
|
|
61
|
+
default=None,
|
|
62
|
+
description="API token for token authentication",
|
|
63
|
+
)
|
|
64
|
+
oauth_token: Optional[str] = Field(
|
|
65
|
+
default=None,
|
|
66
|
+
description="OAuth token for Bearer authentication",
|
|
67
|
+
)
|
|
68
|
+
timeout: float = Field(
|
|
69
|
+
default=30.0,
|
|
70
|
+
description="HTTP request timeout in seconds",
|
|
71
|
+
gt=0,
|
|
72
|
+
)
|
|
73
|
+
max_retries: int = Field(
|
|
74
|
+
default=3,
|
|
75
|
+
description="Maximum number of retry attempts",
|
|
76
|
+
ge=0,
|
|
77
|
+
)
|
|
78
|
+
proactive_ratelimit: Optional[int] = Field(
|
|
79
|
+
default=None,
|
|
80
|
+
description="Start sleeping when X-Rate-Limit-Remaining drops below this threshold",
|
|
81
|
+
ge=1,
|
|
82
|
+
)
|
|
83
|
+
proactive_ratelimit_request_interval: int = Field(
|
|
84
|
+
default=10,
|
|
85
|
+
description="Seconds to wait between requests when proactive rate limit threshold is reached",
|
|
86
|
+
ge=1,
|
|
87
|
+
)
|
|
88
|
+
cache: CacheConfig = Field(
|
|
89
|
+
default_factory=CacheConfig,
|
|
90
|
+
description="Cache configuration",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def __init__(self, **data: Any) -> None:
|
|
94
|
+
# Load from environment variables if not provided
|
|
95
|
+
if "subdomain" not in data:
|
|
96
|
+
data["subdomain"] = os.getenv("ZENDESK_SUBDOMAIN", data.get("subdomain"))
|
|
97
|
+
|
|
98
|
+
# Only load env vars for one auth method — explicit args take precedence
|
|
99
|
+
has_explicit_token_auth = "email" in data or "token" in data
|
|
100
|
+
has_explicit_oauth = "oauth_token" in data
|
|
101
|
+
|
|
102
|
+
if not has_explicit_token_auth and not has_explicit_oauth:
|
|
103
|
+
# Nothing explicit — try env vars, token auth first
|
|
104
|
+
env_email = os.getenv("ZENDESK_EMAIL")
|
|
105
|
+
env_token = os.getenv("ZENDESK_TOKEN")
|
|
106
|
+
env_oauth = os.getenv("ZENDESK_OAUTH_TOKEN")
|
|
107
|
+
if env_email or env_token:
|
|
108
|
+
data.setdefault("email", env_email)
|
|
109
|
+
data.setdefault("token", env_token)
|
|
110
|
+
elif env_oauth:
|
|
111
|
+
data.setdefault("oauth_token", env_oauth)
|
|
112
|
+
elif has_explicit_token_auth and not has_explicit_oauth:
|
|
113
|
+
# Token auth explicit — fill missing from env
|
|
114
|
+
data.setdefault("email", os.getenv("ZENDESK_EMAIL"))
|
|
115
|
+
data.setdefault("token", os.getenv("ZENDESK_TOKEN"))
|
|
116
|
+
elif has_explicit_oauth and not has_explicit_token_auth:
|
|
117
|
+
# OAuth explicit — only fill oauth from env
|
|
118
|
+
data.setdefault("oauth_token", os.getenv("ZENDESK_OAUTH_TOKEN"))
|
|
119
|
+
|
|
120
|
+
super().__init__(**data)
|
|
121
|
+
|
|
122
|
+
@model_validator(mode="after")
|
|
123
|
+
def validate_auth(self) -> "ZendeskConfig":
|
|
124
|
+
"""Validate that exactly one auth method is provided."""
|
|
125
|
+
has_token_auth = self.email is not None or self.token is not None
|
|
126
|
+
has_oauth = self.oauth_token is not None
|
|
127
|
+
|
|
128
|
+
if has_token_auth and has_oauth:
|
|
129
|
+
raise ValueError("Cannot use both token auth (email/token) and oauth_token simultaneously")
|
|
130
|
+
|
|
131
|
+
if not has_token_auth and not has_oauth:
|
|
132
|
+
raise ValueError("Either email/token or oauth_token must be provided")
|
|
133
|
+
|
|
134
|
+
if has_token_auth:
|
|
135
|
+
if not self.email or not self.token:
|
|
136
|
+
raise ValueError("Both email and token are required for token authentication")
|
|
137
|
+
if "@" not in self.email:
|
|
138
|
+
raise ValueError("Invalid email format")
|
|
139
|
+
|
|
140
|
+
return self
|
|
141
|
+
|
|
142
|
+
@field_validator("subdomain")
|
|
143
|
+
@classmethod
|
|
144
|
+
def validate_subdomain(cls, v: str) -> str:
|
|
145
|
+
"""Validate subdomain format."""
|
|
146
|
+
if not v.replace("-", "").replace("_", "").isalnum():
|
|
147
|
+
raise ValueError("Subdomain can only contain letters, numbers, hyphens and underscores")
|
|
148
|
+
return v.lower()
|
|
149
|
+
|
|
150
|
+
@computed_field # type: ignore[prop-decorator]
|
|
151
|
+
@property
|
|
152
|
+
def endpoint(self) -> str:
|
|
153
|
+
"""Generate the base API endpoint URL."""
|
|
154
|
+
return f"https://{self.subdomain}.zendesk.com/api/v2"
|
|
155
|
+
|
|
156
|
+
@computed_field # type: ignore[prop-decorator]
|
|
157
|
+
@property
|
|
158
|
+
def auth_tuple(self) -> Optional[tuple[str, str]]:
|
|
159
|
+
"""Generate authentication tuple for HTTP requests. None for OAuth mode."""
|
|
160
|
+
if self.email and self.token:
|
|
161
|
+
return f"{self.email}/token", self.token
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
def __repr__(self) -> str:
|
|
165
|
+
"""String representation without exposing credentials."""
|
|
166
|
+
auth_info = f"email='{self.email}'" if self.email else "oauth=True"
|
|
167
|
+
parts = [
|
|
168
|
+
f"subdomain='{self.subdomain}'",
|
|
169
|
+
auth_info,
|
|
170
|
+
f"timeout={self.timeout}",
|
|
171
|
+
f"max_retries={self.max_retries}",
|
|
172
|
+
]
|
|
173
|
+
if self.proactive_ratelimit is not None:
|
|
174
|
+
parts.append(f"proactive_ratelimit={self.proactive_ratelimit}")
|
|
175
|
+
return f"ZendeskConfig({', '.join(parts)})"
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
|
+
from time import monotonic
|
|
5
6
|
from typing import Any, Dict, Optional
|
|
6
7
|
from urllib.parse import urljoin
|
|
7
8
|
|
|
@@ -31,6 +32,10 @@ class HTTPClient:
|
|
|
31
32
|
self._client: Optional[httpx.AsyncClient] = None
|
|
32
33
|
self._closed = False
|
|
33
34
|
|
|
35
|
+
# Proactive rate limit tracking
|
|
36
|
+
self._last_call_time: Optional[float] = None
|
|
37
|
+
self._last_limit_remaining: Optional[int] = None
|
|
38
|
+
|
|
34
39
|
@property
|
|
35
40
|
def client(self) -> httpx.AsyncClient:
|
|
36
41
|
"""Get or create httpx async client."""
|
|
@@ -40,14 +45,18 @@ class HTTPClient:
|
|
|
40
45
|
|
|
41
46
|
def _create_client(self) -> httpx.AsyncClient:
|
|
42
47
|
"""Create configured httpx async client."""
|
|
43
|
-
auth = httpx.BasicAuth(username=self.config.auth_tuple[0], password=self.config.auth_tuple[1])
|
|
44
|
-
|
|
45
48
|
headers = {
|
|
46
49
|
"User-Agent": "python-zendesk-sdk/0.1.0",
|
|
47
50
|
"Content-Type": "application/json",
|
|
48
51
|
"Accept": "application/json",
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
auth: Optional[httpx.BasicAuth] = None
|
|
55
|
+
if self.config.auth_tuple:
|
|
56
|
+
auth = httpx.BasicAuth(username=self.config.auth_tuple[0], password=self.config.auth_tuple[1])
|
|
57
|
+
elif self.config.oauth_token:
|
|
58
|
+
headers["Authorization"] = f"Bearer {self.config.oauth_token}"
|
|
59
|
+
|
|
51
60
|
return httpx.AsyncClient(
|
|
52
61
|
auth=auth,
|
|
53
62
|
headers=headers,
|
|
@@ -55,6 +64,40 @@ class HTTPClient:
|
|
|
55
64
|
limits=httpx.Limits(max_keepalive_connections=10, max_connections=20),
|
|
56
65
|
)
|
|
57
66
|
|
|
67
|
+
async def _apply_proactive_ratelimit(self) -> None:
|
|
68
|
+
"""Apply proactive rate limiting by sleeping if remaining requests are below threshold."""
|
|
69
|
+
if self.config.proactive_ratelimit is None:
|
|
70
|
+
return
|
|
71
|
+
if self._last_limit_remaining is None or self._last_call_time is None:
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
if self._last_limit_remaining >= self.config.proactive_ratelimit: # type: ignore[operator]
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
time_since_last = monotonic() - self._last_call_time
|
|
78
|
+
interval = self.config.proactive_ratelimit_request_interval
|
|
79
|
+
if time_since_last >= interval:
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
remaining_sleep = interval - time_since_last
|
|
83
|
+
logger.warning(
|
|
84
|
+
f"Proactive rate limit: {self._last_limit_remaining} remaining "
|
|
85
|
+
f"(threshold: {self.config.proactive_ratelimit}), sleeping {remaining_sleep:.1f}s"
|
|
86
|
+
)
|
|
87
|
+
await asyncio.sleep(remaining_sleep)
|
|
88
|
+
|
|
89
|
+
def _update_rate_limit_state(self, response: httpx.Response) -> None:
|
|
90
|
+
"""Update rate limit tracking state from response headers."""
|
|
91
|
+
if self.config.proactive_ratelimit is None:
|
|
92
|
+
return
|
|
93
|
+
self._last_call_time = monotonic()
|
|
94
|
+
remaining_header = response.headers.get("X-Rate-Limit-Remaining")
|
|
95
|
+
if remaining_header is not None:
|
|
96
|
+
try:
|
|
97
|
+
self._last_limit_remaining = int(remaining_header)
|
|
98
|
+
except (ValueError, TypeError):
|
|
99
|
+
pass
|
|
100
|
+
|
|
58
101
|
async def _make_request_with_retry(
|
|
59
102
|
self,
|
|
60
103
|
method: str,
|
|
@@ -72,6 +115,9 @@ class HTTPClient:
|
|
|
72
115
|
|
|
73
116
|
for attempt in range(max_retries + 1):
|
|
74
117
|
try:
|
|
118
|
+
# Apply proactive rate limiting before request
|
|
119
|
+
await self._apply_proactive_ratelimit()
|
|
120
|
+
|
|
75
121
|
# Make the actual request
|
|
76
122
|
response = await self.client.request(
|
|
77
123
|
method=method,
|
|
@@ -80,6 +126,9 @@ class HTTPClient:
|
|
|
80
126
|
json=json,
|
|
81
127
|
)
|
|
82
128
|
|
|
129
|
+
# Update rate limit tracking state
|
|
130
|
+
self._update_rate_limit_state(response)
|
|
131
|
+
|
|
83
132
|
# Handle different response types
|
|
84
133
|
retry_info = await self._handle_response(response, attempt, max_retries)
|
|
85
134
|
if retry_info:
|
|
@@ -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})"
|