livekit-api 1.1.0__tar.gz → 1.2.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 (34) hide show
  1. livekit_api-1.2.0/.gitignore +167 -0
  2. {livekit_api-1.1.0 → livekit_api-1.2.0}/PKG-INFO +15 -24
  3. {livekit_api-1.1.0 → livekit_api-1.2.0}/livekit/api/__init__.py +10 -1
  4. livekit_api-1.2.0/livekit/api/_dial_timeout.py +51 -0
  5. livekit_api-1.2.0/livekit/api/_failover.py +162 -0
  6. livekit_api-1.2.0/livekit/api/_service.py +41 -0
  7. {livekit_api-1.1.0 → livekit_api-1.2.0}/livekit/api/access_token.py +8 -4
  8. {livekit_api-1.1.0 → livekit_api-1.2.0}/livekit/api/agent_dispatch_service.py +12 -3
  9. {livekit_api-1.1.0 → livekit_api-1.2.0}/livekit/api/connector_service.py +30 -3
  10. {livekit_api-1.1.0 → livekit_api-1.2.0}/livekit/api/egress_service.py +9 -2
  11. {livekit_api-1.1.0 → livekit_api-1.2.0}/livekit/api/ingress_service.py +9 -2
  12. livekit_api-1.2.0/livekit/api/livekit_api.py +170 -0
  13. {livekit_api-1.1.0 → livekit_api-1.2.0}/livekit/api/room_service.py +9 -2
  14. {livekit_api-1.1.0 → livekit_api-1.2.0}/livekit/api/sip_service.py +74 -33
  15. livekit_api-1.2.0/livekit/api/twirp_client.py +284 -0
  16. livekit_api-1.2.0/livekit/api/version.py +1 -0
  17. livekit_api-1.2.0/pyproject.toml +53 -0
  18. livekit_api-1.1.0/livekit/api/_service.py +0 -30
  19. livekit_api-1.1.0/livekit/api/livekit_api.py +0 -117
  20. livekit_api-1.1.0/livekit/api/twirp_client.py +0 -134
  21. livekit_api-1.1.0/livekit/api/version.py +0 -1
  22. livekit_api-1.1.0/livekit_api.egg-info/PKG-INFO +0 -45
  23. livekit_api-1.1.0/livekit_api.egg-info/SOURCES.txt +0 -24
  24. livekit_api-1.1.0/livekit_api.egg-info/dependency_links.txt +0 -1
  25. livekit_api-1.1.0/livekit_api.egg-info/requires.txt +0 -5
  26. livekit_api-1.1.0/livekit_api.egg-info/top_level.txt +0 -1
  27. livekit_api-1.1.0/pyproject.toml +0 -5
  28. livekit_api-1.1.0/setup.cfg +0 -4
  29. livekit_api-1.1.0/setup.py +0 -66
  30. livekit_api-1.1.0/tests/test_access_token.py +0 -101
  31. livekit_api-1.1.0/tests/test_webhook.py +0 -81
  32. {livekit_api-1.1.0 → livekit_api-1.2.0}/README.md +0 -0
  33. {livekit_api-1.1.0 → livekit_api-1.2.0}/livekit/api/py.typed +0 -0
  34. {livekit_api-1.1.0 → livekit_api-1.2.0}/livekit/api/webhook.py +0 -0
@@ -0,0 +1,167 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ # *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ # lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # poetry
98
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102
+ #poetry.lock
103
+
104
+ # pdm
105
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106
+ #pdm.lock
107
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108
+ # in version control.
109
+ # https://pdm.fming.dev/#use-with-ide
110
+ .pdm.toml
111
+
112
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113
+ __pypackages__/
114
+
115
+ # Celery stuff
116
+ celerybeat-schedule
117
+ celerybeat.pid
118
+
119
+ # SageMath parsed files
120
+ *.sage.py
121
+
122
+ # Environments
123
+ .env
124
+ .venv
125
+ env/
126
+ venv/
127
+ ENV/
128
+ env.bak/
129
+ venv.bak/
130
+
131
+ # Spyder project settings
132
+ .spyderproject
133
+ .spyproject
134
+
135
+ # Rope project settings
136
+ .ropeproject
137
+
138
+ # mkdocs documentation
139
+ /site
140
+
141
+ # mypy
142
+ .mypy_cache/
143
+ .dmypy.json
144
+ dmypy.json
145
+
146
+ # Pyre type checker
147
+ .pyre/
148
+
149
+ # pytype static type analyzer
150
+ .pytype/
151
+
152
+ # Cython debug symbols
153
+ cython_debug/
154
+
155
+ # PyCharm
156
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
159
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160
+ #.idea/
161
+
162
+ # vscode project settings
163
+ .vscode
164
+
165
+ .DS_Store
166
+
167
+ docs
@@ -1,41 +1,32 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: livekit-api
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: Python Server API for LiveKit
5
- Home-page: https://github.com/livekit/python-sdks
6
- License: Apache-2.0
7
5
  Project-URL: Documentation, https://docs.livekit.io
