livekit-api 1.2.0__tar.gz → 1.2.2__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.2/PKG-INFO +62 -0
- livekit_api-1.2.2/README.md +29 -0
- {livekit_api-1.2.0 → livekit_api-1.2.2}/livekit/api/__init__.py +6 -0
- {livekit_api-1.2.0 → livekit_api-1.2.2}/livekit/api/_failover.py +10 -4
- {livekit_api-1.2.0 → livekit_api-1.2.2}/livekit/api/access_token.py +101 -9
- {livekit_api-1.2.0 → livekit_api-1.2.2}/livekit/api/egress_service.py +11 -0
- {livekit_api-1.2.0 → livekit_api-1.2.2}/livekit/api/sip_service.py +12 -7
- {livekit_api-1.2.0 → livekit_api-1.2.2}/livekit/api/twirp_client.py +55 -19
- livekit_api-1.2.2/livekit/api/version.py +1 -0
- {livekit_api-1.2.0 → livekit_api-1.2.2}/livekit/api/webhook.py +1 -1
- {livekit_api-1.2.0 → livekit_api-1.2.2}/pyproject.toml +8 -3
- livekit_api-1.2.0/PKG-INFO +0 -36
- livekit_api-1.2.0/README.md +0 -6
- livekit_api-1.2.0/livekit/api/version.py +0 -1
- {livekit_api-1.2.0 → livekit_api-1.2.2}/.gitignore +0 -0
- {livekit_api-1.2.0 → livekit_api-1.2.2}/livekit/api/_dial_timeout.py +0 -0
- {livekit_api-1.2.0 → livekit_api-1.2.2}/livekit/api/_service.py +0 -0
- {livekit_api-1.2.0 → livekit_api-1.2.2}/livekit/api/agent_dispatch_service.py +0 -0
- {livekit_api-1.2.0 → livekit_api-1.2.2}/livekit/api/connector_service.py +0 -0
- {livekit_api-1.2.0 → livekit_api-1.2.2}/livekit/api/ingress_service.py +0 -0
- {livekit_api-1.2.0 → livekit_api-1.2.2}/livekit/api/livekit_api.py +0 -0
- {livekit_api-1.2.0 → livekit_api-1.2.2}/livekit/api/py.typed +0 -0
- {livekit_api-1.2.0 → livekit_api-1.2.2}/livekit/api/room_service.py +0 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: livekit-api
|
|
3
|
+
Version: 1.2.2
|
|
4
|
+
Summary: Python Server API for LiveKit
|
|
5
|
+
Project-URL: Documentation, https://docs.livekit.io
|
|
6
|
+
Project-URL: Website, https://livekit.io/
|
|
7
|
+
Project-URL: Source, https://github.com/livekit/python-sdks/
|
|
8
|
+
Author-email: LiveKit <support@livekit.io>
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
Keywords: audio,livekit,realtime,video,webrtc
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
21
|
+
Classifier: Topic :: Multimedia :: Video
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
23
|
+
Requires-Python: >=3.9.0
|
|
24
|
+
Requires-Dist: aiohttp>=3.13.5; python_version < '3.10'
|
|
25
|
+
Requires-Dist: aiohttp>=3.14.4; python_version >= '3.10'
|
|
26
|
+
Requires-Dist: idna>=3.15
|
|
27
|
+
Requires-Dist: livekit-protocol<2.0.0,>=1.1.27
|
|
28
|
+
Requires-Dist: multidict>=6.9.1; python_version >= '3.10'
|
|
29
|
+
Requires-Dist: protobuf>=4
|
|
30
|
+
Requires-Dist: pyjwt>=2.15.1
|
|
31
|
+
Requires-Dist: types-protobuf>=4
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# LiveKit Server APIs
|
|
35
|
+
|
|
36
|
+
Access LiveKit server APIs and generate access tokens.
|
|
37
|
+
|
|
38
|
+
See https://docs.livekit.io/reference/server/server-apis for more information.
|
|
39
|
+
|
|
40
|
+
## Authentication
|
|
41
|
+
|
|
42
|
+
Every request to the server APIs is authenticated. `LiveKitAPI` supports two modes:
|
|
43
|
+
|
|
44
|
+
- **API key & secret** — recommended for backend use. The SDK signs a short-lived token per request from your key and secret. Keep your API secret on the server; never ship it to a client.
|
|
45
|
+
- **Access token** — for client-side use where the API secret must not be exposed. Pass a pre-signed [access token](https://docs.livekit.io/home/get-started/authentication/) that already carries the grants for the operations you'll perform; the SDK sends it verbatim.
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from livekit import api
|
|
49
|
+
|
|
50
|
+
# API key & secret: set LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET,
|
|
51
|
+
# then construct with no arguments...
|
|
52
|
+
lkapi = api.LiveKitAPI()
|
|
53
|
+
|
|
54
|
+
# ...or pass any of them explicitly to override the corresponding env var:
|
|
55
|
+
lkapi = api.LiveKitAPI("https://my.livekit.host", api_key="api-key", api_secret="api-secret")
|
|
56
|
+
|
|
57
|
+
# Pre-signed access token (client-side): with LIVEKIT_URL set, pass just the token:
|
|
58
|
+
lkapi = api.LiveKitAPI.with_token("a-pre-signed-token")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The url and credentials fall back to the `LIVEKIT_URL`, `LIVEKIT_API_KEY`, `LIVEKIT_API_SECRET`, and `LIVEKIT_TOKEN` environment variables. Values you pass explicitly take precedence; the environment variables are used only as a fallback for arguments you omit — an ambient `LIVEKIT_TOKEN`, for example, won't override an explicitly-provided API key and secret.
|
|
62
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# LiveKit Server APIs
|
|
2
|
+
|
|
3
|
+
Access LiveKit server APIs and generate access tokens.
|
|
4
|
+
|
|
5
|
+
See https://docs.livekit.io/reference/server/server-apis for more information.
|
|
6
|
+
|
|
7
|
+
## Authentication
|
|
8
|
+
|
|
9
|
+
Every request to the server APIs is authenticated. `LiveKitAPI` supports two modes:
|
|
10
|
+
|
|
11
|
+
- **API key & secret** — recommended for backend use. The SDK signs a short-lived token per request from your key and secret. Keep your API secret on the server; never ship it to a client.
|
|
12
|
+
- **Access token** — for client-side use where the API secret must not be exposed. Pass a pre-signed [access token](https://docs.livekit.io/home/get-started/authentication/) that already carries the grants for the operations you'll perform; the SDK sends it verbatim.
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from livekit import api
|
|
16
|
+
|
|
17
|
+
# API key & secret: set LIVEKIT_URL, LIVEKIT_API_KEY, and LIVEKIT_API_SECRET,
|
|
18
|
+
# then construct with no arguments...
|
|
19
|
+
lkapi = api.LiveKitAPI()
|
|
20
|
+
|
|
21
|
+
# ...or pass any of them explicitly to override the corresponding env var:
|
|
22
|
+
lkapi = api.LiveKitAPI("https://my.livekit.host", api_key="api-key", api_secret="api-secret")
|
|
23
|
+
|
|
24
|
+
# Pre-signed access token (client-side): with LIVEKIT_URL set, pass just the token:
|
|
25
|
+
lkapi = api.LiveKitAPI.with_token("a-pre-signed-token")
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The url and credentials fall back to the `LIVEKIT_URL`, `LIVEKIT_API_KEY`, `LIVEKIT_API_SECRET`, and `LIVEKIT_TOKEN` environment variables. Values you pass explicitly take precedence; the environment variables are used only as a fallback for arguments you omit — an ambient `LIVEKIT_TOKEN`, for example, won't override an explicitly-provided API key and secret.
|
|
29
|
+
|
|
@@ -47,6 +47,9 @@ from .livekit_api import LiveKitAPI
|
|
|
47
47
|
from .access_token import (
|
|
48
48
|
InferenceGrants,
|
|
49
49
|
ObservabilityGrants,
|
|
50
|
+
AgentGrants,
|
|
51
|
+
AgentEndpointGrants,
|
|
52
|
+
AgentThreadGrants,
|
|
50
53
|
VideoGrants,
|
|
51
54
|
SIPGrants,
|
|
52
55
|
AccessToken,
|
|
@@ -65,6 +68,9 @@ __all__ = [
|
|
|
65
68
|
"connector_service",
|
|
66
69
|
"InferenceGrants",
|
|
67
70
|
"ObservabilityGrants",
|
|
71
|
+
"AgentGrants",
|
|
72
|
+
"AgentEndpointGrants",
|
|
73
|
+
"AgentThreadGrants",
|
|
68
74
|
"VideoGrants",
|
|
69
75
|
"SIPGrants",
|
|
70
76
|
"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,43 @@ 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
|
+
# access the project's agent databases
|
|
105
|
+
database_admin: bool = False
|
|
106
|
+
# manage the project's agent dispatch queue
|
|
107
|
+
dispatch_admin: bool = False
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class AgentEndpointGrants:
|
|
112
|
+
# invoke an agent's non-public HTTP endpoints
|
|
113
|
+
call: bool = False
|
|
114
|
+
# restrict to one agent; empty grants every agent in the project
|
|
115
|
+
agent_name: str = ""
|
|
116
|
+
# restrict to one deployment; empty grants every deployment
|
|
117
|
+
deployment: str = ""
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass
|
|
121
|
+
class AgentThreadGrants:
|
|
122
|
+
# every thread and scope of the project; the action flags still apply
|
|
123
|
+
all: bool = False
|
|
124
|
+
# scope in which threads may be listed and created
|
|
125
|
+
scope: str = ""
|
|
126
|
+
# threads that may be read, written and deleted
|
|
127
|
+
thread_ids: Optional[List[str]] = None
|
|
128
|
+
list: bool = False
|
|
129
|
+
create: bool = False
|
|
130
|
+
write: bool = False
|
|
131
|
+
read: bool = False
|
|
132
|
+
delete: bool = False
|
|
133
|
+
|
|
134
|
+
|
|
95
135
|
@dataclass
|
|
96
136
|
class Claims:
|
|
97
137
|
identity: str = ""
|
|
@@ -102,6 +142,9 @@ class Claims:
|
|
|
102
142
|
sip: Optional[SIPGrants] = None
|
|
103
143
|
inference: Optional[InferenceGrants] = None
|
|
104
144
|
observability: Optional[ObservabilityGrants] = None
|
|
145
|
+
agent: Optional[AgentGrants] = None
|
|
146
|
+
agent_endpoint: Optional[AgentEndpointGrants] = None
|
|
147
|
+
agent_thread: Optional[AgentThreadGrants] = None
|
|
105
148
|
attributes: Optional[dict[str, str]] = None
|
|
106
149
|
sha256: Optional[str] = None
|
|
107
150
|
room_preset: Optional[str] = None
|
|
@@ -162,6 +205,18 @@ class AccessToken:
|
|
|
162
205
|
self.claims.inference = grants
|
|
163
206
|
return self
|
|
164
207
|
|
|
208
|
+
def with_agent_grants(self, grants: AgentGrants) -> "AccessToken":
|
|
209
|
+
self.claims.agent = grants
|
|
210
|
+
return self
|
|
211
|
+
|
|
212
|
+
def with_agent_endpoint_grants(self, grants: AgentEndpointGrants) -> "AccessToken":
|
|
213
|
+
self.claims.agent_endpoint = grants
|
|
214
|
+
return self
|
|
215
|
+
|
|
216
|
+
def with_agent_thread_grants(self, grants: AgentThreadGrants) -> "AccessToken":
|
|
217
|
+
self.claims.agent_thread = grants
|
|
218
|
+
return self
|
|
219
|
+
|
|
165
220
|
def with_identity(self, identity: str) -> "AccessToken":
|
|
166
221
|
self.identity = identity
|
|
167
222
|
return self
|
|
@@ -233,14 +288,26 @@ class TokenVerifier:
|
|
|
233
288
|
if verify_signature and (not self.api_key or not self.api_secret):
|
|
234
289
|
raise ValueError("api_key and api_secret must be set")
|
|
235
290
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
291
|
+
# First-party minters always set exp. Without this, a hand-rolled token
|
|
292
|
+
# with a valid signature and no exp verifies forever (livekit/protocol#1706).
|
|
293
|
+
if verify_signature:
|
|
294
|
+
claims = jwt.decode(
|
|
295
|
+
token,
|
|
296
|
+
key=self.api_secret or "",
|
|
297
|
+
issuer=self.api_key or "",
|
|
298
|
+
algorithms=["HS256"],
|
|
299
|
+
leeway=self._leeway.total_seconds(),
|
|
300
|
+
options={"verify_signature": True, "require": ["exp"]},
|
|
301
|
+
)
|
|
302
|
+
else:
|
|
303
|
+
claims = jwt.decode(
|
|
304
|
+
token,
|
|
305
|
+
key=self.api_secret or "",
|
|
306
|
+
issuer=self.api_key or "",
|
|
307
|
+
algorithms=["HS256"],
|
|
308
|
+
leeway=self._leeway.total_seconds(),
|
|
309
|
+
options={"verify_signature": False},
|
|
310
|
+
)
|
|
244
311
|
video_dict = claims.get("video", dict())
|
|
245
312
|
video_dict = {camel_to_snake(k): v for k, v in video_dict.items()}
|
|
246
313
|
video_dict = {k: v for k, v in video_dict.items() if k in VideoGrants.__dataclass_fields__}
|
|
@@ -258,15 +325,29 @@ class TokenVerifier:
|
|
|
258
325
|
}
|
|
259
326
|
inference = InferenceGrants(**inference_dict)
|
|
260
327
|
|
|
328
|
+
observability_dict = claims.get("observability", dict())
|
|
329
|
+
observability_dict = {camel_to_snake(k): v for k, v in observability_dict.items()}
|
|
330
|
+
observability_dict = {
|
|
331
|
+
k: v
|
|
332
|
+
for k, v in observability_dict.items()
|
|
333
|
+
if k in ObservabilityGrants.__dataclass_fields__
|
|
334
|
+
}
|
|
335
|
+
observability = ObservabilityGrants(**observability_dict)
|
|
336
|
+
|
|
261
337
|
grant_claims = Claims(
|
|
262
338
|
identity=claims.get("sub", ""),
|
|
263
339
|
name=claims.get("name", ""),
|
|
340
|
+
kind=claims.get("kind", ""),
|
|
264
341
|
video=video,
|
|
265
342
|
sip=sip,
|
|
266
343
|
inference=inference,
|
|
344
|
+
observability=observability,
|
|
267
345
|
attributes=claims.get("attributes", {}),
|
|
268
346
|
metadata=claims.get("metadata", ""),
|
|
269
347
|
sha256=claims.get("sha256", ""),
|
|
348
|
+
agent=_parse_grants(claims, "agent", AgentGrants),
|
|
349
|
+
agent_endpoint=_parse_grants(claims, "agentEndpoint", AgentEndpointGrants),
|
|
350
|
+
agent_thread=_parse_grants(claims, "agentThread", AgentThreadGrants),
|
|
270
351
|
)
|
|
271
352
|
|
|
272
353
|
if claims.get("roomPreset"):
|
|
@@ -282,6 +363,17 @@ class TokenVerifier:
|
|
|
282
363
|
return grant_claims
|
|
283
364
|
|
|
284
365
|
|
|
366
|
+
_G = TypeVar("_G")
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _parse_grants(claims: dict, key: str, cls: Type[_G]) -> Optional[_G]:
|
|
370
|
+
grants = claims.get(key)
|
|
371
|
+
if grants is None:
|
|
372
|
+
return None
|
|
373
|
+
fields = cls.__dataclass_fields__ # type: ignore[attr-defined]
|
|
374
|
+
return cls(**{camel_to_snake(k): v for k, v in grants.items() if camel_to_snake(k) in fields})
|
|
375
|
+
|
|
376
|
+
|
|
285
377
|
def camel_to_snake(t: str) -> str:
|
|
286
378
|
return re.sub(r"(?<!^)(?=[A-Z])", "_", t).lower()
|
|
287
379
|
|
|
@@ -9,6 +9,7 @@ from livekit.protocol.egress import (
|
|
|
9
9
|
UpdateStreamRequest,
|
|
10
10
|
ListEgressRequest,
|
|
11
11
|
StopEgressRequest,
|
|
12
|
+
StartEgressRequest,
|
|
12
13
|
EgressInfo,
|
|
13
14
|
ListEgressResponse,
|
|
14
15
|
)
|
|
@@ -93,6 +94,16 @@ class EgressService(Service):
|
|
|
93
94
|
EgressInfo,
|
|
94
95
|
)
|
|
95
96
|
|
|
97
|
+
async def start_egress(self, start: StartEgressRequest) -> EgressInfo:
|
|
98
|
+
"""Starts an egress; the request's source and outputs determine the egress type."""
|
|
99
|
+
return await self._client.request(
|
|
100
|
+
SVC,
|
|
101
|
+
"StartEgress",
|
|
102
|
+
start,
|
|
103
|
+
self._auth_header(VideoGrants(room_record=True)),
|
|
104
|
+
EgressInfo,
|
|
105
|
+
)
|
|
106
|
+
|
|
96
107
|
async def update_layout(self, update: UpdateLayoutRequest) -> EgressInfo:
|
|
97
108
|
"""Updates the layout of a composite recording."""
|
|
98
109
|
return await self._client.request(
|
|
@@ -31,6 +31,7 @@ from livekit.protocol.sip import (
|
|
|
31
31
|
DeleteSIPDispatchRuleRequest,
|
|
32
32
|
CreateSIPParticipantRequest,
|
|
33
33
|
TransferSIPParticipantRequest,
|
|
34
|
+
TransferSIPParticipantResponse,
|
|
34
35
|
SIPParticipantInfo,
|
|
35
36
|
SIPTransport,
|
|
36
37
|
SIPMediaConfig,
|
|
@@ -48,9 +49,10 @@ SVC = "SIP"
|
|
|
48
49
|
|
|
49
50
|
|
|
50
51
|
def _as_sip_error(err: ServerError) -> ServerError:
|
|
51
|
-
"""Surface a SIP dialing failure as a SipCallError so callers can
|
|
52
|
-
the SIP status; other failures (auth,
|
|
53
|
-
|
|
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:
|
|
54
56
|
return SipCallError.from_server_error(err)
|
|
55
57
|
return err
|
|
56
58
|
|
|
@@ -832,8 +834,8 @@ class SipService(Service):
|
|
|
832
834
|
transfer: TransferSIPParticipantRequest,
|
|
833
835
|
*,
|
|
834
836
|
timeout: Optional[float] = None,
|
|
835
|
-
) ->
|
|
836
|
-
"""Transfer a SIP participant to
|
|
837
|
+
) -> TransferSIPParticipantResponse:
|
|
838
|
+
"""Transfer a SIP participant to another number or SIP endpoint.
|
|
837
839
|
|
|
838
840
|
Args:
|
|
839
841
|
transfer: Request containing transfer details
|
|
@@ -842,7 +844,10 @@ class SipService(Service):
|
|
|
842
844
|
longer timeout when unset.
|
|
843
845
|
|
|
844
846
|
Returns:
|
|
845
|
-
|
|
847
|
+
The outcome of the transfer: its id, status, reason, and the SIP
|
|
848
|
+
status when the outcome came from a SIP response. A status other
|
|
849
|
+
than successful means the transfer did not complete, even when no
|
|
850
|
+
error was raised.
|
|
846
851
|
"""
|
|
847
852
|
# Transferring a call dials a phone, which takes longer than a normal
|
|
848
853
|
# call, so keep the request alive past ringing. Pin the ring window so the
|
|
@@ -861,7 +866,7 @@ class SipService(Service):
|
|
|
861
866
|
),
|
|
862
867
|
sip=SIPGrants(call=True),
|
|
863
868
|
),
|
|
864
|
-
|
|
869
|
+
TransferSIPParticipantResponse,
|
|
865
870
|
timeout=client_timeout,
|
|
866
871
|
)
|
|
867
872
|
except ServerError as e:
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import asyncio
|
|
16
16
|
import logging
|
|
17
|
+
import uuid
|
|
17
18
|
from typing import Dict, List, Optional, Type, TypeVar
|
|
18
19
|
|
|
19
20
|
import aiohttp
|
|
@@ -25,6 +26,7 @@ from ._failover import (
|
|
|
25
26
|
RegionCache,
|
|
26
27
|
failover_attempts,
|
|
27
28
|
host_key,
|
|
29
|
+
is_cloud_api,
|
|
28
30
|
origin_of,
|
|
29
31
|
pick_next,
|
|
30
32
|
)
|
|
@@ -37,6 +39,11 @@ logger = logging.getLogger("livekit")
|
|
|
37
39
|
# Identifies the SDK and version to the server on every request.
|
|
38
40
|
_USER_AGENT = f"livekit-server-sdk-python/{__version__}"
|
|
39
41
|
|
|
42
|
+
# Carries a per-request idempotency key. The SDK's auto-retries (see _failover)
|
|
43
|
+
# keep the same key across attempts, so the server can identify and deduplicate
|
|
44
|
+
# repeated requests.
|
|
45
|
+
REQUEST_ID_HEADER = "X-Livekit-Request-Id"
|
|
46
|
+
|
|
40
47
|
# Shared across all clients in the process so the region list is fetched once.
|
|
41
48
|
_REGION_CACHE = RegionCache()
|
|
42
49
|
|
|
@@ -81,10 +88,19 @@ class ServerError(Exception):
|
|
|
81
88
|
return result
|
|
82
89
|
|
|
83
90
|
|
|
91
|
+
_SIP_META_KEYS = (
|
|
92
|
+
"sip_status_code",
|
|
93
|
+
"sip_status",
|
|
94
|
+
"sip_transfer_reason",
|
|
95
|
+
"error_details",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
84
99
|
class SipCallError(ServerError):
|
|
85
100
|
"""A :class:`ServerError` from a SIP dialing call (``create_sip_participant`` /
|
|
86
|
-
``transfer_sip_participant``) that failed with a SIP response status
|
|
87
|
-
|
|
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
|
|
88
104
|
available via :attr:`metadata`."""
|
|
89
105
|
|
|
90
106
|
@property
|
|
@@ -103,25 +119,36 @@ class SipCallError(ServerError):
|
|
|
103
119
|
"""The SIP reason phrase of the failed call, e.g. "Busy Here"."""
|
|
104
120
|
return self.metadata.get("sip_status")
|
|
105
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
|
+
|
|
106
133
|
@classmethod
|
|
107
134
|
def from_server_error(cls, err: ServerError) -> "SipCallError":
|
|
108
135
|
return cls(err.code, err.message, status=err.status, metadata=err.metadata)
|
|
109
136
|
|
|
110
137
|
def __str__(self) -> str:
|
|
111
138
|
code = self.metadata.get("sip_status_code")
|
|
112
|
-
|
|
139
|
+
transfer_reason = self.metadata.get("sip_transfer_reason")
|
|
140
|
+
if code is None and transfer_reason is None:
|
|
113
141
|
return super().__str__()
|
|
114
142
|
# A clear, SIP-specific representation, including any extra metadata.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
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}
|
|
125
152
|
if extra:
|
|
126
153
|
result += " [" + ", ".join(f"{k}={v}" for k, v in extra.items()) + "]"
|
|
127
154
|
return result
|
|
@@ -176,7 +203,7 @@ class TwirpClient:
|
|
|
176
203
|
if scheme.startswith("ws"):
|
|
177
204
|
scheme = scheme.replace("ws", "http")
|
|
178
205
|
|
|
179
|
-
host = f"{scheme}://{parse_res.netloc}
|
|
206
|
+
host = f"{scheme}://{parse_res.netloc}{parse_res.path}"
|
|
180
207
|
self.host = host.rstrip("/")
|
|
181
208
|
self.pkg = pkg
|
|
182
209
|
self.prefix = prefix
|
|
@@ -207,6 +234,7 @@ class TwirpClient:
|
|
|
207
234
|
headers["User-Agent"] = _USER_AGENT
|
|
208
235
|
forward_headers = dict(headers) # for the discovery fetch (no content-type yet)
|
|
209
236
|
headers["Content-Type"] = "application/protobuf"
|
|
237
|
+
headers[REQUEST_ID_HEADER] = str(uuid.uuid4())
|
|
210
238
|
serialized_data = data.SerializeToString()
|
|
211
239
|
|
|
212
240
|
# The effective per-attempt timeout is the per-call override, or the
|
|
@@ -219,12 +247,16 @@ class TwirpClient:
|
|
|
219
247
|
self._failover, host, self._failover_force, effective_timeout
|
|
220
248
|
)
|
|
221
249
|
attempted = {host_key(self._origin)}
|
|
222
|
-
|
|
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
|
|
223
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
|
|
224
256
|
|
|
225
257
|
for attempt in range(max_attempts):
|
|
226
258
|
is_last = attempt + 1 >= max_attempts
|
|
227
|
-
url = f"{
|
|
259
|
+
url = f"{current_base}/{path}"
|
|
228
260
|
|
|
229
261
|
transport_exc: Optional[BaseException] = None
|
|
230
262
|
retryable_status: Optional[int] = None
|
|
@@ -257,9 +289,12 @@ class TwirpClient:
|
|
|
257
289
|
next_origin = pick_next(region_origins, attempted)
|
|
258
290
|
|
|
259
291
|
if next_origin is None:
|
|
260
|
-
if
|
|
261
|
-
|
|
262
|
-
|
|
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
|
|
263
298
|
|
|
264
299
|
reason = transport_exc if transport_exc is not None else f"status {retryable_status}"
|
|
265
300
|
logger.warning(
|
|
@@ -271,6 +306,7 @@ class TwirpClient:
|
|
|
271
306
|
await asyncio.sleep(self._failover_backoff * (2**attempt))
|
|
272
307
|
attempted.add(host_key(next_origin))
|
|
273
308
|
current_origin = next_origin
|
|
309
|
+
current_base = next_origin
|
|
274
310
|
|
|
275
311
|
raise RuntimeError("failover loop exited without returning") # unreachable
|
|
276
312
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.2.2"
|
|
@@ -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.4; 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.27,<2.0.0",
|
|
36
41
|
]
|
|
37
42
|
|
|
38
43
|
[project.urls]
|
livekit_api-1.2.0/PKG-INFO
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: livekit-api
|
|
3
|
-
Version: 1.2.0
|
|
4
|
-
Summary: Python Server API for LiveKit
|
|
5
|
-
Project-URL: Documentation, https://docs.livekit.io
|
|
6
|
-
Project-URL: Website, https://livekit.io/
|
|
7
|
-
Project-URL: Source, https://github.com/livekit/python-sdks/
|
|
8
|
-
Author-email: LiveKit <support@livekit.io>
|
|
9
|
-
License-Expression: Apache-2.0
|
|
10
|
-
Keywords: audio,livekit,realtime,video,webrtc
|
|
11
|
-
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
-
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
21
|
-
Classifier: Topic :: Multimedia :: Video
|
|
22
|
-
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
23
|
-
Requires-Python: >=3.9.0
|
|
24
|
-
Requires-Dist: aiohttp>=3.9.0
|
|
25
|
-
Requires-Dist: livekit-protocol<2.0.0,>=1.1.19
|
|
26
|
-
Requires-Dist: protobuf>=4
|
|
27
|
-
Requires-Dist: pyjwt>=2.0.0
|
|
28
|
-
Requires-Dist: types-protobuf>=4
|
|
29
|
-
Description-Content-Type: text/markdown
|
|
30
|
-
|
|
31
|
-
# LiveKit Server APIs
|
|
32
|
-
|
|
33
|
-
Access LiveKit server APIs and generate access tokens.
|
|
34
|
-
|
|
35
|
-
See https://docs.livekit.io/reference/server/server-apis for more information.
|
|
36
|
-
|
livekit_api-1.2.0/README.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.2.0"
|
|
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
|