transcodely 0.2.0__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.
- buf/__init__.py +0 -0
- buf/validate/__init__.py +0 -0
- buf/validate/validate_pb2.py +469 -0
- buf/validate/validate_pb2.pyi +651 -0
- transcodely/__init__.py +175 -0
- transcodely/_codec/__init__.py +0 -0
- transcodely/_codec/json_codec.py +193 -0
- transcodely/_transport/__init__.py +0 -0
- transcodely/_transport/error_mapping.py +102 -0
- transcodely/_transport/headers.py +31 -0
- transcodely/_transport/transport.py +312 -0
- transcodely/client.py +160 -0
- transcodely/errors.py +115 -0
- transcodely/pagination.py +53 -0
- transcodely/py.typed +0 -0
- transcodely/resources/__init__.py +0 -0
- transcodely/resources/_helpers.py +29 -0
- transcodely/resources/api_keys.py +48 -0
- transcodely/resources/apps.py +59 -0
- transcodely/resources/events.py +118 -0
- transcodely/resources/health.py +19 -0
- transcodely/resources/jobs.py +120 -0
- transcodely/resources/memberships.py +54 -0
- transcodely/resources/organizations.py +73 -0
- transcodely/resources/origins.py +55 -0
- transcodely/resources/presets.py +63 -0
- transcodely/resources/users.py +46 -0
- transcodely/resources/videos.py +114 -0
- transcodely/resources/webhook_endpoints.py +242 -0
- transcodely/streaming.py +47 -0
- transcodely/types.py +570 -0
- transcodely/v1/api_key_pb2.py +69 -0
- transcodely/v1/api_key_pb2.pyi +97 -0
- transcodely/v1/app_pb2.py +107 -0
- transcodely/v1/app_pb2.pyi +219 -0
- transcodely/v1/codec_av1_pb2.py +55 -0
- transcodely/v1/codec_av1_pb2.pyi +34 -0
- transcodely/v1/codec_h264_pb2.py +71 -0
- transcodely/v1/codec_h264_pb2.pyi +44 -0
- transcodely/v1/codec_h265_pb2.py +69 -0
- transcodely/v1/codec_h265_pb2.pyi +42 -0
- transcodely/v1/codec_vp9_pb2.py +57 -0
- transcodely/v1/codec_vp9_pb2.pyi +30 -0
- transcodely/v1/common_pb2.py +67 -0
- transcodely/v1/common_pb2.pyi +171 -0
- transcodely/v1/content_aware_pb2.py +56 -0
- transcodely/v1/content_aware_pb2.pyi +51 -0
- transcodely/v1/drm_pb2.py +65 -0
- transcodely/v1/drm_pb2.pyi +68 -0
- transcodely/v1/hdr_pb2.py +57 -0
- transcodely/v1/hdr_pb2.pyi +64 -0
- transcodely/v1/health_pb2.py +44 -0
- transcodely/v1/health_pb2.pyi +44 -0
- transcodely/v1/job_pb2.py +201 -0
- transcodely/v1/job_pb2.pyi +546 -0
- transcodely/v1/media_pb2.py +43 -0
- transcodely/v1/media_pb2.pyi +100 -0
- transcodely/v1/membership_pb2.py +71 -0
- transcodely/v1/membership_pb2.pyi +122 -0
- transcodely/v1/organization_pb2.py +81 -0
- transcodely/v1/organization_pb2.pyi +121 -0
- transcodely/v1/origin_pb2.py +157 -0
- transcodely/v1/origin_pb2.pyi +317 -0
- transcodely/v1/preset_pb2.py +192 -0
- transcodely/v1/preset_pb2.pyi +290 -0
- transcodely/v1/streaming_pb2.py +59 -0
- transcodely/v1/streaming_pb2.pyi +56 -0
- transcodely/v1/subtitles_pb2.py +69 -0
- transcodely/v1/subtitles_pb2.pyi +75 -0
- transcodely/v1/thumbnails_pb2.py +63 -0
- transcodely/v1/thumbnails_pb2.pyi +74 -0
- transcodely/v1/user_pb2.py +72 -0
- transcodely/v1/user_pb2.pyi +137 -0
- transcodely/v1/video_pb2.py +166 -0
- transcodely/v1/video_pb2.pyi +367 -0
- transcodely/v1/webhook_pb2.py +151 -0
- transcodely/v1/webhook_pb2.pyi +336 -0
- transcodely/version.py +11 -0
- transcodely/webhooks/__init__.py +44 -0
- transcodely/webhooks/construct_event.py +111 -0
- transcodely/webhooks/signature.py +110 -0
- transcodely/webhooks/types.py +115 -0
- transcodely-0.2.0.dist-info/METADATA +349 -0
- transcodely-0.2.0.dist-info/RECORD +86 -0
- transcodely-0.2.0.dist-info/WHEEL +4 -0
- transcodely-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Video resource — uploads, get/list/update/delete, watch, usage."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Generator, Optional
|
|
6
|
+
|
|
7
|
+
from .._transport.transport import CallOptions, Transport
|
|
8
|
+
from ..pagination import Page, PageContents
|
|
9
|
+
from ..streaming import watch
|
|
10
|
+
from ..v1 import video_pb2
|
|
11
|
+
from ._helpers import fill_from_dict
|
|
12
|
+
|
|
13
|
+
_SERVICE = "transcodely.v1.VideoService"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Videos:
|
|
17
|
+
def __init__(self, transport: Transport) -> None:
|
|
18
|
+
self._t = transport
|
|
19
|
+
|
|
20
|
+
def create_upload(self, **kwargs: Any) -> video_pb2.CreateUploadResponse:
|
|
21
|
+
req = fill_from_dict(video_pb2.CreateUploadRequest(), kwargs)
|
|
22
|
+
return self._t.unary(_SERVICE, "CreateUpload", req, video_pb2.CreateUploadResponse())
|
|
23
|
+
|
|
24
|
+
def complete_upload(self, **kwargs: Any) -> video_pb2.CompleteUploadResponse:
|
|
25
|
+
req = fill_from_dict(video_pb2.CompleteUploadRequest(), kwargs)
|
|
26
|
+
return self._t.unary(_SERVICE, "CompleteUpload", req, video_pb2.CompleteUploadResponse())
|
|
27
|
+
|
|
28
|
+
def create_multipart_upload(self, **kwargs: Any) -> video_pb2.CreateMultipartUploadResponse:
|
|
29
|
+
req = fill_from_dict(video_pb2.CreateMultipartUploadRequest(), kwargs)
|
|
30
|
+
return self._t.unary(
|
|
31
|
+
_SERVICE,
|
|
32
|
+
"CreateMultipartUpload",
|
|
33
|
+
req,
|
|
34
|
+
video_pb2.CreateMultipartUploadResponse(),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def get_upload_part_urls(self, **kwargs: Any) -> video_pb2.GetUploadPartUrlsResponse:
|
|
38
|
+
req = fill_from_dict(video_pb2.GetUploadPartUrlsRequest(), kwargs)
|
|
39
|
+
return self._t.unary(
|
|
40
|
+
_SERVICE, "GetUploadPartUrls", req, video_pb2.GetUploadPartUrlsResponse()
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def complete_multipart_upload(self, **kwargs: Any) -> video_pb2.CompleteMultipartUploadResponse:
|
|
44
|
+
req = fill_from_dict(video_pb2.CompleteMultipartUploadRequest(), kwargs)
|
|
45
|
+
return self._t.unary(
|
|
46
|
+
_SERVICE,
|
|
47
|
+
"CompleteMultipartUpload",
|
|
48
|
+
req,
|
|
49
|
+
video_pb2.CompleteMultipartUploadResponse(),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def abort_multipart_upload(self, **kwargs: Any) -> video_pb2.AbortMultipartUploadResponse:
|
|
53
|
+
req = fill_from_dict(video_pb2.AbortMultipartUploadRequest(), kwargs)
|
|
54
|
+
return self._t.unary(
|
|
55
|
+
_SERVICE,
|
|
56
|
+
"AbortMultipartUpload",
|
|
57
|
+
req,
|
|
58
|
+
video_pb2.AbortMultipartUploadResponse(),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def get(self, id: str, opts: Optional[CallOptions] = None) -> video_pb2.Video:
|
|
62
|
+
req = video_pb2.GetVideoRequest(id=id)
|
|
63
|
+
return self._t.unary(_SERVICE, "Get", req, video_pb2.GetVideoResponse(), opts).video
|
|
64
|
+
|
|
65
|
+
def list(
|
|
66
|
+
self,
|
|
67
|
+
*,
|
|
68
|
+
page_size: Optional[int] = None,
|
|
69
|
+
status: Optional[str] = None,
|
|
70
|
+
opts: Optional[CallOptions] = None,
|
|
71
|
+
) -> Page[video_pb2.Video]:
|
|
72
|
+
def fetch(cursor: Optional[str]) -> PageContents[video_pb2.Video]:
|
|
73
|
+
req = video_pb2.ListVideosRequest()
|
|
74
|
+
if page_size is not None:
|
|
75
|
+
req.page_size = page_size
|
|
76
|
+
if status is not None:
|
|
77
|
+
req.status = status
|
|
78
|
+
if cursor is not None:
|
|
79
|
+
req.page_token = cursor
|
|
80
|
+
res = self._t.unary(_SERVICE, "List", req, video_pb2.ListVideosResponse(), opts)
|
|
81
|
+
return PageContents(items=list(res.videos), next_cursor=res.next_page_token or None)
|
|
82
|
+
|
|
83
|
+
return Page(fetch)
|
|
84
|
+
|
|
85
|
+
def update(self, **kwargs: Any) -> video_pb2.Video:
|
|
86
|
+
req = fill_from_dict(video_pb2.UpdateVideoRequest(), kwargs)
|
|
87
|
+
return self._t.unary(_SERVICE, "Update", req, video_pb2.UpdateVideoResponse()).video
|
|
88
|
+
|
|
89
|
+
def delete(self, id: str, opts: Optional[CallOptions] = None) -> None:
|
|
90
|
+
req = video_pb2.DeleteVideoRequest(id=id)
|
|
91
|
+
self._t.unary(_SERVICE, "Delete", req, video_pb2.DeleteVideoResponse(), opts)
|
|
92
|
+
|
|
93
|
+
def watch(
|
|
94
|
+
self,
|
|
95
|
+
id: str,
|
|
96
|
+
*,
|
|
97
|
+
include_heartbeats: bool = False,
|
|
98
|
+
max_reconnects: int = 5,
|
|
99
|
+
opts: Optional[CallOptions] = None,
|
|
100
|
+
) -> Generator[video_pb2.WatchVideoResponse, None, None]:
|
|
101
|
+
def factory() -> Generator[video_pb2.WatchVideoResponse, None, None]:
|
|
102
|
+
req = video_pb2.WatchVideoRequest(id=id)
|
|
103
|
+
yield from self._t.stream(_SERVICE, "Watch", req, video_pb2.WatchVideoResponse, opts)
|
|
104
|
+
|
|
105
|
+
yield from watch(
|
|
106
|
+
factory,
|
|
107
|
+
is_heartbeat=lambda e: getattr(e, "event", "") == "heartbeat",
|
|
108
|
+
include_heartbeats=include_heartbeats,
|
|
109
|
+
max_reconnects=max_reconnects,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def get_usage(self, **kwargs: Any) -> video_pb2.GetUsageResponse:
|
|
113
|
+
req = fill_from_dict(video_pb2.GetUsageRequest(), kwargs)
|
|
114
|
+
return self._t.unary(_SERVICE, "GetUsage", req, video_pb2.GetUsageResponse())
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Webhook endpoint resource — manage signed endpoints, test deliveries, and health.
|
|
2
|
+
|
|
3
|
+
Stripe parity: method names match the resource verbs (``create``, ``retrieve``,
|
|
4
|
+
``update``, ``delete``, ``list``) without re-stating the "Webhook" prefix already
|
|
5
|
+
implied by ``client.webhook_endpoints``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Literal, Mapping, Optional
|
|
11
|
+
|
|
12
|
+
from .._transport.transport import CallOptions, Transport
|
|
13
|
+
from ..pagination import Page, PageContents
|
|
14
|
+
from ..v1 import common_pb2, webhook_pb2
|
|
15
|
+
|
|
16
|
+
_SERVICE = "transcodely.v1.WebhookService"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class WebhookEndpoints:
|
|
20
|
+
def __init__(self, transport: Transport) -> None:
|
|
21
|
+
self._t = transport
|
|
22
|
+
|
|
23
|
+
def create(
|
|
24
|
+
self,
|
|
25
|
+
*,
|
|
26
|
+
app_id: Optional[str] = None,
|
|
27
|
+
url: Optional[str] = None,
|
|
28
|
+
enabled_events: Optional[list[str]] = None,
|
|
29
|
+
description: Optional[str] = None,
|
|
30
|
+
api_version: Optional[str] = None,
|
|
31
|
+
metadata: Optional[Mapping[str, str]] = None,
|
|
32
|
+
request: Optional[webhook_pb2.CreateWebhookEndpointRequest] = None,
|
|
33
|
+
opts: Optional[CallOptions] = None,
|
|
34
|
+
) -> webhook_pb2.WebhookEndpoint:
|
|
35
|
+
"""Create a webhook endpoint.
|
|
36
|
+
|
|
37
|
+
The signing secret is populated on the returned endpoint **only here**
|
|
38
|
+
(and on :meth:`rotate_secret`) — store it securely; it cannot be
|
|
39
|
+
retrieved again.
|
|
40
|
+
"""
|
|
41
|
+
if request is None:
|
|
42
|
+
req = webhook_pb2.CreateWebhookEndpointRequest()
|
|
43
|
+
if app_id is not None:
|
|
44
|
+
req.app_id = app_id
|
|
45
|
+
if url is not None:
|
|
46
|
+
req.url = url
|
|
47
|
+
if enabled_events:
|
|
48
|
+
req.enabled_events.extend(enabled_events)
|
|
49
|
+
if description is not None:
|
|
50
|
+
req.description = description
|
|
51
|
+
if api_version is not None:
|
|
52
|
+
req.api_version = api_version
|
|
53
|
+
if metadata:
|
|
54
|
+
for k, v in metadata.items():
|
|
55
|
+
req.metadata[k] = v
|
|
56
|
+
else:
|
|
57
|
+
req = request
|
|
58
|
+
res = self._t.unary(
|
|
59
|
+
_SERVICE,
|
|
60
|
+
"CreateWebhookEndpoint",
|
|
61
|
+
req,
|
|
62
|
+
webhook_pb2.CreateWebhookEndpointResponse(),
|
|
63
|
+
opts,
|
|
64
|
+
)
|
|
65
|
+
return res.endpoint
|
|
66
|
+
|
|
67
|
+
def retrieve(self, id: str, opts: Optional[CallOptions] = None) -> webhook_pb2.WebhookEndpoint:
|
|
68
|
+
req = webhook_pb2.RetrieveWebhookEndpointRequest(id=id)
|
|
69
|
+
res = self._t.unary(
|
|
70
|
+
_SERVICE,
|
|
71
|
+
"RetrieveWebhookEndpoint",
|
|
72
|
+
req,
|
|
73
|
+
webhook_pb2.RetrieveWebhookEndpointResponse(),
|
|
74
|
+
opts,
|
|
75
|
+
)
|
|
76
|
+
return res.endpoint
|
|
77
|
+
|
|
78
|
+
def update(
|
|
79
|
+
self,
|
|
80
|
+
id: str,
|
|
81
|
+
*,
|
|
82
|
+
url: Optional[str] = None,
|
|
83
|
+
description: Optional[str] = None,
|
|
84
|
+
enabled_events: Optional[list[str]] = None,
|
|
85
|
+
status: Optional[str] = None,
|
|
86
|
+
metadata: Optional[Mapping[str, str]] = None,
|
|
87
|
+
request: Optional[webhook_pb2.UpdateWebhookEndpointRequest] = None,
|
|
88
|
+
opts: Optional[CallOptions] = None,
|
|
89
|
+
) -> webhook_pb2.WebhookEndpoint:
|
|
90
|
+
"""Update an endpoint. Only provided fields are changed; omit the rest."""
|
|
91
|
+
if request is None:
|
|
92
|
+
req = webhook_pb2.UpdateWebhookEndpointRequest(id=id)
|
|
93
|
+
if url is not None:
|
|
94
|
+
req.url = url
|
|
95
|
+
if description is not None:
|
|
96
|
+
req.description = description
|
|
97
|
+
if enabled_events is not None:
|
|
98
|
+
req.enabled_events.extend(enabled_events)
|
|
99
|
+
if status is not None:
|
|
100
|
+
req.status = status
|
|
101
|
+
if metadata is not None:
|
|
102
|
+
for k, v in metadata.items():
|
|
103
|
+
req.metadata[k] = v
|
|
104
|
+
else:
|
|
105
|
+
req = request
|
|
106
|
+
res = self._t.unary(
|
|
107
|
+
_SERVICE,
|
|
108
|
+
"UpdateWebhookEndpoint",
|
|
109
|
+
req,
|
|
110
|
+
webhook_pb2.UpdateWebhookEndpointResponse(),
|
|
111
|
+
opts,
|
|
112
|
+
)
|
|
113
|
+
return res.endpoint
|
|
114
|
+
|
|
115
|
+
def delete(self, id: str, opts: Optional[CallOptions] = None) -> None:
|
|
116
|
+
req = webhook_pb2.DeleteWebhookEndpointRequest(id=id)
|
|
117
|
+
self._t.unary(
|
|
118
|
+
_SERVICE,
|
|
119
|
+
"DeleteWebhookEndpoint",
|
|
120
|
+
req,
|
|
121
|
+
webhook_pb2.DeleteWebhookEndpointResponse(),
|
|
122
|
+
opts,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def list(
|
|
126
|
+
self,
|
|
127
|
+
*,
|
|
128
|
+
app_id: str,
|
|
129
|
+
limit: Optional[int] = None,
|
|
130
|
+
opts: Optional[CallOptions] = None,
|
|
131
|
+
) -> Page[webhook_pb2.WebhookEndpoint]:
|
|
132
|
+
def fetch(cursor: Optional[str]) -> PageContents[webhook_pb2.WebhookEndpoint]:
|
|
133
|
+
req = webhook_pb2.ListWebhookEndpointsRequest(app_id=app_id)
|
|
134
|
+
pagination = common_pb2.PaginationRequest()
|
|
135
|
+
if limit is not None:
|
|
136
|
+
pagination.limit = limit
|
|
137
|
+
if cursor is not None:
|
|
138
|
+
pagination.cursor = cursor
|
|
139
|
+
req.pagination.CopyFrom(pagination)
|
|
140
|
+
res = self._t.unary(
|
|
141
|
+
_SERVICE,
|
|
142
|
+
"ListWebhookEndpoints",
|
|
143
|
+
req,
|
|
144
|
+
webhook_pb2.ListWebhookEndpointsResponse(),
|
|
145
|
+
opts,
|
|
146
|
+
)
|
|
147
|
+
return PageContents(
|
|
148
|
+
items=list(res.endpoints),
|
|
149
|
+
next_cursor=res.pagination.next_cursor or None,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return Page(fetch)
|
|
153
|
+
|
|
154
|
+
def rotate_secret(
|
|
155
|
+
self, id: str, opts: Optional[CallOptions] = None
|
|
156
|
+
) -> webhook_pb2.WebhookEndpoint:
|
|
157
|
+
"""Rotate the signing secret.
|
|
158
|
+
|
|
159
|
+
The previous secret stays valid for 24 h to drain in-flight deliveries.
|
|
160
|
+
The returned endpoint includes the new plain-text ``secret`` — the only
|
|
161
|
+
response besides :meth:`create` that exposes it.
|
|
162
|
+
"""
|
|
163
|
+
req = webhook_pb2.RotateWebhookSecretRequest(id=id)
|
|
164
|
+
res = self._t.unary(
|
|
165
|
+
_SERVICE,
|
|
166
|
+
"RotateWebhookSecret",
|
|
167
|
+
req,
|
|
168
|
+
webhook_pb2.RotateWebhookSecretResponse(),
|
|
169
|
+
opts,
|
|
170
|
+
)
|
|
171
|
+
return res.endpoint
|
|
172
|
+
|
|
173
|
+
def send_test(
|
|
174
|
+
self, endpoint_id: str, event_type: str, opts: Optional[CallOptions] = None
|
|
175
|
+
) -> webhook_pb2.WebhookDelivery:
|
|
176
|
+
"""Send a synthetic test event to a single endpoint.
|
|
177
|
+
|
|
178
|
+
The synthetic event is invisible to ``client.events.list`` and never
|
|
179
|
+
bumps ``pending_webhooks`` on any other event. ``event_type`` must be a
|
|
180
|
+
concrete type (the ``"*"`` wildcard is rejected server-side).
|
|
181
|
+
Rate-limited to 10/min per endpoint.
|
|
182
|
+
"""
|
|
183
|
+
req = webhook_pb2.SendTestWebhookRequest(endpoint_id=endpoint_id, event_type=event_type)
|
|
184
|
+
res = self._t.unary(
|
|
185
|
+
_SERVICE, "SendTestWebhook", req, webhook_pb2.SendTestWebhookResponse(), opts
|
|
186
|
+
)
|
|
187
|
+
return res.delivery
|
|
188
|
+
|
|
189
|
+
def list_deliveries(
|
|
190
|
+
self,
|
|
191
|
+
*,
|
|
192
|
+
endpoint_id: Optional[str] = None,
|
|
193
|
+
event_id: Optional[str] = None,
|
|
194
|
+
status: Optional[str] = None,
|
|
195
|
+
limit: Optional[int] = None,
|
|
196
|
+
opts: Optional[CallOptions] = None,
|
|
197
|
+
) -> Page[webhook_pb2.WebhookDelivery]:
|
|
198
|
+
"""List delivery attempts. At least one of ``endpoint_id`` / ``event_id``
|
|
199
|
+
is required server-side."""
|
|
200
|
+
|
|
201
|
+
def fetch(cursor: Optional[str]) -> PageContents[webhook_pb2.WebhookDelivery]:
|
|
202
|
+
req = webhook_pb2.ListWebhookDeliveriesRequest()
|
|
203
|
+
if endpoint_id is not None:
|
|
204
|
+
req.endpoint_id = endpoint_id
|
|
205
|
+
if event_id is not None:
|
|
206
|
+
req.event_id = event_id
|
|
207
|
+
if status is not None:
|
|
208
|
+
req.status = status
|
|
209
|
+
pagination = common_pb2.PaginationRequest()
|
|
210
|
+
if limit is not None:
|
|
211
|
+
pagination.limit = limit
|
|
212
|
+
if cursor is not None:
|
|
213
|
+
pagination.cursor = cursor
|
|
214
|
+
req.pagination.CopyFrom(pagination)
|
|
215
|
+
res = self._t.unary(
|
|
216
|
+
_SERVICE,
|
|
217
|
+
"ListWebhookDeliveries",
|
|
218
|
+
req,
|
|
219
|
+
webhook_pb2.ListWebhookDeliveriesResponse(),
|
|
220
|
+
opts,
|
|
221
|
+
)
|
|
222
|
+
return PageContents(
|
|
223
|
+
items=list(res.deliveries),
|
|
224
|
+
next_cursor=res.pagination.next_cursor or None,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
return Page(fetch)
|
|
228
|
+
|
|
229
|
+
def get_health(
|
|
230
|
+
self,
|
|
231
|
+
endpoint_id: str,
|
|
232
|
+
window: Optional[Literal["24h", "7d", "30d"]] = None,
|
|
233
|
+
opts: Optional[CallOptions] = None,
|
|
234
|
+
) -> webhook_pb2.GetEndpointHealthResponse:
|
|
235
|
+
"""Aggregate delivery health for one endpoint over a rolling window.
|
|
236
|
+
Response is cached server-side for ~30 s."""
|
|
237
|
+
req = webhook_pb2.GetEndpointHealthRequest(endpoint_id=endpoint_id)
|
|
238
|
+
if window is not None:
|
|
239
|
+
req.window = window
|
|
240
|
+
return self._t.unary(
|
|
241
|
+
_SERVICE, "GetEndpointHealth", req, webhook_pb2.GetEndpointHealthResponse(), opts
|
|
242
|
+
)
|
transcodely/streaming.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Watch-stream wrapper with auto-reconnect and HEARTBEAT filtering."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import random
|
|
6
|
+
import time
|
|
7
|
+
from typing import Callable, Generator, Optional, TypeVar
|
|
8
|
+
|
|
9
|
+
from .errors import APIConnectionError
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def watch(
|
|
15
|
+
factory: Callable[[], Generator[T, None, None]],
|
|
16
|
+
*,
|
|
17
|
+
is_heartbeat: Optional[Callable[[T], bool]] = None,
|
|
18
|
+
include_heartbeats: bool = False,
|
|
19
|
+
max_reconnects: int = 5,
|
|
20
|
+
) -> Generator[T, None, None]:
|
|
21
|
+
"""Wrap a stream factory with reconnect + heartbeat-filter semantics.
|
|
22
|
+
|
|
23
|
+
``factory`` is called once per connection; if it raises an
|
|
24
|
+
:class:`APIConnectionError` we re-invoke it with backoff up to
|
|
25
|
+
``max_reconnects`` times. Watch RPCs are idempotent — every reconnect
|
|
26
|
+
starts with a SNAPSHOT event so resumption is safe.
|
|
27
|
+
"""
|
|
28
|
+
attempt = 0
|
|
29
|
+
while True:
|
|
30
|
+
try:
|
|
31
|
+
for event in factory():
|
|
32
|
+
if not include_heartbeats and is_heartbeat is not None and is_heartbeat(event):
|
|
33
|
+
continue
|
|
34
|
+
yield event
|
|
35
|
+
return
|
|
36
|
+
except APIConnectionError:
|
|
37
|
+
attempt += 1
|
|
38
|
+
if attempt > max_reconnects:
|
|
39
|
+
raise
|
|
40
|
+
_backoff(attempt)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _backoff(attempt: int) -> None:
|
|
44
|
+
base = 0.5
|
|
45
|
+
cap = 5.0
|
|
46
|
+
expo = min(cap, base * 2 ** (attempt - 1))
|
|
47
|
+
time.sleep(expo * (0.5 + random.random() * 0.5))
|