gl-speech-sdk 0.0.1b1__py3-none-any.whl

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.
@@ -0,0 +1,551 @@
1
+ """Webhook handling for the GL Speech Python client.
2
+
3
+ This module provides the Webhooks class for handling webhook operations
4
+ with the Prosa Speech API, including endpoint management, event handling,
5
+ and delivery management.
6
+
7
+ Authors:
8
+ GDP Labs
9
+
10
+ References:
11
+ https://docs2.prosa.ai/speech/webhook/rest/api/
12
+ """
13
+
14
+ import logging
15
+ from typing import Any
16
+ from urllib.parse import urljoin
17
+
18
+ import httpx
19
+
20
+ from gl_speech_sdk.models import (
21
+ DeliveryTicket,
22
+ WebhookDelivery,
23
+ WebhookEndpoint,
24
+ WebhookEndpointCreate,
25
+ WebhookEndpointListing,
26
+ WebhookEndpointUpdate,
27
+ WebhookEvent,
28
+ WebhookEventListing,
29
+ WebhookRotation,
30
+ WebhookRotationPeriod,
31
+ )
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ class Webhooks:
37
+ """Handles Webhook API operations for the Prosa Speech API."""
38
+
39
+ def __init__(self, client):
40
+ """Initialize Webhooks API.
41
+
42
+ Args:
43
+ client: SpeechClient instance
44
+ """
45
+ self._client = client
46
+
47
+ def _prepare_headers(
48
+ self, extra_headers: dict[str, str] | None = None
49
+ ) -> dict[str, str]:
50
+ """Prepare headers for the API request.
51
+
52
+ Args:
53
+ extra_headers (dict[str, str] | None): Additional headers to merge with default headers
54
+
55
+ Returns:
56
+ dict[str, str]: Dictionary containing the request headers
57
+ """
58
+ headers = self._client.default_headers.copy()
59
+
60
+ if self._client.api_key:
61
+ headers["x-api-key"] = self._client.api_key
62
+
63
+ if extra_headers:
64
+ headers.update(extra_headers)
65
+
66
+ return headers
67
+
68
+ def _make_request(
69
+ self,
70
+ method: str,
71
+ url: str,
72
+ headers: dict[str, str],
73
+ json_data: dict[str, Any] | None = None,
74
+ params: dict[str, Any] | None = None,
75
+ ) -> dict[str, Any] | list[dict[str, Any]] | int:
76
+ """Make an HTTP request to the API.
77
+
78
+ Args:
79
+ method (str): HTTP method (GET, POST, DELETE, PUT)
80
+ url (str): Request URL
81
+ headers (dict[str, str]): Request headers
82
+ json_data (dict[str, Any] | None): JSON body data
83
+ params (dict[str, Any] | None): Query parameters
84
+
85
+ Returns:
86
+ dict[str, Any] | list[dict[str, Any]] | int: Response JSON data
87
+
88
+ Raises:
89
+ httpx.HTTPStatusError: If the request fails
90
+ """
91
+ timeout = httpx.Timeout(self._client.timeout)
92
+
93
+ logger.debug("Request: %s %s", method, url)
94
+ logger.debug("Headers: %s", headers)
95
+ if json_data:
96
+ logger.debug("Body: %s", json_data)
97
+
98
+ with httpx.Client(timeout=timeout) as client:
99
+ response = client.request(
100
+ method=method,
101
+ url=url,
102
+ headers=headers,
103
+ json=json_data,
104
+ params=params,
105
+ )
106
+ response.raise_for_status()
107
+
108
+ if response.status_code == 204 or not response.content:
109
+ return {}
110
+
111
+ try:
112
+ data = response.json()
113
+ except Exception:
114
+ return {}
115
+
116
+ if not isinstance(data, (dict, list, int)):
117
+ raise TypeError(f"Unexpected response type: {type(data)}")
118
+ return data
119
+
120
+ # =========================================================================
121
+ # Endpoint Management
122
+ # =========================================================================
123
+
124
+ def list_endpoints(
125
+ self,
126
+ extra_headers: dict[str, str] | None = None,
127
+ ) -> list[WebhookEndpointListing]:
128
+ """List all webhook endpoints.
129
+
130
+ Args:
131
+ extra_headers (dict[str, str] | None): Additional headers
132
+
133
+ Returns:
134
+ list[WebhookEndpointListing]: List of webhook endpoints
135
+
136
+ Raises:
137
+ httpx.HTTPStatusError: If the API request fails
138
+ """
139
+ logger.debug("Listing webhook endpoints")
140
+
141
+ url = urljoin(self._client.base_url, "webhooks/endpoints")
142
+ headers = self._prepare_headers(extra_headers)
143
+
144
+ response_data = self._make_request("GET", url, headers)
145
+ if not isinstance(response_data, list):
146
+ return []
147
+ return [WebhookEndpointListing(**item) for item in response_data]
148
+
149
+ def create_endpoint(
150
+ self,
151
+ url: str,
152
+ event_filters: list[str] | None = None,
153
+ ssl_verification: bool | None = True,
154
+ secret_key: str | None = None,
155
+ extra_headers: dict[str, str] | None = None,
156
+ ) -> WebhookEndpoint:
157
+ """Create a new webhook endpoint.
158
+
159
+ Args:
160
+ url (str): Callback URL for webhook events
161
+ event_filters (list[str] | None): Event types to filter. Empty = all events.
162
+ ssl_verification (bool | None): Verify SSL certificate. Default: True.
163
+ secret_key (str | None): Secret key for signing. Empty = auto-generated.
164
+ extra_headers (dict[str, str] | None): Additional headers
165
+
166
+ Returns:
167
+ WebhookEndpoint: Created webhook endpoint with secrets
168
+
169
+ Raises:
170
+ ValueError: If url is empty
171
+ httpx.HTTPStatusError: If the API request fails
172
+ """
173
+ if not url:
174
+ raise ValueError("url cannot be empty")
175
+
176
+ logger.debug("Creating webhook endpoint: %s", url)
177
+
178
+ endpoint_url = urljoin(self._client.base_url, "webhooks/endpoints")
179
+ headers = self._prepare_headers(extra_headers)
180
+
181
+ request = WebhookEndpointCreate(
182
+ url=url,
183
+ event_filters=event_filters,
184
+ ssl_verification=ssl_verification,
185
+ secret_key=secret_key,
186
+ )
187
+ json_data = request.model_dump(exclude_none=True)
188
+
189
+ response_data = self._make_request("POST", endpoint_url, headers, json_data)
190
+ if not isinstance(response_data, dict):
191
+ raise TypeError("Expected dict response from API")
192
+ return WebhookEndpoint(**response_data)
193
+
194
+ def get_endpoint(
195
+ self,
196
+ endpoint_id: str,
197
+ extra_headers: dict[str, str] | None = None,
198
+ ) -> WebhookEndpoint:
199
+ """Get details of a specific webhook endpoint.
200
+
201
+ Args:
202
+ endpoint_id (str): Unique identifier of the endpoint
203
+ extra_headers (dict[str, str] | None): Additional headers
204
+
205
+ Returns:
206
+ WebhookEndpoint: Webhook endpoint details with secrets
207
+
208
+ Raises:
209
+ ValueError: If endpoint_id is empty
210
+ httpx.HTTPStatusError: If the API request fails
211
+ """
212
+ if not endpoint_id:
213
+ raise ValueError("endpoint_id cannot be empty")
214
+
215
+ logger.debug("Getting webhook endpoint: %s", endpoint_id)
216
+
217
+ url = urljoin(self._client.base_url, f"webhooks/endpoints/{endpoint_id}")
218
+ headers = self._prepare_headers(extra_headers)
219
+
220
+ response_data = self._make_request("GET", url, headers)
221
+ if not isinstance(response_data, dict):
222
+ raise TypeError("Expected dict response from API")
223
+ return WebhookEndpoint(**response_data)
224
+
225
+ def update_endpoint(
226
+ self,
227
+ endpoint_id: str,
228
+ url: str | None = None,
229
+ event_filters: list[str] | None = None,
230
+ ssl_verification: bool | None = True,
231
+ extra_headers: dict[str, str] | None = None,
232
+ ) -> WebhookEndpoint:
233
+ """Update an existing webhook endpoint.
234
+
235
+ Args:
236
+ endpoint_id (str): Unique identifier of the endpoint
237
+ url (str | None): Callback URL for webhook events
238
+ event_filters (list[str] | None): Event types to filter. Empty = all events.
239
+ ssl_verification (bool | None): Verify SSL certificate. Default: True.
240
+ extra_headers (dict[str, str] | None): Additional headers
241
+
242
+ Returns:
243
+ WebhookEndpoint: Updated webhook endpoint
244
+
245
+ Raises:
246
+ ValueError: If endpoint_id is empty
247
+ httpx.HTTPStatusError: If the API request fails
248
+ """
249
+ if not endpoint_id:
250
+ raise ValueError("endpoint_id cannot be empty")
251
+
252
+ logger.debug("Updating webhook endpoint: %s", endpoint_id)
253
+
254
+ endpoint_url = urljoin(self._client.base_url, f"webhooks/endpoints/{endpoint_id}")
255
+ headers = self._prepare_headers(extra_headers)
256
+
257
+ request = WebhookEndpointUpdate(
258
+ url=url,
259
+ event_filters=event_filters,
260
+ ssl_verification=ssl_verification,
261
+ )
262
+ json_data = request.model_dump(exclude_none=True)
263
+
264
+ response_data = self._make_request("PUT", endpoint_url, headers, json_data)
265
+ if not isinstance(response_data, dict):
266
+ raise TypeError("Expected dict response from API")
267
+ return WebhookEndpoint(**response_data)
268
+
269
+ def delete_endpoint(
270
+ self,
271
+ endpoint_id: str,
272
+ extra_headers: dict[str, str] | None = None,
273
+ ) -> WebhookEndpoint:
274
+ """Delete a webhook endpoint.
275
+
276
+ Args:
277
+ endpoint_id (str): Unique identifier of the endpoint
278
+ extra_headers (dict[str, str] | None): Additional headers
279
+
280
+ Returns:
281
+ WebhookEndpoint: Deleted webhook endpoint details
282
+
283
+ Raises:
284
+ ValueError: If endpoint_id is empty
285
+ httpx.HTTPStatusError: If the API request fails
286
+ """
287
+ if not endpoint_id:
288
+ raise ValueError("endpoint_id cannot be empty")
289
+
290
+ logger.debug("Deleting webhook endpoint: %s", endpoint_id)
291
+
292
+ url = urljoin(self._client.base_url, f"webhooks/endpoints/{endpoint_id}")
293
+ headers = self._prepare_headers(extra_headers)
294
+
295
+ response_data = self._make_request("DELETE", url, headers)
296
+ if not response_data:
297
+ # Return dummy but required fields if response is empty
298
+ return WebhookEndpoint(id=endpoint_id, url="", ssl_verification=True)
299
+ if not isinstance(response_data, dict):
300
+ raise TypeError("Expected dict response from API")
301
+ return WebhookEndpoint(**response_data)
302
+
303
+ def rotate_secret(
304
+ self,
305
+ endpoint_id: str,
306
+ days: int | None = 3,
307
+ hours: int | None = 0,
308
+ extra_headers: dict[str, str] | None = None,
309
+ ) -> WebhookEndpoint:
310
+ """Rotate the secret key for a webhook endpoint.
311
+
312
+ Args:
313
+ endpoint_id (str): Unique identifier of the endpoint
314
+ days (int | None): Days old secret remains valid. Default: 3.
315
+ hours (int | None): Hours old secret remains valid. Default: 0.
316
+ extra_headers (dict[str, str] | None): Additional headers
317
+
318
+ Returns:
319
+ WebhookEndpoint: Webhook endpoint with new and expiring secrets
320
+
321
+ Raises:
322
+ ValueError: If endpoint_id is empty
323
+ httpx.HTTPStatusError: If the API request fails
324
+ """
325
+ if not endpoint_id:
326
+ raise ValueError("endpoint_id cannot be empty")
327
+
328
+ logger.debug("Rotating secret for webhook endpoint: %s", endpoint_id)
329
+
330
+ url = urljoin(self._client.base_url, f"webhooks/endpoints/{endpoint_id}/secret")
331
+ headers = self._prepare_headers(extra_headers)
332
+
333
+ period = WebhookRotationPeriod(days=days, hours=hours)
334
+ request = WebhookRotation(rotation_period=period)
335
+ json_data = request.model_dump(exclude_none=True)
336
+
337
+ response_data = self._make_request("POST", url, headers, json_data)
338
+ if not isinstance(response_data, dict):
339
+ raise TypeError("Expected dict response from API")
340
+ return WebhookEndpoint(**response_data)
341
+
342
+ # =========================================================================
343
+ # Event Handling
344
+ # =========================================================================
345
+
346
+ def list_events(
347
+ self,
348
+ from_date: str | None = None,
349
+ until_date: str | None = None,
350
+ extra_headers: dict[str, str] | None = None,
351
+ ) -> list[WebhookEventListing]:
352
+ """List webhook events.
353
+
354
+ Args:
355
+ from_date (str | None): Filter events from this date (YYYY-MM-DD)
356
+ until_date (str | None): Filter events until this date (YYYY-MM-DD)
357
+ extra_headers (dict[str, str] | None): Additional headers
358
+
359
+ Returns:
360
+ list[WebhookEventListing]: List of webhook events
361
+
362
+ Raises:
363
+ httpx.HTTPStatusError: If the API request fails
364
+ """
365
+ logger.debug("Listing webhook events")
366
+
367
+ url = urljoin(self._client.base_url, "webhooks/events")
368
+ headers = self._prepare_headers(extra_headers)
369
+
370
+ params: dict[str, Any] = {}
371
+ if from_date is not None:
372
+ params["from_date"] = from_date
373
+ if until_date is not None:
374
+ params["until_date"] = until_date
375
+
376
+ response_data = self._make_request("GET", url, headers, params=params)
377
+ if not isinstance(response_data, list):
378
+ return []
379
+ return [WebhookEventListing(**item) for item in response_data]
380
+
381
+ def get_event(
382
+ self,
383
+ event_id: str,
384
+ extra_headers: dict[str, str] | None = None,
385
+ ) -> WebhookEvent:
386
+ """Get details of a specific webhook event.
387
+
388
+ Args:
389
+ event_id (str): Unique identifier of the event
390
+ extra_headers (dict[str, str] | None): Additional headers
391
+
392
+ Returns:
393
+ WebhookEvent: Webhook event details with data payload
394
+
395
+ Raises:
396
+ ValueError: If event_id is empty
397
+ httpx.HTTPStatusError: If the API request fails
398
+ """
399
+ if not event_id:
400
+ raise ValueError("event_id cannot be empty")
401
+
402
+ logger.debug("Getting webhook event: %s", event_id)
403
+
404
+ url = urljoin(self._client.base_url, f"webhooks/events/{event_id}")
405
+ headers = self._prepare_headers(extra_headers)
406
+
407
+ response_data = self._make_request("GET", url, headers)
408
+ if not isinstance(response_data, dict):
409
+ raise TypeError("Expected dict response from API")
410
+ return WebhookEvent(**response_data)
411
+
412
+ def test_endpoint(
413
+ self,
414
+ endpoint_id: str,
415
+ extra_headers: dict[str, str] | None = None,
416
+ ) -> DeliveryTicket:
417
+ """Trigger a test event for a webhook endpoint.
418
+
419
+ Args:
420
+ endpoint_id (str): Unique identifier of the endpoint
421
+ extra_headers (dict[str, str] | None): Additional headers
422
+
423
+ Returns:
424
+ DeliveryTicket: Test event delivery ticket
425
+
426
+ Raises:
427
+ ValueError: If endpoint_id is empty
428
+ httpx.HTTPStatusError: If the API request fails
429
+ """
430
+ if not endpoint_id:
431
+ raise ValueError("endpoint_id cannot be empty")
432
+
433
+ logger.debug("Testing webhook endpoint: %s", endpoint_id)
434
+
435
+ url = urljoin(self._client.base_url, f"webhooks/endpoints/{endpoint_id}/test")
436
+ headers = self._prepare_headers(extra_headers)
437
+
438
+ response_data = self._make_request("POST", url, headers)
439
+ if not isinstance(response_data, dict):
440
+ raise TypeError("Expected dict response from API")
441
+ return DeliveryTicket(**response_data)
442
+
443
+ # =========================================================================
444
+ # Delivery Management
445
+ # =========================================================================
446
+
447
+ def list_deliveries(
448
+ self,
449
+ endpoint_id: str,
450
+ from_date: str | None = None,
451
+ until_date: str | None = None,
452
+ extra_headers: dict[str, str] | None = None,
453
+ ) -> list[WebhookDelivery]:
454
+ """List webhook deliveries for an endpoint.
455
+
456
+ Args:
457
+ endpoint_id (str): Unique identifier of the endpoint
458
+ from_date (str | None): Filter deliveries from this date (YYYY-MM-DD)
459
+ until_date (str | None): Filter deliveries until this date (YYYY-MM-DD)
460
+ extra_headers (dict[str, str] | None): Additional headers
461
+
462
+ Returns:
463
+ list[WebhookDelivery]: List of webhook deliveries
464
+
465
+ Raises:
466
+ ValueError: If endpoint_id is empty
467
+ httpx.HTTPStatusError: If the API request fails
468
+ """
469
+ if not endpoint_id:
470
+ raise ValueError("endpoint_id cannot be empty")
471
+
472
+ logger.debug("Listing webhook deliveries for endpoint: %s", endpoint_id)
473
+
474
+ url = urljoin(self._client.base_url, f"webhooks/endpoints/{endpoint_id}/deliveries")
475
+ headers = self._prepare_headers(extra_headers)
476
+
477
+ params: dict[str, Any] = {}
478
+ if from_date is not None:
479
+ params["from_date"] = from_date
480
+ if until_date is not None:
481
+ params["until_date"] = until_date
482
+
483
+ response_data = self._make_request("GET", url, headers, params=params)
484
+ if not isinstance(response_data, list):
485
+ return []
486
+ return [WebhookDelivery(**item) for item in response_data]
487
+
488
+ def replay_delivery(
489
+ self,
490
+ delivery_id: str,
491
+ extra_headers: dict[str, str] | None = None,
492
+ ) -> DeliveryTicket:
493
+ """Replay a specific webhook delivery.
494
+
495
+ Args:
496
+ delivery_id (str): Unique identifier of the delivery
497
+ extra_headers (dict[str, str] | None): Additional headers
498
+
499
+ Returns:
500
+ DeliveryTicket: Delivery ticket for the replayed event
501
+
502
+ Raises:
503
+ ValueError: If delivery_id is empty
504
+ httpx.HTTPStatusError: If the API request fails
505
+ """
506
+ if not delivery_id:
507
+ raise ValueError("delivery_id cannot be empty")
508
+
509
+ logger.debug("Replaying webhook delivery: %s", delivery_id)
510
+
511
+ url = urljoin(self._client.base_url, f"webhooks/deliveries/{delivery_id}/replay")
512
+ headers = self._prepare_headers(extra_headers)
513
+
514
+ response_data = self._make_request("POST", url, headers)
515
+ if not isinstance(response_data, dict):
516
+ raise TypeError("Expected dict response from API")
517
+ return DeliveryTicket(**response_data)
518
+
519
+ def replay_failed_deliveries(
520
+ self,
521
+ endpoint_id: str,
522
+ extra_headers: dict[str, str] | None = None,
523
+ ) -> list[DeliveryTicket]:
524
+ """Replay all failed deliveries for an endpoint.
525
+
526
+ Args:
527
+ endpoint_id (str): Unique identifier of the endpoint
528
+ extra_headers (dict[str, str] | None): Additional headers
529
+
530
+ Returns:
531
+ list[DeliveryTicket]: List of new delivery tickets
532
+
533
+ Raises:
534
+ ValueError: If endpoint_id is empty
535
+ httpx.HTTPStatusError: If the API request fails
536
+ """
537
+ if not endpoint_id:
538
+ raise ValueError("endpoint_id cannot be empty")
539
+
540
+ logger.debug("Replaying failed deliveries for endpoint: %s", endpoint_id)
541
+
542
+ url = urljoin(
543
+ self._client.base_url,
544
+ f"webhooks/endpoints/{endpoint_id}/replay-failed-deliveries"
545
+ )
546
+ headers = self._prepare_headers(extra_headers)
547
+
548
+ response_data = self._make_request("POST", url, headers)
549
+ if not isinstance(response_data, list):
550
+ return []
551
+ return [DeliveryTicket(**item) for item in response_data]