python-zendesk-sdk 0.12.0__tar.gz → 0.14.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.12.0 → python_zendesk_sdk-0.14.0}/.gitignore +4 -1
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/PKG-INFO +117 -3
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/README.md +116 -2
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/examples/pagination_example.py +19 -1
- python_zendesk_sdk-0.14.0/examples/ticket_metrics_example.py +74 -0
- python_zendesk_sdk-0.14.0/examples/views.py +84 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/pyproject.toml +1 -1
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/__init__.py +11 -1
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/client.py +54 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/__init__.py +4 -0
- python_zendesk_sdk-0.14.0/src/zendesk_sdk/clients/ticket_metrics.py +84 -0
- python_zendesk_sdk-0.14.0/src/zendesk_sdk/clients/views.py +198 -0
- python_zendesk_sdk-0.14.0/src/zendesk_sdk/config.py +179 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/http_client.py +51 -2
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/__init__.py +4 -0
- python_zendesk_sdk-0.14.0/src/zendesk_sdk/models/view.py +69 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/pagination.py +102 -1
- python_zendesk_sdk-0.14.0/tests/test_config.py +216 -0
- python_zendesk_sdk-0.14.0/tests/test_http_client.py +368 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/tests/test_package_import.py +1 -1
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/tests/test_pagination.py +112 -0
- python_zendesk_sdk-0.14.0/tests/test_ticket_metrics_client.py +114 -0
- python_zendesk_sdk-0.14.0/tests/test_views_client.py +248 -0
- python_zendesk_sdk-0.12.0/src/zendesk_sdk/config.py +0 -126
- python_zendesk_sdk-0.12.0/tests/test_config.py +0 -92
- python_zendesk_sdk-0.12.0/tests/test_http_client.py +0 -180
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/.flake8 +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/.github/workflows/publish.yml +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/.python-version +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/LICENSE +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/context7.json +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/examples/basic_usage.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/examples/caching.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/examples/enriched_tickets.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/examples/error_handling.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/examples/groups.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/examples/help_center.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/examples/organizations.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/examples/search.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/examples/users.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/attachments.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/base.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/groups.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/help_center/__init__.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/help_center/articles.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/help_center/categories.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/help_center/sections.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/organizations.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/search.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/ticket_fields.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/tickets.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/users.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/exceptions.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/base.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/comment.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/enriched_ticket.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/group.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/group_membership.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/help_center.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/organization.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/search.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/ticket.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/user.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/tests/__init__.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/tests/test_client.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/tests/test_clients.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/tests/test_exceptions.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/tests/test_help_center_client.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/tests/test_models.py +0 -0
- {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/tests/test_search_query_config.py +0 -0
|
@@ -190,4 +190,7 @@ debug_*.py
|
|
|
190
190
|
|
|
191
191
|
# Local directories
|
|
192
192
|
.claude/
|
|
193
|
-
|
|
193
|
+
CLAUDE.md
|
|
194
|
+
# Note: `llm/` is excluded via .git/info/exclude (local git-ignore not committed)
|
|
195
|
+
# so Claude Code can still see it via @-autocomplete, which honors .gitignore
|
|
196
|
+
# but not .git/info/exclude.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-zendesk-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.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
|
|
@@ -63,11 +63,14 @@ Modern Python SDK for Zendesk API, designed for automation and AI agents.
|
|
|
63
63
|
- [Comments](#comments-nested-under-tickets)
|
|
64
64
|
- [Tags](#tags-nested-under-tickets)
|
|
65
65
|
- [Ticket Fields](#ticket-fields)
|
|
66
|
+
- [Ticket Metrics](#ticket-metrics)
|
|
66
67
|
- [Enriched Tickets](#enriched-tickets)
|
|
67
68
|
- [Attachments](#attachments)
|
|
68
69
|
- [Search](#search)
|
|
70
|
+
- [Views](#views)
|
|
69
71
|
- [Help Center](#help-center)
|
|
70
72
|
- [Error Handling](#error-handling)
|
|
73
|
+
- [Proactive Rate Limiting](#proactive-rate-limiting)
|
|
71
74
|
- [Caching](#caching)
|
|
72
75
|
- [Examples](#examples)
|
|
73
76
|
|
|
@@ -102,6 +105,8 @@ Zendesk has a powerful REST API, but using it directly is painful:
|
|
|
102
105
|
- **Caching**: TTL-based caching for users, organizations, and Help Center
|
|
103
106
|
- **Help Center**: Full CRUD for Categories, Sections, and Articles
|
|
104
107
|
- **Async HTTP**: Built on httpx with retry logic, rate limiting, exponential backoff
|
|
108
|
+
- **Proactive Rate Limiting**: Monitors `X-Rate-Limit-Remaining` and throttles before hitting limits
|
|
109
|
+
- **Authentication**: Token auth (Basic Auth) and OAuth (Bearer token)
|
|
105
110
|
- **Configuration**: Environment variables or direct instantiation
|
|
106
111
|
|
|
107
112
|
## Installation
|
|
@@ -141,20 +146,33 @@ asyncio.run(main())
|
|
|
141
146
|
|
|
142
147
|
## Configuration
|
|
143
148
|
|
|
144
|
-
###
|
|
149
|
+
### Token Authentication
|
|
145
150
|
```python
|
|
146
151
|
config = ZendeskConfig(
|
|
147
152
|
subdomain="mycompany",
|
|
148
153
|
email="user@example.com",
|
|
149
|
-
token="api_token_here"
|
|
154
|
+
token="api_token_here",
|
|
155
|
+
)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### OAuth Authentication
|
|
159
|
+
```python
|
|
160
|
+
config = ZendeskConfig(
|
|
161
|
+
subdomain="mycompany",
|
|
162
|
+
oauth_token="your_oauth_token",
|
|
150
163
|
)
|
|
151
164
|
```
|
|
152
165
|
|
|
153
166
|
### Environment variables
|
|
154
167
|
```bash
|
|
168
|
+
# Token auth
|
|
155
169
|
export ZENDESK_SUBDOMAIN=mycompany
|
|
156
170
|
export ZENDESK_EMAIL=user@example.com
|
|
157
171
|
export ZENDESK_TOKEN=api_token_here
|
|
172
|
+
|
|
173
|
+
# Or OAuth
|
|
174
|
+
export ZENDESK_SUBDOMAIN=mycompany
|
|
175
|
+
export ZENDESK_OAUTH_TOKEN=your_oauth_token
|
|
158
176
|
```
|
|
159
177
|
|
|
160
178
|
```python
|
|
@@ -178,6 +196,11 @@ async for user in client.users.list():
|
|
|
178
196
|
|
|
179
197
|
# 3. Collect to list
|
|
180
198
|
users = await client.users.list(limit=50).collect()
|
|
199
|
+
|
|
200
|
+
# Get total count without iterating (uses Zendesk's count from response)
|
|
201
|
+
paginator = client.users.list()
|
|
202
|
+
total = await paginator.count() # async; issues a probe request if needed
|
|
203
|
+
cached = paginator.total_count # sync; None until at least one page fetched
|
|
181
204
|
```
|
|
182
205
|
|
|
183
206
|
### Users
|
|
@@ -363,6 +386,32 @@ field = await client.ticket_fields.get(field_id)
|
|
|
363
386
|
field = await client.ticket_fields.get_by_title("Subscription")
|
|
364
387
|
```
|
|
365
388
|
|
|
389
|
+
### Ticket Metrics
|
|
390
|
+
|
|
391
|
+
Read-only access to per-ticket metrics: first reply time, full resolution time,
|
|
392
|
+
agent/requester wait time, on-hold time. Every time field exposes both calendar
|
|
393
|
+
(wall-clock) and business (business-hours) values.
|
|
394
|
+
|
|
395
|
+
```python
|
|
396
|
+
# Metrics for a specific ticket — primary use case
|
|
397
|
+
metrics = await client.ticket_metrics.for_ticket(ticket_id)
|
|
398
|
+
print(metrics.reply_time_in_minutes)
|
|
399
|
+
# {"calendar": 42, "business": 15}
|
|
400
|
+
|
|
401
|
+
# Metric by its own id
|
|
402
|
+
metrics = await client.ticket_metrics.get(metric_id)
|
|
403
|
+
|
|
404
|
+
# Iterate over all metrics (offset pagination)
|
|
405
|
+
async for m in client.ticket_metrics.list(per_page=100):
|
|
406
|
+
if m.full_resolution_time_in_minutes:
|
|
407
|
+
...
|
|
408
|
+
|
|
409
|
+
# Total count (single extra request, see Pagination)
|
|
410
|
+
total = await client.ticket_metrics.list().count()
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Metrics are live data — the client intentionally does not cache responses.
|
|
414
|
+
|
|
366
415
|
### Enriched Tickets
|
|
367
416
|
|
|
368
417
|
Load tickets with all related data (comments, users, field definitions) in minimum API requests:
|
|
@@ -527,6 +576,48 @@ async for ticket in client.search.export_tickets("priority:high", limit=500):
|
|
|
527
576
|
| `search.tickets()` | 1000 max | Offset | Possible |
|
|
528
577
|
| `search.export_tickets()` | None | Cursor | None |
|
|
529
578
|
|
|
579
|
+
### Views
|
|
580
|
+
|
|
581
|
+
Views are saved searches over tickets — the day-to-day workspace of an
|
|
582
|
+
agent. Read-only access through `client.views`: list views, fetch a
|
|
583
|
+
view's tickets, count tickets without loading them. Useful for
|
|
584
|
+
LLM-agents (skip query construction — use views the support team has
|
|
585
|
+
already curated) and dashboards (counts in one request).
|
|
586
|
+
|
|
587
|
+
```python
|
|
588
|
+
# List views
|
|
589
|
+
async for view in client.views.list():
|
|
590
|
+
print(f"{view.title} (active={view.active})")
|
|
591
|
+
|
|
592
|
+
# Active views only
|
|
593
|
+
active = await client.views.list(active_only=True).collect()
|
|
594
|
+
|
|
595
|
+
# Get a single view (cached)
|
|
596
|
+
view = await client.views.get(12345)
|
|
597
|
+
print(view.conditions) # raw {'all': [...], 'any': [...]}
|
|
598
|
+
|
|
599
|
+
# Get many views in one request (max 100 IDs)
|
|
600
|
+
views = await client.views.get_many([1, 2, 3])
|
|
601
|
+
|
|
602
|
+
# Iterate tickets in a view
|
|
603
|
+
async for ticket in client.views.tickets(12345):
|
|
604
|
+
print(f"#{ticket.id}: {ticket.subject}")
|
|
605
|
+
|
|
606
|
+
# Count tickets without loading them
|
|
607
|
+
count = await client.views.count(12345)
|
|
608
|
+
print(f"{count.value} tickets (fresh={count.fresh})")
|
|
609
|
+
|
|
610
|
+
# Counts for several views in one request (great for dashboards, max 20 IDs)
|
|
611
|
+
counts = await client.views.count_many([1, 2, 3])
|
|
612
|
+
for c in counts:
|
|
613
|
+
print(f"view {c.view_id}: {c.value}")
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
> **Read-only** by design. Views are owned by admins and rarely edited
|
|
617
|
+
> via API. For very large views the count is served from a server-side
|
|
618
|
+
> cache — check `ViewCount.fresh` to know if it's authoritative.
|
|
619
|
+
|
|
620
|
+
|
|
530
621
|
### Help Center
|
|
531
622
|
|
|
532
623
|
Access Help Center (Guide) via `client.help_center` namespace:
|
|
@@ -656,6 +747,29 @@ config = ZendeskConfig(
|
|
|
656
747
|
)
|
|
657
748
|
```
|
|
658
749
|
|
|
750
|
+
### Proactive Rate Limiting
|
|
751
|
+
|
|
752
|
+
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:
|
|
753
|
+
|
|
754
|
+
```python
|
|
755
|
+
config = ZendeskConfig(
|
|
756
|
+
subdomain="mycompany",
|
|
757
|
+
email="user@example.com",
|
|
758
|
+
token="api_token",
|
|
759
|
+
proactive_ratelimit=50, # Start throttling when < 50 requests remaining
|
|
760
|
+
proactive_ratelimit_request_interval=10, # Wait 10s between requests when throttling
|
|
761
|
+
)
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
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.
|
|
765
|
+
|
|
766
|
+
| Parameter | Default | Description |
|
|
767
|
+
|-----------|---------|-------------|
|
|
768
|
+
| `proactive_ratelimit` | None (disabled) | Threshold to start throttling |
|
|
769
|
+
| `proactive_ratelimit_request_interval` | 10 | Seconds to wait between requests when throttling |
|
|
770
|
+
|
|
771
|
+
Zendesk typically allows 400 requests per minute. A threshold of 40-60 provides a good safety margin.
|
|
772
|
+
|
|
659
773
|
## Caching
|
|
660
774
|
|
|
661
775
|
The SDK includes built-in caching for frequently accessed resources. Caching is enabled by default and can be configured or disabled.
|
|
@@ -25,11 +25,14 @@ Modern Python SDK for Zendesk API, designed for automation and AI agents.
|
|
|
25
25
|
- [Comments](#comments-nested-under-tickets)
|
|
26
26
|
- [Tags](#tags-nested-under-tickets)
|
|
27
27
|
- [Ticket Fields](#ticket-fields)
|
|
28
|
+
- [Ticket Metrics](#ticket-metrics)
|
|
28
29
|
- [Enriched Tickets](#enriched-tickets)
|
|
29
30
|
- [Attachments](#attachments)
|
|
30
31
|
- [Search](#search)
|
|
32
|
+
- [Views](#views)
|
|
31
33
|
- [Help Center](#help-center)
|
|
32
34
|
- [Error Handling](#error-handling)
|
|
35
|
+
- [Proactive Rate Limiting](#proactive-rate-limiting)
|
|
33
36
|
- [Caching](#caching)
|
|
34
37
|
- [Examples](#examples)
|
|
35
38
|
|
|
@@ -64,6 +67,8 @@ Zendesk has a powerful REST API, but using it directly is painful:
|
|
|
64
67
|
- **Caching**: TTL-based caching for users, organizations, and Help Center
|
|
65
68
|
- **Help Center**: Full CRUD for Categories, Sections, and Articles
|
|
66
69
|
- **Async HTTP**: Built on httpx with retry logic, rate limiting, exponential backoff
|
|
70
|
+
- **Proactive Rate Limiting**: Monitors `X-Rate-Limit-Remaining` and throttles before hitting limits
|
|
71
|
+
- **Authentication**: Token auth (Basic Auth) and OAuth (Bearer token)
|
|
67
72
|
- **Configuration**: Environment variables or direct instantiation
|
|
68
73
|
|
|
69
74
|
## Installation
|
|
@@ -103,20 +108,33 @@ asyncio.run(main())
|
|
|
103
108
|
|
|
104
109
|
## Configuration
|
|
105
110
|
|
|
106
|
-
###
|
|
111
|
+
### Token Authentication
|
|
107
112
|
```python
|
|
108
113
|
config = ZendeskConfig(
|
|
109
114
|
subdomain="mycompany",
|
|
110
115
|
email="user@example.com",
|
|
111
|
-
token="api_token_here"
|
|
116
|
+
token="api_token_here",
|
|
117
|
+
)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### OAuth Authentication
|
|
121
|
+
```python
|
|
122
|
+
config = ZendeskConfig(
|
|
123
|
+
subdomain="mycompany",
|
|
124
|
+
oauth_token="your_oauth_token",
|
|
112
125
|
)
|
|
113
126
|
```
|
|
114
127
|
|
|
115
128
|
### Environment variables
|
|
116
129
|
```bash
|
|
130
|
+
# Token auth
|
|
117
131
|
export ZENDESK_SUBDOMAIN=mycompany
|
|
118
132
|
export ZENDESK_EMAIL=user@example.com
|
|
119
133
|
export ZENDESK_TOKEN=api_token_here
|
|
134
|
+
|
|
135
|
+
# Or OAuth
|
|
136
|
+
export ZENDESK_SUBDOMAIN=mycompany
|
|
137
|
+
export ZENDESK_OAUTH_TOKEN=your_oauth_token
|
|
120
138
|
```
|
|
121
139
|
|
|
122
140
|
```python
|
|
@@ -140,6 +158,11 @@ async for user in client.users.list():
|
|
|
140
158
|
|
|
141
159
|
# 3. Collect to list
|
|
142
160
|
users = await client.users.list(limit=50).collect()
|
|
161
|
+
|
|
162
|
+
# Get total count without iterating (uses Zendesk's count from response)
|
|
163
|
+
paginator = client.users.list()
|
|
164
|
+
total = await paginator.count() # async; issues a probe request if needed
|
|
165
|
+
cached = paginator.total_count # sync; None until at least one page fetched
|
|
143
166
|
```
|
|
144
167
|
|
|
145
168
|
### Users
|
|
@@ -325,6 +348,32 @@ field = await client.ticket_fields.get(field_id)
|
|
|
325
348
|
field = await client.ticket_fields.get_by_title("Subscription")
|
|
326
349
|
```
|
|
327
350
|
|
|
351
|
+
### Ticket Metrics
|
|
352
|
+
|
|
353
|
+
Read-only access to per-ticket metrics: first reply time, full resolution time,
|
|
354
|
+
agent/requester wait time, on-hold time. Every time field exposes both calendar
|
|
355
|
+
(wall-clock) and business (business-hours) values.
|
|
356
|
+
|
|
357
|
+
```python
|
|
358
|
+
# Metrics for a specific ticket — primary use case
|
|
359
|
+
metrics = await client.ticket_metrics.for_ticket(ticket_id)
|
|
360
|
+
print(metrics.reply_time_in_minutes)
|
|
361
|
+
# {"calendar": 42, "business": 15}
|
|
362
|
+
|
|
363
|
+
# Metric by its own id
|
|
364
|
+
metrics = await client.ticket_metrics.get(metric_id)
|
|
365
|
+
|
|
366
|
+
# Iterate over all metrics (offset pagination)
|
|
367
|
+
async for m in client.ticket_metrics.list(per_page=100):
|
|
368
|
+
if m.full_resolution_time_in_minutes:
|
|
369
|
+
...
|
|
370
|
+
|
|
371
|
+
# Total count (single extra request, see Pagination)
|
|
372
|
+
total = await client.ticket_metrics.list().count()
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Metrics are live data — the client intentionally does not cache responses.
|
|
376
|
+
|
|
328
377
|
### Enriched Tickets
|
|
329
378
|
|
|
330
379
|
Load tickets with all related data (comments, users, field definitions) in minimum API requests:
|
|
@@ -489,6 +538,48 @@ async for ticket in client.search.export_tickets("priority:high", limit=500):
|
|
|
489
538
|
| `search.tickets()` | 1000 max | Offset | Possible |
|
|
490
539
|
| `search.export_tickets()` | None | Cursor | None |
|
|
491
540
|
|
|
541
|
+
### Views
|
|
542
|
+
|
|
543
|
+
Views are saved searches over tickets — the day-to-day workspace of an
|
|
544
|
+
agent. Read-only access through `client.views`: list views, fetch a
|
|
545
|
+
view's tickets, count tickets without loading them. Useful for
|
|
546
|
+
LLM-agents (skip query construction — use views the support team has
|
|
547
|
+
already curated) and dashboards (counts in one request).
|
|
548
|
+
|
|
549
|
+
```python
|
|
550
|
+
# List views
|
|
551
|
+
async for view in client.views.list():
|
|
552
|
+
print(f"{view.title} (active={view.active})")
|
|
553
|
+
|
|
554
|
+
# Active views only
|
|
555
|
+
active = await client.views.list(active_only=True).collect()
|
|
556
|
+
|
|
557
|
+
# Get a single view (cached)
|
|
558
|
+
view = await client.views.get(12345)
|
|
559
|
+
print(view.conditions) # raw {'all': [...], 'any': [...]}
|
|
560
|
+
|
|
561
|
+
# Get many views in one request (max 100 IDs)
|
|
562
|
+
views = await client.views.get_many([1, 2, 3])
|
|
563
|
+
|
|
564
|
+
# Iterate tickets in a view
|
|
565
|
+
async for ticket in client.views.tickets(12345):
|
|
566
|
+
print(f"#{ticket.id}: {ticket.subject}")
|
|
567
|
+
|
|
568
|
+
# Count tickets without loading them
|
|
569
|
+
count = await client.views.count(12345)
|
|
570
|
+
print(f"{count.value} tickets (fresh={count.fresh})")
|
|
571
|
+
|
|
572
|
+
# Counts for several views in one request (great for dashboards, max 20 IDs)
|
|
573
|
+
counts = await client.views.count_many([1, 2, 3])
|
|
574
|
+
for c in counts:
|
|
575
|
+
print(f"view {c.view_id}: {c.value}")
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
> **Read-only** by design. Views are owned by admins and rarely edited
|
|
579
|
+
> via API. For very large views the count is served from a server-side
|
|
580
|
+
> cache — check `ViewCount.fresh` to know if it's authoritative.
|
|
581
|
+
|
|
582
|
+
|
|
492
583
|
### Help Center
|
|
493
584
|
|
|
494
585
|
Access Help Center (Guide) via `client.help_center` namespace:
|
|
@@ -618,6 +709,29 @@ config = ZendeskConfig(
|
|
|
618
709
|
)
|
|
619
710
|
```
|
|
620
711
|
|
|
712
|
+
### Proactive Rate Limiting
|
|
713
|
+
|
|
714
|
+
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:
|
|
715
|
+
|
|
716
|
+
```python
|
|
717
|
+
config = ZendeskConfig(
|
|
718
|
+
subdomain="mycompany",
|
|
719
|
+
email="user@example.com",
|
|
720
|
+
token="api_token",
|
|
721
|
+
proactive_ratelimit=50, # Start throttling when < 50 requests remaining
|
|
722
|
+
proactive_ratelimit_request_interval=10, # Wait 10s between requests when throttling
|
|
723
|
+
)
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
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.
|
|
727
|
+
|
|
728
|
+
| Parameter | Default | Description |
|
|
729
|
+
|-----------|---------|-------------|
|
|
730
|
+
| `proactive_ratelimit` | None (disabled) | Threshold to start throttling |
|
|
731
|
+
| `proactive_ratelimit_request_interval` | 10 | Seconds to wait between requests when throttling |
|
|
732
|
+
|
|
733
|
+
Zendesk typically allows 400 requests per minute. A threshold of 40-60 provides a good safety margin.
|
|
734
|
+
|
|
621
735
|
## Caching
|
|
622
736
|
|
|
623
737
|
The SDK includes built-in caching for frequently accessed resources. Caching is enabled by default and can be configured or disabled.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""Pagination example for Zendesk SDK.
|
|
2
2
|
|
|
3
|
-
This example demonstrates
|
|
3
|
+
This example demonstrates four ways to work with paginators:
|
|
4
4
|
1. Get specific page
|
|
5
5
|
2. Iterate through all items
|
|
6
6
|
3. Collect to list
|
|
7
|
+
4. Ask for total count without collecting
|
|
7
8
|
|
|
8
9
|
All list() and search() methods return Paginator objects.
|
|
9
10
|
"""
|
|
@@ -68,6 +69,23 @@ async def main() -> None:
|
|
|
68
69
|
# Collect all (be careful with large datasets!)
|
|
69
70
|
# all_users = await client.users.list().collect()
|
|
70
71
|
|
|
72
|
+
# ==================== Method 4: Total count only ====================
|
|
73
|
+
|
|
74
|
+
print("\n=== Method 4: Total count without collecting ===")
|
|
75
|
+
|
|
76
|
+
# count() issues a lightweight probe (per_page=1) when no page has been
|
|
77
|
+
# fetched yet; returns the cached count otherwise. Does not mutate state.
|
|
78
|
+
total_open = await client.search.tickets("status:open").count()
|
|
79
|
+
print(f"Open tickets in total: {total_open}")
|
|
80
|
+
|
|
81
|
+
# After iteration / get_page(), total_count is a free sync property.
|
|
82
|
+
paginator = client.users.list(per_page=100)
|
|
83
|
+
await paginator.get_page()
|
|
84
|
+
print(f"Users total_count (cached): {paginator.total_count}")
|
|
85
|
+
|
|
86
|
+
# Cursor-based paginators (incremental, search export) return None:
|
|
87
|
+
# the Zendesk API does not expose a total for them.
|
|
88
|
+
|
|
71
89
|
# ==================== Pagination with search ====================
|
|
72
90
|
|
|
73
91
|
print("\n=== Pagination with search ===")
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Ticket Metrics example for Zendesk SDK.
|
|
2
|
+
|
|
3
|
+
Three ways to use ticket_metrics:
|
|
4
|
+
1. Get metrics for a specific ticket (primary use case)
|
|
5
|
+
2. Iterate over all metrics
|
|
6
|
+
3. Find tickets that took long to resolve
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
|
|
11
|
+
from zendesk_sdk import ZendeskClient, ZendeskConfig
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def main() -> None:
|
|
15
|
+
config = ZendeskConfig(
|
|
16
|
+
subdomain="your-subdomain",
|
|
17
|
+
email="your-email@example.com",
|
|
18
|
+
token="your-api-token",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
async with ZendeskClient(config) as client:
|
|
22
|
+
# ==================== Method 1: Metrics for a ticket ====================
|
|
23
|
+
|
|
24
|
+
print("=== Method 1: Metrics for a specific ticket ===")
|
|
25
|
+
|
|
26
|
+
ticket_id = 12345
|
|
27
|
+
metrics = await client.ticket_metrics.for_ticket(ticket_id)
|
|
28
|
+
|
|
29
|
+
# Time fields are dicts with "calendar" (wall-clock) and "business"
|
|
30
|
+
# (business hours) sub-values, either of which may be None.
|
|
31
|
+
reply = metrics.reply_time_in_minutes or {}
|
|
32
|
+
resolution = metrics.full_resolution_time_in_minutes or {}
|
|
33
|
+
|
|
34
|
+
print(f"Ticket #{metrics.ticket_id}")
|
|
35
|
+
print(f" Replies: {metrics.replies}")
|
|
36
|
+
print(f" Reply time (calendar): {reply.get('calendar')} min")
|
|
37
|
+
print(f" Reply time (business): {reply.get('business')} min")
|
|
38
|
+
print(f" Full resolution (calendar): {resolution.get('calendar')} min")
|
|
39
|
+
print(f" Reopens: {metrics.reopens}")
|
|
40
|
+
|
|
41
|
+
# ==================== Method 2: Iterate all ====================
|
|
42
|
+
|
|
43
|
+
print("\n=== Method 2: Iterate over all metrics ===")
|
|
44
|
+
|
|
45
|
+
total = await client.ticket_metrics.list().count()
|
|
46
|
+
print(f"Total ticket_metrics: {total}")
|
|
47
|
+
|
|
48
|
+
count = 0
|
|
49
|
+
async for m in client.ticket_metrics.list(per_page=100, limit=10):
|
|
50
|
+
count += 1
|
|
51
|
+
print(f" #{m.ticket_id}: replies={m.replies}, reopens={m.reopens}")
|
|
52
|
+
|
|
53
|
+
print(f"Looked at {count} metric records")
|
|
54
|
+
|
|
55
|
+
# ==================== Method 3: Long-to-resolve tickets =================
|
|
56
|
+
|
|
57
|
+
print("\n=== Method 3: Tickets that took > 1 day of business hours ===")
|
|
58
|
+
|
|
59
|
+
threshold_minutes = 24 * 60
|
|
60
|
+
slow = []
|
|
61
|
+
async for m in client.ticket_metrics.list(limit=500):
|
|
62
|
+
resolution = m.full_resolution_time_in_minutes or {}
|
|
63
|
+
business = resolution.get("business")
|
|
64
|
+
if business is not None and business > threshold_minutes:
|
|
65
|
+
slow.append((m.ticket_id, business))
|
|
66
|
+
|
|
67
|
+
slow.sort(key=lambda pair: pair[1], reverse=True)
|
|
68
|
+
for ticket_id, business_minutes in slow[:10]:
|
|
69
|
+
hours = business_minutes / 60
|
|
70
|
+
print(f" #{ticket_id}: {hours:.1f} business hours to resolve")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Views API example for Zendesk SDK.
|
|
2
|
+
|
|
3
|
+
This example demonstrates read-only operations on Views:
|
|
4
|
+
- Listing views (all + active only)
|
|
5
|
+
- Getting a single view (cached)
|
|
6
|
+
- Iterating tickets in a view
|
|
7
|
+
- Counting tickets in views (single + bulk)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
|
|
12
|
+
from zendesk_sdk import ZendeskClient, ZendeskConfig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def main() -> None:
|
|
16
|
+
config = ZendeskConfig(
|
|
17
|
+
subdomain="your-subdomain",
|
|
18
|
+
email="your-email@example.com",
|
|
19
|
+
token="your-api-token",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
async with ZendeskClient(config) as client:
|
|
23
|
+
# ==================== List views ====================
|
|
24
|
+
|
|
25
|
+
print("=== All views ===")
|
|
26
|
+
async for view in client.views.list(limit=10):
|
|
27
|
+
status = "active" if view.active else "inactive"
|
|
28
|
+
default = " (default)" if view.default else ""
|
|
29
|
+
print(f" {view.id}: {view.title} [{status}]{default}")
|
|
30
|
+
|
|
31
|
+
print("\n=== Active views only ===")
|
|
32
|
+
active_views = await client.views.list(active_only=True, limit=10).collect()
|
|
33
|
+
for view in active_views:
|
|
34
|
+
print(f" {view.id}: {view.title}")
|
|
35
|
+
|
|
36
|
+
if not active_views:
|
|
37
|
+
print(" (no active views found)")
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
sample_view = active_views[0]
|
|
41
|
+
|
|
42
|
+
# ==================== Get a single view ====================
|
|
43
|
+
|
|
44
|
+
print(f"\n=== Get view {sample_view.id} ===")
|
|
45
|
+
view = await client.views.get(sample_view.id) # type: ignore[arg-type]
|
|
46
|
+
print(f" Title: {view.title}")
|
|
47
|
+
print(f" Description: {view.description or '(none)'}")
|
|
48
|
+
print(f" Conditions: {view.conditions}")
|
|
49
|
+
|
|
50
|
+
# Second call hits the cache
|
|
51
|
+
view_cached = await client.views.get(sample_view.id) # type: ignore[arg-type]
|
|
52
|
+
print(f" (cached) Title: {view_cached.title}")
|
|
53
|
+
|
|
54
|
+
# ==================== get_many ====================
|
|
55
|
+
|
|
56
|
+
print("\n=== get_many ===")
|
|
57
|
+
ids = [v.id for v in active_views[:3] if v.id is not None]
|
|
58
|
+
many = await client.views.get_many(ids)
|
|
59
|
+
for vid, v in many.items():
|
|
60
|
+
print(f" {vid}: {v.title}")
|
|
61
|
+
|
|
62
|
+
# ==================== Tickets in a view ====================
|
|
63
|
+
|
|
64
|
+
print(f"\n=== First 5 tickets in view {sample_view.id} ===")
|
|
65
|
+
async for ticket in client.views.tickets(sample_view.id, limit=5): # type: ignore[arg-type]
|
|
66
|
+
print(f" #{ticket.id}: {ticket.subject} [{ticket.status}]")
|
|
67
|
+
|
|
68
|
+
# ==================== Count tickets in a view ====================
|
|
69
|
+
|
|
70
|
+
print(f"\n=== Count for view {sample_view.id} ===")
|
|
71
|
+
count = await client.views.count(sample_view.id) # type: ignore[arg-type]
|
|
72
|
+
staleness = "fresh" if count.fresh else "stale (cached server-side)"
|
|
73
|
+
print(f" {count.value} tickets ({staleness})")
|
|
74
|
+
|
|
75
|
+
# ==================== count_many for a dashboard ====================
|
|
76
|
+
|
|
77
|
+
print("\n=== count_many for several views ===")
|
|
78
|
+
counts = await client.views.count_many(ids)
|
|
79
|
+
for c in counts:
|
|
80
|
+
print(f" view {c.view_id}: {c.value} tickets")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
asyncio.run(main())
|
|
@@ -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.14.0"
|
|
9
9
|
|
|
10
10
|
from .client import ZendeskClient
|
|
11
11
|
from .clients import (
|
|
@@ -20,8 +20,10 @@ from .clients import (
|
|
|
20
20
|
SectionsClient,
|
|
21
21
|
TagsClient,
|
|
22
22
|
TicketFieldsClient,
|
|
23
|
+
TicketMetricsClient,
|
|
23
24
|
TicketsClient,
|
|
24
25
|
UsersClient,
|
|
26
|
+
ViewsClient,
|
|
25
27
|
)
|
|
26
28
|
from .config import CacheConfig, ZendeskConfig
|
|
27
29
|
from .exceptions import (
|
|
@@ -46,6 +48,7 @@ from .models import (
|
|
|
46
48
|
SortOrder,
|
|
47
49
|
TicketChannel,
|
|
48
50
|
TicketField,
|
|
51
|
+
TicketMetrics,
|
|
49
52
|
TicketPriority,
|
|
50
53
|
TicketPriorityInput,
|
|
51
54
|
TicketStatus,
|
|
@@ -53,6 +56,8 @@ from .models import (
|
|
|
53
56
|
TicketType,
|
|
54
57
|
TicketTypeInput,
|
|
55
58
|
UserRole,
|
|
59
|
+
View,
|
|
60
|
+
ViewCount,
|
|
56
61
|
)
|
|
57
62
|
|
|
58
63
|
__all__ = [
|
|
@@ -66,10 +71,12 @@ __all__ = [
|
|
|
66
71
|
"OrganizationsClient",
|
|
67
72
|
"TicketsClient",
|
|
68
73
|
"TicketFieldsClient",
|
|
74
|
+
"TicketMetricsClient",
|
|
69
75
|
"CommentsClient",
|
|
70
76
|
"TagsClient",
|
|
71
77
|
"AttachmentsClient",
|
|
72
78
|
"SearchClient",
|
|
79
|
+
"ViewsClient",
|
|
73
80
|
# Help Center
|
|
74
81
|
"HelpCenterClient",
|
|
75
82
|
"CategoriesClient",
|
|
@@ -80,9 +87,12 @@ __all__ = [
|
|
|
80
87
|
"GroupMembership",
|
|
81
88
|
"EnrichedTicket",
|
|
82
89
|
"TicketField",
|
|
90
|
+
"TicketMetrics",
|
|
83
91
|
"Category",
|
|
84
92
|
"Section",
|
|
85
93
|
"Article",
|
|
94
|
+
"View",
|
|
95
|
+
"ViewCount",
|
|
86
96
|
# Search
|
|
87
97
|
"SearchQueryConfig",
|
|
88
98
|
"SearchType",
|