8
6
  Project-URL: Website, https://livekit.io/
9
7
  Project-URL: Source, https://github.com/livekit/python-sdks/
10
- Keywords: webrtc,realtime,audio,video,livekit
8
+ Author-email: LiveKit <support@livekit.io>
9
+ License-Expression: Apache-2.0
10
+ Keywords: audio,livekit,realtime,video,webrtc
11
11
  Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: Apache Software License
13
- Classifier: Topic :: Multimedia :: Sound/Audio
14
- Classifier: Topic :: Multimedia :: Video
15
- Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
16
12
  Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.7
18
- Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3 :: Only
19
14
  Classifier: Programming Language :: Python :: 3.9
20
15
  Classifier: Programming Language :: Python :: 3.10
21
- Classifier: Programming Language :: Python :: 3 :: Only
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
22
23
  Requires-Python: >=3.9.0
23
- Description-Content-Type: text/markdown
24
- Requires-Dist: pyjwt>=2.0.0
25
24
  Requires-Dist: aiohttp>=3.9.0
25
+ Requires-Dist: livekit-protocol<2.0.0,>=1.1.19
26
26
  Requires-Dist: protobuf>=4
27
+ Requires-Dist: pyjwt>=2.0.0
27
28
  Requires-Dist: types-protobuf>=4
28
- Requires-Dist: livekit-protocol<2.0.0,>=1.1.1
29
- Dynamic: classifier
30
- Dynamic: description
31
- Dynamic: description-content-type
32
- Dynamic: home-page
33
- Dynamic: keywords
34
- Dynamic: license
35
- Dynamic: project-url
36
- Dynamic: requires-dist
37
- Dynamic: requires-python
38
- Dynamic: summary
29
+ Description-Content-Type: text/markdown
39
30
 
40
31
  # LiveKit Server APIs
41
32
 
@@ -36,7 +36,13 @@ from livekit.protocol.sip import *
36
36
  from livekit.protocol.connector_whatsapp import *
37
37
  from livekit.protocol.connector_twilio import *
38
38
 
39
- from .twirp_client import TwirpError, TwirpErrorCode
39
+ from .twirp_client import (
40
+ ServerError,
41
+ ServerErrorCode,
42
+ SipCallError,
43
+ TwirpError,
44
+ TwirpErrorCode,
45
+ )
40
46
  from .livekit_api import LiveKitAPI
