livekit-api 1.2.1__tar.gz → 1.2.3__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.
- {livekit_api-1.2.1 → livekit_api-1.2.3}/PKG-INFO +7 -4
- {livekit_api-1.2.1 → livekit_api-1.2.3}/livekit/api/__init__.py +8 -0
- {livekit_api-1.2.1 → livekit_api-1.2.3}/livekit/api/_failover.py +10 -4
- {livekit_api-1.2.1 → livekit_api-1.2.3}/livekit/api/access_token.py +94 -21
- {livekit_api-1.2.1 → livekit_api-1.2.3}/livekit/api/sip_service.py +4 -3
- {livekit_api-1.2.1 → livekit_api-1.2.3}/livekit/api/twirp_client.py +48 -19
- livekit_api-1.2.3/livekit/api/version.py +1 -0
- {livekit_api-1.2.1 → livekit_api-1.2.3}/livekit/api/webhook.py +1 -1
- {livekit_api-1.2.1 → livekit_api-1.2.3}/pyproject.toml +8 -3
- livekit_api-1.2.1/livekit/api/version.py +0 -1
- {livekit_api-1.2.1 → livekit_api-1.2.3}/.gitignore +0 -0
- {livekit_api-1.2.1 → livekit_api-1.2.3}/README.md +0 -0
- {livekit_api-1.2.1 → livekit_api-1.2.3}/livekit/api/_dial_timeout.py +0 -0
- {livekit_api-1.2.1 → livekit_api-1.2.3}/livekit/api/_service.py +0 -0
- {livekit_api-1.2.1 → livekit_api-1.2.3}/livekit/api/agent_dispatch_service.py +0 -0
- {livekit_api-1.2.1 → livekit_api-1.2.3}/livekit/api/connector_service.py +0 -0
- {livekit_api-1.2.1 → livekit_api-1.2.3}/livekit/api/egress_service.py +0 -0
- {livekit_api-1.2.1 → livekit_api-1.2.3}/livekit/api/ingress_service.py +0 -0
- {livekit_api-1.2.1 → livekit_api-1.2.3}/livekit/api/livekit_api.py +0 -0
- {livekit_api-1.2.1 → livekit_api-1.2.3}/livekit/api/py.typed +0 -0
- {livekit_api-1.2.1 → livekit_api-1.2.3}/livekit/api/room_service.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: livekit-api
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
4
4
|
Summary: Python Server API for LiveKit
|
|
5
5
|
Project-URL: Documentation, https://docs.livekit.io
|
|
6
6
|
Project-URL: Website, https://livekit.io/
|
|
@@ -21,10 +21,13 @@ Classifier: Topic :: Multimedia :: Sound/Audio
|
|
|
21
21
|
Classifier: Topic :: Multimedia :: Video
|
|
22
22
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
23
23
|
Requires-Python: >=3.9.0
|
|
24
|
-
Requires-Dist: aiohttp>=3.
|
|
25
|
-
Requires-Dist:
|
|
24
|
+
Requires-Dist: aiohttp>=3.13.5; python_version < '3.10'
|
|
25
|
+
Requires-Dist: aiohttp>=3.14.3; python_version >= '3.10'
|
|
26
|
+
Requires-Dist: idna>=3.15
|
|
27
|
+
Requires-Dist: livekit-protocol<2.0.0,>=1.1.32
|
|
28
|
+
Requires-Dist: multidict>=6.9.1; python_version >= '3.10'
|
|
26
29
|
Requires-Dist: protobuf>=4
|
|
27
|
-
Requires-Dist: pyjwt>=2.
|
|
30
|
+
Requires-Dist: pyjwt>=2.15.1
|
|
28
31
|
Requires-Dist: types-protobuf>=4
|
|
29
32
|
Description-Content-Type: text/markdown
|
|
30
33
|
|
|
@@ -47,6 +47,10 @@ from .livekit_api import LiveKitAPI
|
|
|
47
47
|
from .access_token import (
|
|
48
48
|
InferenceGrants,
|
|
49
49
|
ObservabilityGrants,
|
|
50
|
+
AgentGrants,
|
|
51
|
+
AgentEndpointGrants,
|
|
52
|
+
AgentThreadGrants,
|
|
53
|
+
AgentDatabaseGrants,
|
|
50
54
|
VideoGrants,
|
|
51
55
|
SIPGrants,
|
|
52
56
|
AccessToken,
|
|
@@ -65,6 +69,10 @@ __all__ = [
|
|
|
65
69
|
"connector_service",
|
|
66
70
|
"InferenceGrants",
|
|
67
71
|
"ObservabilityGrants",
|
|
72
|
+
"AgentGrants",
|
|
73
|
+
"AgentEndpointGrants",
|
|
74
|
+
"AgentThreadGrants",
|
|
75
|
+
"AgentDatabaseGrants",
|
|
68
76
|
"VideoGrants",
|
|
69
77
|
"SIPGrants",
|
|
70
78
|
"AccessToken",
|
|
@@ -44,11 +44,11 @@ def failover_attempts(
|
|
|
44
44
|
timeout: Optional[float] = None,
|
|
45
45
|
) -> int:
|
|
46
46
|
"""Total request attempts for a host; 1 means no failover. Failover only
|
|
47
|
-
engages when enabled, the host is a LiveKit Cloud
|
|
48
|
-
timeout is long enough to retry. ``force`` bypasses
|
|
49
|
-
is for internal testing only.
|
|
47
|
+
engages when enabled, the host is a LiveKit Cloud project or Cloud API
|
|
48
|
+
domain, and the request timeout is long enough to retry. ``force`` bypasses
|
|
49
|
+
the cloud-host check and is for internal testing only.
|
|
50
50
|
"""
|
|
51
|
-
if not (enabled and (force or (host is not None and is_cloud(host)))):
|
|
51
|
+
if not (enabled and (force or (host is not None and (is_cloud(host) or is_cloud_api(host))))):
|
|
52
52
|
return 1
|
|
53
53
|
if timeout is not None and 0 < timeout < MIN_FAILOVER_TIMEOUT:
|
|
54
54
|
return 1
|
|
@@ -60,6 +60,12 @@ def is_cloud(host: str) -> bool:
|
|
|
60
60
|
return host.endswith(".livekit.cloud")
|
|
61
61
|
|
|
62
62
|
|
|
63
|
+
def is_cloud_api(host: str) -> bool:
|
|
64
|
+
# cloud-api.livekit.io or a cloud-api.<env>.livekit.io variant; hostnames are case-insensitive.
|
|
65
|
+
host = host.lower()
|
|
66
|
+
return host.startswith("cloud-api.") and host.endswith(".livekit.io")
|
|
67
|
+
|
|
68
|
+
|
|
63
69
|
def to_http(url: str) -> str:
|
|
64
70
|
"""Normalizes a region URL to an http(s) scheme (ws -> http, wss -> https)."""
|
|
65
71
|
if url.startswith("ws"):
|
|
@@ -19,7 +19,7 @@ import os
|
|
|
19
19
|
import jwt
|
|
20
20
|
import dataclasses
|
|
21
21
|
from dataclasses import dataclass
|
|
22
|
-
from typing import Optional, List, Literal
|
|
22
|
+
from typing import Optional, List, Literal, Type, TypeVar
|
|
23
23
|
from google.protobuf.json_format import MessageToDict, ParseDict
|
|
24
24
|
|
|
25
25
|
from livekit.protocol.room import RoomConfiguration
|
|
@@ -71,6 +71,9 @@ class VideoGrants:
|
|
|
71
71
|
# allow participant to manage an agent session via RemoteSession
|
|
72
72
|
can_manage_agent_session: Optional[bool] = None
|
|
73
73
|
|
|
74
|
+
# allow participant to subscribe to metrics
|
|
75
|
+
can_subscribe_metrics: Optional[bool] = None
|
|
76
|
+
|
|
74
77
|
|
|
75
78
|
@dataclass
|
|
76
79
|
class SIPGrants:
|
|
@@ -92,6 +95,56 @@ class ObservabilityGrants:
|
|
|
92
95
|
write: bool = False
|
|
93
96
|
|
|
94
97
|
|
|
98
|
+
@dataclass
|
|
99
|
+
class AgentGrants:
|
|
100
|
+
# create/update/delete Cloud Agents
|
|
101
|
+
admin: bool = False
|
|
102
|
+
# manage simulations and scenarios for evaluating agents
|
|
103
|
+
simulation_admin: bool = False
|
|
104
|
+
# manage the project's agent dispatch queue
|
|
105
|
+
dispatch_admin: bool = False
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass
|
|
109
|
+
class AgentEndpointGrants:
|
|
110
|
+
# invoke an agent's non-public HTTP endpoints
|
|
111
|
+
call: bool = False
|
|
112
|
+
# restrict to one agent; empty grants every agent in the project
|
|
113
|
+
agent_name: str = ""
|
|
114
|
+
# restrict to one deployment; empty grants every deployment
|
|
115
|
+
deployment: str = ""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class AgentThreadGrants:
|
|
120
|
+
# every thread and scope of the project; the action flags still apply
|
|
121
|
+
all: bool = False
|
|
122
|
+
# scope in which threads may be listed and created
|
|
123
|
+
scope: str = ""
|
|
124
|
+
# threads that may be read, written and deleted
|
|
125
|
+
thread_ids: Optional[List[str]] = None
|
|
126
|
+
list: bool = False
|
|
127
|
+
create: bool = False
|
|
128
|
+
write: bool = False
|
|
129
|
+
read: bool = False
|
|
130
|
+
delete: bool = False
|
|
131
|
+
# open a covered thread's database, whose id is the thread's
|
|
132
|
+
sql: bool = False
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass
|
|
136
|
+
class AgentDatabaseGrants:
|
|
137
|
+
# databases that may be read, written and deleted
|
|
138
|
+
database_ids: Optional[List[str]] = None
|
|
139
|
+
read: bool = False
|
|
140
|
+
write: bool = False
|
|
141
|
+
delete: bool = False
|
|
142
|
+
# create databases in the project
|
|
143
|
+
create: bool = False
|
|
144
|
+
# list the project's databases
|
|
145
|
+
list: bool = False
|
|
146
|
+
|
|
147
|
+
|
|
95
148
|
@dataclass
|
|
96
149
|
class Claims:
|
|
97
150
|
identity: str = ""
|
|
@@ -102,6 +155,10 @@ class Claims:
|
|
|
102
155
|
sip: Optional[SIPGrants] = None
|
|
103
156
|
inference: Optional[InferenceGrants] = None
|
|
104
157
|
observability: Optional[ObservabilityGrants] = None
|
|
158
|
+
agent: Optional[AgentGrants] = None
|
|
159
|
+
agent_endpoint: Optional[AgentEndpointGrants] = None
|
|
160
|
+
agent_thread: Optional[AgentThreadGrants] = None
|
|
161
|
+
agent_database: Optional[AgentDatabaseGrants] = None
|
|
105
162
|
attributes: Optional[dict[str, str]] = None
|
|
106
163
|
sha256: Optional[str] = None
|
|
107
164
|
room_preset: Optional[str] = None
|
|
@@ -162,6 +219,22 @@ class AccessToken:
|
|
|
162
219
|
self.claims.inference = grants
|
|
163
220
|
return self
|
|
164
221
|
|
|
222
|
+
def with_agent_grants(self, grants: AgentGrants) -> "AccessToken":
|
|
223
|
+
self.claims.agent = grants
|
|
224
|
+
return self
|
|
225
|
+
|
|
226
|
+
def with_agent_endpoint_grants(self, grants: AgentEndpointGrants) -> "AccessToken":
|
|
227
|
+
self.claims.agent_endpoint = grants
|
|
228
|
+
return self
|
|
229
|
+
|
|
230
|
+
def with_agent_thread_grants(self, grants: AgentThreadGrants) -> "AccessToken":
|
|
231
|
+
self.claims.agent_thread = grants
|
|
232
|
+
return self
|
|
233
|
+
|
|
234
|
+
def with_agent_database_grants(self, grants: AgentDatabaseGrants) -> "AccessToken":
|
|
235
|
+
self.claims.agent_database = grants
|
|
236
|
+
return self
|
|
237
|
+
|
|
165
238
|
def with_identity(self, identity: str) -> "AccessToken":
|
|
166
239
|
self.identity = identity
|
|
167
240
|
return self
|
|
@@ -253,32 +326,21 @@ class TokenVerifier:
|
|
|
253
326
|
leeway=self._leeway.total_seconds(),
|
|
254
327
|
options={"verify_signature": False},
|
|
255
328
|
)
|
|
256
|
-
video_dict = claims.get("video", dict())
|
|
257
|
-
video_dict = {camel_to_snake(k): v for k, v in video_dict.items()}
|
|
258
|
-
video_dict = {k: v for k, v in video_dict.items() if k in VideoGrants.__dataclass_fields__}
|
|
259
|
-
video = VideoGrants(**video_dict)
|
|
260
|
-
|
|
261
|
-
sip_dict = claims.get("sip", dict())
|
|
262
|
-
sip_dict = {camel_to_snake(k): v for k, v in sip_dict.items()}
|
|
263
|
-
sip_dict = {k: v for k, v in sip_dict.items() if k in SIPGrants.__dataclass_fields__}
|
|
264
|
-
sip = SIPGrants(**sip_dict)
|
|
265
|
-
|
|
266
|
-
inference_dict = claims.get("inference", dict())
|
|
267
|
-
inference_dict = {camel_to_snake(k): v for k, v in inference_dict.items()}
|
|
268
|
-
inference_dict = {
|
|
269
|
-
k: v for k, v in inference_dict.items() if k in InferenceGrants.__dataclass_fields__
|
|
270
|
-
}
|
|
271
|
-
inference = InferenceGrants(**inference_dict)
|
|
272
|
-
|
|
273
329
|
grant_claims = Claims(
|
|
274
330
|
identity=claims.get("sub", ""),
|
|
275
331
|
name=claims.get("name", ""),
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
332
|
+
kind=claims.get("kind", ""),
|
|
333
|
+
video=_parse_grants(claims, "video", VideoGrants),
|
|
334
|
+
sip=_parse_grants(claims, "sip", SIPGrants),
|
|
335
|
+
inference=_parse_grants(claims, "inference", InferenceGrants),
|
|
336
|
+
observability=_parse_grants(claims, "observability", ObservabilityGrants),
|
|
279
337
|
attributes=claims.get("attributes", {}),
|
|
280
338
|
metadata=claims.get("metadata", ""),
|
|
281
339
|
sha256=claims.get("sha256", ""),
|
|
340
|
+
agent=_parse_grants(claims, "agent", AgentGrants),
|
|
341
|
+
agent_endpoint=_parse_grants(claims, "agentEndpoint", AgentEndpointGrants),
|
|
342
|
+
agent_thread=_parse_grants(claims, "agentThread", AgentThreadGrants),
|
|
343
|
+
agent_database=_parse_grants(claims, "agentDatabase", AgentDatabaseGrants),
|
|
282
344
|
)
|
|
283
345
|
|
|
284
346
|
if claims.get("roomPreset"):
|
|
@@ -294,6 +356,17 @@ class TokenVerifier:
|
|
|
294
356
|
return grant_claims
|
|
295
357
|
|
|
296
358
|
|
|
359
|
+
_G = TypeVar("_G")
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _parse_grants(claims: dict, key: str, cls: Type[_G]) -> Optional[_G]:
|
|
363
|
+
grants = claims.get(key)
|
|
364
|
+
if grants is None:
|
|
365
|
+
return None
|
|
366
|
+
fields = cls.__dataclass_fields__ # type: ignore[attr-defined]
|
|
367
|
+
return cls(**{camel_to_snake(k): v for k, v in grants.items() if camel_to_snake(k) in fields})
|
|
368
|
+
|
|
369
|
+
|
|
297
370
|
def camel_to_snake(t: str) -> str:
|
|
298
371
|
return re.sub(r"(?<!^)(?=[A-Z])", "_", t).lower()
|
|
299
372
|
|
|
@@ -49,9 +49,10 @@ SVC = "SIP"
|
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
def _as_sip_error(err: ServerError) -> ServerError:
|
|
52
|
-
"""Surface a SIP dialing failure as a SipCallError so callers can
|
|
53
|
-
the SIP status; other failures (auth,
|
|
54
|
-
|
|
52
|
+
"""Surface a SIP dialing or transfer failure as a SipCallError so callers can
|
|
53
|
+
branch on the SIP status or the transfer reason; other failures (auth,
|
|
54
|
+
validation) are returned unchanged."""
|
|
55
|
+
if "sip_status_code" in err.metadata or "sip_transfer_reason" in err.metadata:
|
|
55
56
|
return SipCallError.from_server_error(err)
|
|
56
57
|
return err
|
|
57
58
|
|
|
@@ -26,6 +26,7 @@ from ._failover import (
|
|
|
26
26
|
RegionCache,
|
|
27
27
|
failover_attempts,
|
|
28
28
|
host_key,
|
|
29
|
+
is_cloud_api,
|
|
29
30
|
origin_of,
|
|
30
31
|
pick_next,
|
|
31
32
|
)
|
|
@@ -87,10 +88,19 @@ class ServerError(Exception):
|
|
|
87
88
|
return result
|
|
88
89
|
|
|
89
90
|
|
|
91
|
+
_SIP_META_KEYS = (
|
|
92
|
+
"sip_status_code",
|
|
93
|
+
"sip_status",
|
|
94
|
+
"sip_transfer_reason",
|
|
95
|
+
"error_details",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
90
99
|
class SipCallError(ServerError):
|
|
91
100
|
"""A :class:`ServerError` from a SIP dialing call (``create_sip_participant`` /
|
|
92
|
-
``transfer_sip_participant``) that failed with a SIP response status
|
|
93
|
-
|
|
101
|
+
``transfer_sip_participant``) that failed with a SIP response status, or a
|
|
102
|
+
transfer that failed for any other reason. The SIP code and reason, and the
|
|
103
|
+
transfer reason, are exposed as properties; any other error metadata remains
|
|
94
104
|
available via :attr:`metadata`."""
|
|
95
105
|
|
|
96
106
|
@property
|
|
@@ -109,25 +119,36 @@ class SipCallError(ServerError):
|
|
|
109
119
|
"""The SIP reason phrase of the failed call, e.g. "Busy Here"."""
|
|
110
120
|
return self.metadata.get("sip_status")
|
|
111
121
|
|
|
122
|
+
@property
|
|
123
|
+
def sip_transfer_reason(self) -> Optional[str]:
|
|
124
|
+
"""Why a transfer failed, e.g. "STR_RINGING_TIMEOUT". Only set for
|
|
125
|
+
``transfer_sip_participant``."""
|
|
126
|
+
return self.metadata.get("sip_transfer_reason")
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def sip_transfer_id(self) -> Optional[str]:
|
|
130
|
+
"""The id of the failed transfer, for matching against SIP transfer logs."""
|
|
131
|
+
return self.metadata.get("sip_transfer_id")
|
|
132
|
+
|
|
112
133
|
@classmethod
|
|
113
134
|
def from_server_error(cls, err: ServerError) -> "SipCallError":
|
|
114
135
|
return cls(err.code, err.message, status=err.status, metadata=err.metadata)
|
|
115
136
|
|
|
116
137
|
def __str__(self) -> str:
|
|
117
138
|
code = self.metadata.get("sip_status_code")
|
|
118
|
-
|
|
139
|
+
transfer_reason = self.metadata.get("sip_transfer_reason")
|
|
140
|
+
if code is None and transfer_reason is None:
|
|
119
141
|
return super().__str__()
|
|
120
142
|
# A clear, SIP-specific representation, including any extra metadata.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
143
|
+
parts = []
|
|
144
|
+
if transfer_reason is not None:
|
|
145
|
+
parts.append(transfer_reason)
|
|
146
|
+
if code is not None:
|
|
147
|
+
reason = self.metadata.get("sip_status")
|
|
148
|
+
parts.append(f"{code} {reason}" if reason else str(code))
|
|
149
|
+
what = "SIP transfer failed" if transfer_reason is not None else "SIP call failed"
|
|
150
|
+
result = f"{what}: {', '.join(parts)} ({self.code})"
|
|
151
|
+
extra = {k: v for k, v in self.metadata.items() if k not in _SIP_META_KEYS}
|
|
131
152
|
if extra:
|
|
132
153
|
result += " [" + ", ".join(f"{k}={v}" for k, v in extra.items()) + "]"
|
|
133
154
|
return result
|
|
@@ -182,7 +203,7 @@ class TwirpClient:
|
|
|
182
203
|
if scheme.startswith("ws"):
|
|
183
204
|
scheme = scheme.replace("ws", "http")
|
|
184
205
|
|
|
185
|
-
host = f"{scheme}://{parse_res.netloc}
|
|
206
|
+
host = f"{scheme}://{parse_res.netloc}{parse_res.path}"
|
|
186
207
|
self.host = host.rstrip("/")
|
|
187
208
|
self.pkg = pkg
|
|
188
209
|
self.prefix = prefix
|
|
@@ -226,12 +247,16 @@ class TwirpClient:
|
|
|
226
247
|
self._failover, host, self._failover_force, effective_timeout
|
|
227
248
|
)
|
|
228
249
|
attempted = {host_key(self._origin)}
|
|
229
|
-
|
|
250
|
+
# A Cloud API host has a single origin; region discovery is never consulted.
|
|
251
|
+
region_origins: Optional[List[str]] = [] if host and is_cloud_api(host) else None
|
|
230
252
|
current_origin = self._origin
|
|
253
|
+
# The configured host may carry a base path (e.g. behind a reverse
|
|
254
|
+
# proxy); fallback regions are bare origins.
|
|
255
|
+
current_base = self.host
|
|
231
256
|
|
|
232
257
|
for attempt in range(max_attempts):
|
|
233
258
|
is_last = attempt + 1 >= max_attempts
|
|
234
|
-
url = f"{
|
|
259
|
+
url = f"{current_base}/{path}"
|
|
235
260
|
|
|
236
261
|
transport_exc: Optional[BaseException] = None
|
|
237
262
|
retryable_status: Optional[int] = None
|
|
@@ -264,9 +289,12 @@ class TwirpClient:
|
|
|
264
289
|
next_origin = pick_next(region_origins, attempted)
|
|
265
290
|
|
|
266
291
|
if next_origin is None:
|
|
267
|
-
if
|
|
268
|
-
|
|
269
|
-
|
|
292
|
+
if is_last:
|
|
293
|
+
if transport_exc is not None:
|
|
294
|
+
raise transport_exc
|
|
295
|
+
raise self._server_error(error_data, retryable_status or 500)
|
|
296
|
+
# With no fallback origin, a retryable failure is retried against the same host.
|
|
297
|
+
next_origin = current_origin
|
|
270
298
|
|
|
271
299
|
reason = transport_exc if transport_exc is not None else f"status {retryable_status}"
|
|
272
300
|
logger.warning(
|
|
@@ -278,6 +306,7 @@ class TwirpClient:
|
|
|
278
306
|
await asyncio.sleep(self._failover_backoff * (2**attempt))
|
|
279
307
|
attempted.add(host_key(next_origin))
|
|
280
308
|
current_origin = next_origin
|
|
309
|
+
current_base = next_origin
|
|
281
310
|
|
|
282
311
|
raise RuntimeError("failover loop exited without returning") # unreachable
|
|
283
312
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.2.3"
|
|
@@ -11,7 +11,7 @@ class WebhookReceiver:
|
|
|
11
11
|
|
|
12
12
|
def receive(self, body: str, auth_token: str) -> WebhookEvent:
|
|
13
13
|
claims = self._verifier.verify(auth_token)
|
|
14
|
-
if claims.sha256
|
|
14
|
+
if not claims.sha256:
|
|
15
15
|
raise Exception("sha256 was not found in the token")
|
|
16
16
|
|
|
17
17
|
body_hash = hashlib.sha256(body.encode()).digest()
|
|
@@ -28,11 +28,16 @@ classifiers = [
|
|
|
28
28
|
"Programming Language :: Python :: 3 :: Only",
|
|
29
29
|
]
|
|
30
30
|
dependencies = [
|
|
31
|
-
"pyjwt>=2.
|
|
32
|
-
|
|
31
|
+
"pyjwt>=2.15.1",
|
|
32
|
+
# aiohttp 3.14 dropped Python 3.9; keep its final compatible release there.
|
|
33
|
+
"aiohttp>=3.13.5; python_version < '3.10'",
|
|
34
|
+
"aiohttp>=3.14.3; python_version >= '3.10'",
|
|
35
|
+
# Security floors for aiohttp's transitive dependencies.
|
|
36
|
+
"idna>=3.15",
|
|
37
|
+
"multidict>=6.9.1; python_version >= '3.10'",
|
|
33
38
|
"protobuf>=4",
|
|
34
39
|
"types-protobuf>=4",
|
|
35
|
-
"livekit-protocol>=1.1.
|
|
40
|
+
"livekit-protocol>=1.1.32,<2.0.0",
|
|
36
41
|
]
|
|
37
42
|
|
|
38
43
|
[project.urls]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.2.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|