python-zendesk-sdk 0.13.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.13.0 → python_zendesk_sdk-0.14.0}/.gitignore +4 -2
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/PKG-INFO +76 -1
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/README.md +75 -0
- {python_zendesk_sdk-0.13.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.13.0 → python_zendesk_sdk-0.14.0}/pyproject.toml +1 -1
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/__init__.py +11 -1
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/client.py +54 -0
- {python_zendesk_sdk-0.13.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.13.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/config.py +4 -0
- {python_zendesk_sdk-0.13.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.13.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/pagination.py +102 -1
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/tests/test_package_import.py +1 -1
- {python_zendesk_sdk-0.13.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.13.0 → python_zendesk_sdk-0.14.0}/.flake8 +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/.github/workflows/publish.yml +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/.python-version +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/LICENSE +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/context7.json +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/examples/basic_usage.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/examples/caching.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/examples/enriched_tickets.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/examples/error_handling.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/examples/groups.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/examples/help_center.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/examples/organizations.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/examples/search.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/examples/users.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/attachments.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/base.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/groups.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/help_center/__init__.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/help_center/articles.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/help_center/categories.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/help_center/sections.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/organizations.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/search.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/ticket_fields.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/tickets.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/users.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/exceptions.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/http_client.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/base.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/comment.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/enriched_ticket.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/group.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/group_membership.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/help_center.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/organization.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/search.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/ticket.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/user.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/tests/__init__.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/tests/test_client.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/tests/test_clients.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/tests/test_config.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/tests/test_exceptions.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/tests/test_help_center_client.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/tests/test_http_client.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/tests/test_models.py +0 -0
- {python_zendesk_sdk-0.13.0 → python_zendesk_sdk-0.14.0}/tests/test_search_query_config.py +0 -0
|
@@ -190,5 +190,7 @@ debug_*.py
|
|
|
190
190
|
|
|
191
191
|
# Local directories
|
|
192
192
|
.claude/
|
|
193
|
-
|
|
194
|
-
|
|
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,9 +63,11 @@ 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)
|
|
71
73
|
- [Proactive Rate Limiting](#proactive-rate-limiting)
|
|
@@ -194,6 +196,11 @@ async for user in client.users.list():
|
|
|
194
196
|
|
|
195
197
|
# 3. Collect to list
|
|
196
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
|
|
197
204
|
```
|
|
198
205
|
|
|
199
206
|
### Users
|
|
@@ -379,6 +386,32 @@ field = await client.ticket_fields.get(field_id)
|
|
|
379
386
|
field = await client.ticket_fields.get_by_title("Subscription")
|
|
380
387
|
```
|
|
381
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
|
+
|
|
382
415
|
### Enriched Tickets
|
|
383
416
|
|
|
384
417
|
Load tickets with all related data (comments, users, field definitions) in minimum API requests:
|
|
@@ -543,6 +576,48 @@ async for ticket in client.search.export_tickets("priority:high", limit=500):
|
|
|
543
576
|
| `search.tickets()` | 1000 max | Offset | Possible |
|
|
544
577
|
| `search.export_tickets()` | None | Cursor | None |
|
|
545
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
|
+
|
|
546
621
|
### Help Center
|
|
547
622
|
|
|
548
623
|
Access Help Center (Guide) via `client.help_center` namespace:
|
|
@@ -25,9 +25,11 @@ 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)
|
|
33
35
|
- [Proactive Rate Limiting](#proactive-rate-limiting)
|
|
@@ -156,6 +158,11 @@ async for user in client.users.list():
|
|
|
156
158
|
|
|
157
159
|
# 3. Collect to list
|
|
158
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
|
|
159
166
|
```
|
|
160
167
|
|
|
161
168
|
### Users
|
|
@@ -341,6 +348,32 @@ field = await client.ticket_fields.get(field_id)
|
|
|
341
348
|
field = await client.ticket_fields.get_by_title("Subscription")
|
|
342
349
|
```
|
|
343
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
|
+
|
|
344
377
|
### Enriched Tickets
|
|
345
378
|
|
|
346
379
|
Load tickets with all related data (comments, users, field definitions) in minimum API requests:
|
|
@@ -505,6 +538,48 @@ async for ticket in client.search.export_tickets("priority:high", limit=500):
|
|
|
505
538
|
| `search.tickets()` | 1000 max | Offset | Possible |
|
|
506
539
|
| `search.export_tickets()` | None | Cursor | None |
|
|
507
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
|
+
|
|
508
583
|
### Help Center
|
|
509
584
|
|
|
510
585
|
Access Help Center (Guide) via `client.help_center` namespace:
|
|
@@ -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",
|
|
@@ -14,8 +14,10 @@ if TYPE_CHECKING:
|
|
|
14
14
|
OrganizationsClient,
|
|
15
15
|
SearchClient,
|
|
16
16
|
TicketFieldsClient,
|
|
17
|
+
TicketMetricsClient,
|
|
17
18
|
TicketsClient,
|
|
18
19
|
UsersClient,
|
|
20
|
+
ViewsClient,
|
|
19
21
|
)
|
|
20
22
|
|
|
21
23
|
|
|
@@ -204,6 +206,30 @@ class ZendeskClient:
|
|
|
204
206
|
|
|
205
207
|
return TicketFieldsClient(self.http_client, self.config.cache)
|
|
206
208
|
|
|
209
|
+
@cached_property
|
|
210
|
+
def ticket_metrics(self) -> "TicketMetricsClient":
|
|
211
|
+
"""Access Ticket Metrics API.
|
|
212
|
+
|
|
213
|
+
Read-only access to per-ticket metrics: first reply time, full
|
|
214
|
+
resolution time, agent/requester wait time, on-hold time, etc.
|
|
215
|
+
Time fields expose both calendar and business hours.
|
|
216
|
+
|
|
217
|
+
Example:
|
|
218
|
+
# Metrics for a specific ticket (primary use case)
|
|
219
|
+
metrics = await client.ticket_metrics.for_ticket(12345)
|
|
220
|
+
print(metrics.reply_time_in_minutes) # {"calendar": 42, "business": 15}
|
|
221
|
+
|
|
222
|
+
# Metric by its own id
|
|
223
|
+
metrics = await client.ticket_metrics.get(999)
|
|
224
|
+
|
|
225
|
+
# Iterate over all metrics
|
|
226
|
+
async for m in client.ticket_metrics.list():
|
|
227
|
+
...
|
|
228
|
+
"""
|
|
229
|
+
from .clients import TicketMetricsClient
|
|
230
|
+
|
|
231
|
+
return TicketMetricsClient(self.http_client)
|
|
232
|
+
|
|
207
233
|
@cached_property
|
|
208
234
|
def attachments(self) -> "AttachmentsClient":
|
|
209
235
|
"""Access Attachments API.
|
|
@@ -234,6 +260,34 @@ class ZendeskClient:
|
|
|
234
260
|
|
|
235
261
|
return SearchClient(self.http_client)
|
|
236
262
|
|
|
263
|
+
@cached_property
|
|
264
|
+
def views(self) -> "ViewsClient":
|
|
265
|
+
"""Access Views API (read-only).
|
|
266
|
+
|
|
267
|
+
Views are saved searches over tickets — the way agents work
|
|
268
|
+
with tickets day-to-day. Read-only: list, get, tickets, count,
|
|
269
|
+
count_many.
|
|
270
|
+
|
|
271
|
+
Example:
|
|
272
|
+
# List all views
|
|
273
|
+
async for view in client.views.list():
|
|
274
|
+
print(f"{view.title} (active={view.active})")
|
|
275
|
+
|
|
276
|
+
# Get a specific view (cached)
|
|
277
|
+
view = await client.views.get(12345)
|
|
278
|
+
|
|
279
|
+
# Tickets in a view
|
|
280
|
+
async for ticket in client.views.tickets(12345):
|
|
281
|
+
print(ticket.subject)
|
|
282
|
+
|
|
283
|
+
# Counts without loading tickets
|
|
284
|
+
count = await client.views.count(12345)
|
|
285
|
+
counts = await client.views.count_many([1, 2, 3])
|
|
286
|
+
"""
|
|
287
|
+
from .clients import ViewsClient
|
|
288
|
+
|
|
289
|
+
return ViewsClient(self.http_client, self.config.cache)
|
|
290
|
+
|
|
237
291
|
@cached_property
|
|
238
292
|
def help_center(self) -> "HelpCenterClient":
|
|
239
293
|
"""Access Help Center API with nested categories, sections, articles.
|
|
@@ -6,8 +6,10 @@ from .help_center import ArticlesClient, CategoriesClient, HelpCenterClient, Sec
|
|
|
6
6
|
from .organizations import OrganizationsClient
|
|
7
7
|
from .search import SearchClient
|
|
8
8
|
from .ticket_fields import TicketFieldsClient
|
|
9
|
+
from .ticket_metrics import TicketMetricsClient
|
|
9
10
|
from .tickets import CommentsClient, TagsClient, TicketsClient
|
|
10
11
|
from .users import UsersClient
|
|
12
|
+
from .views import ViewsClient
|
|
11
13
|
|
|
12
14
|
__all__ = [
|
|
13
15
|
# Main clients
|
|
@@ -16,8 +18,10 @@ __all__ = [
|
|
|
16
18
|
"OrganizationsClient",
|
|
17
19
|
"TicketsClient",
|
|
18
20
|
"TicketFieldsClient",
|
|
21
|
+
"TicketMetricsClient",
|
|
19
22
|
"AttachmentsClient",
|
|
20
23
|
"SearchClient",
|
|
24
|
+
"ViewsClient",
|
|
21
25
|
# Ticket sub-clients
|
|
22
26
|
"CommentsClient",
|
|
23
27
|
"TagsClient",
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Ticket Metrics API client."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Optional
|
|
4
|
+
|
|
5
|
+
from ..models.ticket import TicketMetrics
|
|
6
|
+
from ..pagination import ZendeskPaginator
|
|
7
|
+
from .base import BaseClient
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from ..http_client import HTTPClient
|
|
11
|
+
from ..pagination import Paginator
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TicketMetricsClient(BaseClient):
|
|
15
|
+
"""Client for Zendesk Ticket Metrics API.
|
|
16
|
+
|
|
17
|
+
Provides read-only access to per-ticket metrics: first reply time,
|
|
18
|
+
full resolution time, requester/agent wait time, on-hold time, etc.
|
|
19
|
+
All time-based fields expose both calendar and business hours.
|
|
20
|
+
|
|
21
|
+
Metrics are live data — they change with every ticket update, so
|
|
22
|
+
responses are not cached.
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
async with ZendeskClient(config) as client:
|
|
26
|
+
# Metrics for a specific ticket (most common)
|
|
27
|
+
metrics = await client.ticket_metrics.for_ticket(12345)
|
|
28
|
+
print(metrics.reply_time_in_minutes)
|
|
29
|
+
# {"calendar": 42, "business": 15}
|
|
30
|
+
|
|
31
|
+
# Metric by its own id
|
|
32
|
+
metrics = await client.ticket_metrics.get(999)
|
|
33
|
+
|
|
34
|
+
# Iterate over all metrics
|
|
35
|
+
async for m in client.ticket_metrics.list(per_page=100):
|
|
36
|
+
if m.full_resolution_time_in_minutes:
|
|
37
|
+
...
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, http_client: "HTTPClient") -> None:
|
|
41
|
+
"""Initialize TicketMetricsClient.
|
|
42
|
+
|
|
43
|
+
No cache: metrics are live data and change with each ticket update.
|
|
44
|
+
"""
|
|
45
|
+
super().__init__(http_client)
|
|
46
|
+
|
|
47
|
+
async def get(self, metric_id: int) -> TicketMetrics:
|
|
48
|
+
"""Get a ticket metric by its own id.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
metric_id: The ticket metric id (not the ticket id)
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
TicketMetrics object
|
|
55
|
+
"""
|
|
56
|
+
response = await self._get(f"ticket_metrics/{metric_id}.json")
|
|
57
|
+
return TicketMetrics(**response["ticket_metric"])
|
|
58
|
+
|
|
59
|
+
async def for_ticket(self, ticket_id: int) -> TicketMetrics:
|
|
60
|
+
"""Get metrics for a given ticket.
|
|
61
|
+
|
|
62
|
+
This is the primary use case — look up metrics for a ticket
|
|
63
|
+
you already have an id for.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
ticket_id: The ticket id
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
TicketMetrics object
|
|
70
|
+
"""
|
|
71
|
+
response = await self._get(f"tickets/{ticket_id}/metrics.json")
|
|
72
|
+
return TicketMetrics(**response["ticket_metric"])
|
|
73
|
+
|
|
74
|
+
def list(self, per_page: int = 100, limit: Optional[int] = None) -> "Paginator[TicketMetrics]":
|
|
75
|
+
"""Get paginated list of all ticket metrics.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
per_page: Number of metrics per page (max 100)
|
|
79
|
+
limit: Maximum total items when iterating (None = no limit)
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Paginator yielding TicketMetrics objects
|
|
83
|
+
"""
|
|
84
|
+
return ZendeskPaginator.create_ticket_metrics_paginator(self._http, per_page=per_page, limit=limit)
|