41
47
  from .access_token import (
42
48
  InferenceGrants,
@@ -64,6 +70,9 @@ __all__ = [
64
70
  "AccessToken",
65
71
  "TokenVerifier",
66
72
  "WebhookReceiver",
73
+ "ServerError",
74
+ "ServerErrorCode",
67
75
  "TwirpError",
68
76
  "TwirpErrorCode",
77
+ "SipCallError",
69
78
  ]
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional, Union
4
+
5
+ from livekit.protocol.sip import CreateSIPParticipantRequest, TransferSIPParticipantRequest
6
+
7
+ # Requests that carry ringing_timeout and share the phone-dialing timeout behavior.
8
+ DialRequest = Union[
9
+ CreateSIPParticipantRequest,
10
+ TransferSIPParticipantRequest,
11
+ ]
12
+ """@private"""
13
+
14
+ # Ring window (seconds) assumed when a request doesn't set ringing_timeout;
15
+ # matches the server default. A dialing request must outlast it.
16
+ DEFAULT_RINGING_TIMEOUT = 30.0
17
+ """@private"""
18
+
19
+ # A dialing request must outlast the ringing window, or it would abort before
20
+ # the call can be answered. Keep the request timeout at least this many seconds
21
+ # above the ringing timeout.
22
+ RINGING_TIMEOUT_MARGIN = 2.0
23
+ """@private"""
24
+
25
+
26
+ def pin_ringing_timeout(request: DialRequest) -> None:
27
+ """Set the ring window explicitly on a dialing request when the caller left it
28
+ unset, so the derived request timeout doesn't depend on the server's default
29
+ (which could change out from under us).
30
+
31
+ @private
32
+ """
33
+ if not request.HasField("ringing_timeout"):
34
+ request.ringing_timeout.seconds = int(DEFAULT_RINGING_TIMEOUT)
35
+
36
+
37
+ def dial_timeout(user_timeout: Optional[float], request: DialRequest) -> float:
38
+ """Request timeout (seconds) for a phone-dialing call: the ring window plus a
39
+ margin, so the request doesn't abort before the call can be answered. The
40
+ ring window is the request's ringing_timeout when set, else
41
+ DEFAULT_RINGING_TIMEOUT. A longer user_timeout is honored; a shorter one is
42
+ raised to the floor.
43
+
44
+ @private
45
+ """
46
+ if request.HasField("ringing_timeout"):
47
+ ring: float = request.ringing_timeout.seconds
48
+ else:
49
+ ring = DEFAULT_RINGING_TIMEOUT
50
+ floor = ring + RINGING_TIMEOUT_MARGIN
51
+ return max(user_timeout if user_timeout else floor, floor)
@@ -0,0 +1,162 @@
1
+ # Copyright 2026 LiveKit, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Region failover for the Twirp API clients.
16
+
17
+ On a retryable failure (any transport error or HTTP 5xx) the client discovers
18
+ alternative LiveKit Cloud regions via ``/settings/regions`` and replays the
19
+ request against the next region, with exponential backoff. 4xx responses are
20
+ returned immediately.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import time
26
+ from dataclasses import dataclass
27
+ from typing import Dict, List, Optional
28
+ from urllib.parse import urlparse
29
+
30
+ import aiohttp
31
+
32
+ FAILOVER_MAX_ATTEMPTS = 3
33
+ FAILOVER_BACKOFF_BASE = 0.2 # seconds
34
+ # Below this per-request timeout (seconds) a retry is unlikely to help and many
35
+ # clients would retry in lockstep across regions, so a short request gets a
36
+ # single attempt (thundering-herd guard).
37
+ MIN_FAILOVER_TIMEOUT = 5.0
38
+
39
+
40
+ def failover_attempts(
41
+ enabled: bool,
42
+ host: Optional[str],
43
+ force: bool = False,
44
+ timeout: Optional[float] = None,
45
+ ) -> int:
46
+ """Total request attempts for a host; 1 means no failover. Failover only
47
+ engages when enabled, the host is a LiveKit Cloud domain, and the request
48
+ timeout is long enough to retry. ``force`` bypasses the cloud-host check and
49
+ is for internal testing only.
50
+ """
51
+ if not (enabled and (force or (host is not None and is_cloud(host)))):
52
+ return 1
53
+ if timeout is not None and 0 < timeout < MIN_FAILOVER_TIMEOUT:
54
+ return 1
55
+ return FAILOVER_MAX_ATTEMPTS
56
+
57
+
58
+ def is_cloud(host: str) -> bool:
59
+ # Failover only engages for LiveKit Cloud project domains.
60
+ return host.endswith(".livekit.cloud")
61
+
62
+
63
+ def to_http(url: str) -> str:
64
+ """Normalizes a region URL to an http(s) scheme (ws -> http, wss -> https)."""
65
+ if url.startswith("ws"):
66
+ return "http" + url[2:]
67
+ return url
68
+
69
+
70
+ def origin_of(url: str) -> str:
71
+ """Returns the scheme://host[:port] origin of a URL, dropping any path."""
72
+ parsed = urlparse(url)
73
+ return f"{parsed.scheme}://{parsed.netloc}"
74
+
75
+
76
+ def host_key(url: str) -> str:
77
+ """A stable key identifying a host (including port) for dedup across attempts."""
78
+ return urlparse(url).netloc.lower()
79
+
80
+
81
+ def pick_next(region_origins: List[str], attempted: set[str]) -> Optional[str]:
82
+ """Returns the first region origin whose host has not yet been attempted."""
83
+ for origin in region_origins:
84
+ if host_key(origin) not in attempted:
85
+ return origin
86
+ return None
87
+
88
+
89
+ @dataclass
90
+ class _CacheEntry:
91
+ origins: List[str]
92
+ fetched_at: float
93
+ ttl: float
94
+
95
+
96
+ class RegionCache:
97
+ """Process-wide cache of the LiveKit Cloud region list, keyed by host."""
98
+
99
+ def __init__(self) -> None:
100
+ self._entries: Dict[str, _CacheEntry] = {}
101
+
102
+ async def region_origins(
103
+ self,
104
+ session: aiohttp.ClientSession,
105
+ origin: str,
106
+ headers: Dict[str, str],
107
+ ) -> List[str]:
108
+ """Returns alternative region origins for ``origin``, fetching
109
+ ``/settings/regions`` if the cache is stale. Best-effort: on a fetch
110
+ failure it serves a stale cached list when available, otherwise an empty
111
+ list. Forwards ``headers`` so a valid token — and any test directives —
112
+ reach the discovery endpoint."""
113
+ key = host_key(origin)
114
+ entry = self._entries.get(key)
115
+ if entry is not None and (time.monotonic() - entry.fetched_at) < entry.ttl:
116
+ return entry.origins
117
+
118
+ try:
119
+ origins, ttl = await self._fetch(session, origin, headers)
120
+ except Exception:
121
+ return entry.origins if entry is not None else []
122
+
123
+ # A zero TTL (e.g. Cache-Control: max-age=0) means "do not cache".
124
+ if ttl > 0:
125
+ self._entries[key] = _CacheEntry(origins, time.monotonic(), ttl)
126
+ return origins
127
+
128
+ async def _fetch(
129
+ self,
130
+ session: aiohttp.ClientSession,
131
+ origin: str,
132
+ headers: Dict[str, str],
133
+ ) -> tuple[List[str], float]:
134
+ fetch_headers = {
135
+ k: v for k, v in headers.items() if k.lower() not in ("content-type", "content-length")
136
+ }
137
+ # Short timeout so a slow/unreachable discovery endpoint doesn't stall
138
+ # the failover path.
139
+ async with session.get(
140
+ f"{origin}/settings/regions",
141
+ headers=fetch_headers,
142
+ timeout=aiohttp.ClientTimeout(total=2),
143
+ ) as resp:
144
+ if resp.status != 200:
145
+ raise RuntimeError(f"region discovery failed: {resp.status}")
146
+ ttl = _parse_max_age(resp.headers.get("Cache-Control"))
147
+ body = await resp.json()
148
+ origins = [origin_of(to_http(r["url"])) for r in body.get("regions", []) if r.get("url")]
149
+ return origins, ttl
150
+
151
+
152
+ def _parse_max_age(cache_control: Optional[str]) -> float:
153
+ if not cache_control:
154
+ return 0.0
155
+ for directive in cache_control.split(","):
156
+ directive = directive.strip().lower()
157
+ if directive.startswith("max-age="):
158
+ try:
159
+ return float(int(directive[len("max-age=") :]))
160
+ except ValueError:
161
+ return 0.0
162
+ return 0.0
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ import aiohttp
4
+ from abc import ABC
5
+ from .twirp_client import TwirpClient
6
+ from .access_token import AccessToken, VideoGrants, SIPGrants
7
+
8
+ AUTHORIZATION = "authorization"
9
+
10
+
11
+ class Service(ABC):
12
+ def __init__(
13
+ self,
14
+ session: aiohttp.ClientSession,
15
+ host: str,
16
+ api_key: str,
17
+ api_secret: str,
18
+ failover: bool = True,
19
+ ):
20
+ self._client = TwirpClient(session, host, "livekit", failover=failover)
21
+ self.api_key = api_key
22
+ self.api_secret = api_secret
23
+ # A pre-signed token set by LiveKitAPI for token auth; sent verbatim,
24
+ # skipping per-call signing. Per-service constructors stay key/secret-only.
25
+ self._token: str | None = None
26
+
27
+ def _auth_header(
28
+ self, grants: VideoGrants | None, sip: SIPGrants | None = None
29
+ ) -> dict[str, str]:
30
+ # A pre-signed token is sent verbatim; the caller is responsible for its grants.
31
+ if self._token:
32
+ return {AUTHORIZATION: "Bearer {}".format(self._token)}
33
+
34
+ tok = AccessToken(self.api_key, self.api_secret)
35
+ if grants:
36
+ tok.with_grants(grants)
37
+ if sip is not None:
38
+ tok.with_sip_grants(sip)
39
+
40
+ token = tok.to_jwt()
41
+ return {AUTHORIZATION: "Bearer {}".format(token)}
@@ -68,6 +68,9 @@ class VideoGrants:
68
68
  # indicates that the holder can register as an Agent framework worker
69
69
  agent: Optional[bool] = None
70
70
 
71
+ # allow participant to manage an agent session via RemoteSession
72
+ can_manage_agent_session: Optional[bool] = None
73
+
71
74
 
72
75
  @dataclass
73
76
  class SIPGrants:
@@ -268,9 +271,10 @@ class TokenVerifier:
268
271
 
269
272
  if claims.get("roomPreset"):
270
273
  grant_claims.room_preset = claims.get("roomPreset")
271
- if claims.get("roomConfig"):
274
+ room_config = claims.get("roomConfig")
275
+ if room_config:
272
276
  grant_claims.room_config = ParseDict(
273
- claims.get("roomConfig"),
277
+ room_config,
274
278
  RoomConfiguration(),
275
279
  ignore_unknown_fields=True,
276
280
  )
@@ -278,9 +282,9 @@ class TokenVerifier:
278
282
  return grant_claims
279
283
 
280
284
 
281
- def camel_to_snake(t: str):
285
+ def camel_to_snake(t: str) -> str:
282
286
  return re.sub(r"(?<!^)(?=[A-Z])", "_", t).lower()
283
287
 
284
288
 
285
- def snake_to_lower_camel(t: str):
289
+ def snake_to_lower_camel(t: str) -> str:
286
290
  return "".join(word.capitalize() if i else word for i, word in enumerate(t.split("_")))
@@ -26,8 +26,15 @@ class AgentDispatchService(Service):
26
26
  ```
27
27
  """
28
28
 
29
- def __init__(self, session: aiohttp.ClientSession, url: str, api_key: str, api_secret: str):
30
- super().__init__(session, url, api_key, api_secret)
29
+ def __init__(
30
+ self,
31
+ session: aiohttp.ClientSession,
32
+ url: str,
33
+ api_key: str,
34
+ api_secret: str,
35
+ failover: bool = True,
36
+ ):
37
+ super().__init__(session, url, api_key, api_secret, failover=failover)
31
38
 
32
39
  async def create_dispatch(self, req: CreateAgentDispatchRequest) -> AgentDispatch:
33
40
  """Create an explicit dispatch for an agent to join a room.
@@ -35,7 +42,9 @@ class AgentDispatchService(Service):
35
42
  To use explicit dispatch, your agent must be registered with an `agentName`.
36
43
 
37
44
  Args:
38
- req (CreateAgentDispatchRequest): Request containing dispatch creation parameters
45
+ req (CreateAgentDispatchRequest): Request containing dispatch creation parameters.
46
+ The request can include an optional `deployment` field to target a specific
47
+ deployment. Leave empty to target the production deployment.
39
48
 
40
49
  Returns:
41
50
  AgentDispatch: The created agent dispatch object
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import aiohttp
4
+ from typing import Optional
4
5
 
5
6
  from livekit.protocol.connector_whatsapp import (
6
7
  DialWhatsAppCallRequest,
@@ -17,6 +18,7 @@ from livekit.protocol.connector_twilio import (
17
18
  ConnectTwilioCallResponse,
18
19
  )
19
20
  from ._service import Service
21
+ from ._dial_timeout import DEFAULT_RINGING_TIMEOUT
20
22
  from .access_token import VideoGrants
21
23
 
22
24
  SVC = "Connector"
@@ -35,8 +37,15 @@ class ConnectorService(Service):
35
37
  ```
36
38
  """
37
39
 
38
- def __init__(self, session: aiohttp.ClientSession, url: str, api_key: str, api_secret: str):
39
- super().__init__(session, url, api_key, api_secret)
40
+ def __init__(
41
+ self,
42
+ session: aiohttp.ClientSession,
43
+ url: str,
44
+ api_key: str,
45
+ api_secret: str,
46
+ failover: bool = True,
47
+ ):
48
+ super().__init__(session, url, api_key, api_secret, failover=failover)
40
49
 
41
50
  async def dial_whatsapp_call(
42
51
  self, request: DialWhatsAppCallRequest
@@ -99,23 +108,41 @@ class ConnectorService(Service):
99
108
  )
100
109
 
101
110
  async def accept_whatsapp_call(
102
- self, request: AcceptWhatsAppCallRequest
111
+ self,
112
+ request: AcceptWhatsAppCallRequest,
113
+ *,
114
+ timeout: Optional[float] = None,
103
115
  ) -> AcceptWhatsAppCallResponse:
104
116
  """
105
117
  Accept an inbound WhatsApp call
106
118
 
107
119
  Args:
108
120
  request: AcceptWhatsAppCallRequest containing call parameters and SDP
121
+ timeout: Optional request timeout in seconds. When the request waits
122
+ for the inbound party to join (wait_until_answered), it defaults
123
+ to the standard ring window.
109
124
 
110
125
  Returns:
111
126
  AcceptWhatsAppCallResponse with the room name
112
127
  """
128
+ client_timeout: Optional[aiohttp.ClientTimeout] = None
129
+ if request.wait_until_answered:
130
+ # Waiting for the inbound party to join can block, so default the
131
+ # request timeout to the standard ring window; the caller overrides
132
+ # via timeout.
133
+ client_timeout = aiohttp.ClientTimeout(
134
+ total=timeout if timeout else DEFAULT_RINGING_TIMEOUT
135
+ )
136
+ elif timeout:
137
+ client_timeout = aiohttp.ClientTimeout(total=timeout)
138
+
113
139
  return await self._client.request(
114
140
  SVC,
115
141
  "AcceptWhatsAppCall",
116
142
  request,
117
143
  self._auth_header(VideoGrants(room_create=True)),
118
144
  AcceptWhatsAppCallResponse,
145
+ timeout=client_timeout,
119
146
  )
120
147
 
121
148
  async def connect_twilio_call(
@@ -33,8 +33,15 @@ class EgressService(Service):
33
33
  Also see https://docs.livekit.io/home/egress/overview/
34
34
  """
35
35
 
36
- def __init__(self, session: aiohttp.ClientSession, url: str, api_key: str, api_secret: str):
37
- super().__init__(session, url, api_key, api_secret)
36
+ def __init__(
37
+ self,
38
+ session: aiohttp.ClientSession,
39
+ url: str,
40
+ api_key: str,
41
+ api_secret: str,
42
+ failover: bool = True,
43
+ ):
44
+ super().__init__(session, url, api_key, api_secret, failover=failover)
38
45
 
39
46
  async def start_room_composite_egress(self, start: RoomCompositeEgressRequest) -> EgressInfo:
40
47
  """Starts a composite recording of a room."""
@@ -28,8 +28,15 @@ class IngressService(Service):
28
28
  Also see https://docs.livekit.io/home/ingress/overview/
29
29
  """
30
30
 
31
- def __init__(self, session: aiohttp.ClientSession, url: str, api_key: str, api_secret: str):
32
- super().__init__(session, url, api_key, api_secret)
31
+ def __init__(
32
+ self,
33
+ session: aiohttp.ClientSession,
34
+ url: str,
35
+ api_key: str,
36
+ api_secret: str,
37
+ failover: bool = True,
38
+ ):
39
+ super().__init__(session, url, api_key, api_secret, failover=failover)
33
40
 
34
41
  async def create_ingress(self, create: CreateIngressRequest) -> IngressInfo:
35
42
  return await self._client.request(