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.
Files changed (70) hide show
  1. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/.gitignore +4 -1
  2. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/PKG-INFO +117 -3
  3. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/README.md +116 -2
  4. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/examples/pagination_example.py +19 -1
  5. python_zendesk_sdk-0.14.0/examples/ticket_metrics_example.py +74 -0
  6. python_zendesk_sdk-0.14.0/examples/views.py +84 -0
  7. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/pyproject.toml +1 -1
  8. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/__init__.py +11 -1
  9. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/client.py +54 -0
  10. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/__init__.py +4 -0
  11. python_zendesk_sdk-0.14.0/src/zendesk_sdk/clients/ticket_metrics.py +84 -0
  12. python_zendesk_sdk-0.14.0/src/zendesk_sdk/clients/views.py +198 -0
  13. python_zendesk_sdk-0.14.0/src/zendesk_sdk/config.py +179 -0
  14. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/http_client.py +51 -2
  15. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/__init__.py +4 -0
  16. python_zendesk_sdk-0.14.0/src/zendesk_sdk/models/view.py +69 -0
  17. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/pagination.py +102 -1
  18. python_zendesk_sdk-0.14.0/tests/test_config.py +216 -0
  19. python_zendesk_sdk-0.14.0/tests/test_http_client.py +368 -0
  20. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/tests/test_package_import.py +1 -1
  21. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/tests/test_pagination.py +112 -0
  22. python_zendesk_sdk-0.14.0/tests/test_ticket_metrics_client.py +114 -0
  23. python_zendesk_sdk-0.14.0/tests/test_views_client.py +248 -0
  24. python_zendesk_sdk-0.12.0/src/zendesk_sdk/config.py +0 -126
  25. python_zendesk_sdk-0.12.0/tests/test_config.py +0 -92
  26. python_zendesk_sdk-0.12.0/tests/test_http_client.py +0 -180
  27. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/.flake8 +0 -0
  28. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/.github/workflows/publish.yml +0 -0
  29. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/.python-version +0 -0
  30. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/LICENSE +0 -0
  31. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/context7.json +0 -0
  32. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/examples/basic_usage.py +0 -0
  33. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/examples/caching.py +0 -0
  34. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/examples/enriched_tickets.py +0 -0
  35. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/examples/error_handling.py +0 -0
  36. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/examples/groups.py +0 -0
  37. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/examples/help_center.py +0 -0
  38. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/examples/organizations.py +0 -0
  39. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/examples/search.py +0 -0
  40. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/examples/users.py +0 -0
  41. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/attachments.py +0 -0
  42. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/base.py +0 -0
  43. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/groups.py +0 -0
  44. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/help_center/__init__.py +0 -0
  45. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/help_center/articles.py +0 -0
  46. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/help_center/categories.py +0 -0
  47. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/help_center/sections.py +0 -0
  48. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/organizations.py +0 -0
  49. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/search.py +0 -0
  50. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/ticket_fields.py +0 -0
  51. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/tickets.py +0 -0
  52. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/clients/users.py +0 -0
  53. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/exceptions.py +0 -0
  54. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/base.py +0 -0
  55. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/comment.py +0 -0
  56. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/enriched_ticket.py +0 -0
  57. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/group.py +0 -0
  58. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/group_membership.py +0 -0
  59. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/help_center.py +0 -0
  60. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/organization.py +0 -0
  61. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/search.py +0 -0
  62. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/ticket.py +0 -0
  63. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/src/zendesk_sdk/models/user.py +0 -0
  64. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/tests/__init__.py +0 -0
  65. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/tests/test_client.py +0 -0
  66. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/tests/test_clients.py +0 -0
  67. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/tests/test_exceptions.py +0 -0
  68. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/tests/test_help_center_client.py +0 -0
  69. {python_zendesk_sdk-0.12.0 → python_zendesk_sdk-0.14.0}/tests/test_models.py +0 -0
  70. {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
- llm/
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.12.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
- ### Direct instantiation
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
- ### Direct instantiation
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 three ways to work with paginators:
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())
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-zendesk-sdk"
3
- version = "0.12.0"
3
+ version = "0.14.0"
4
4
  description = "Modern Python SDK for Zendesk API"
5
5
  authors = [
6
6
  {name = "bormog"}
@@ -5,7 +5,7 @@ This package provides a clean, async-first interface to the Zendesk API
5
5
  with full type safety and comprehensive error handling.
6
6
  """
7
7
 
8
- __version__ = "0.12.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",