livekit-api 0.7.0__tar.gz → 0.8.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 (26) hide show
  1. {livekit_api-0.7.0 → livekit_api-0.8.0}/PKG-INFO +2 -2
  2. {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit/api/__init__.py +2 -0
  3. {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit/api/_service.py +5 -3
  4. {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit/api/access_token.py +75 -29
  5. livekit_api-0.8.0/livekit/api/agent_dispatch_service.py +108 -0
  6. {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit/api/livekit_api.py +8 -0
  7. {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit/api/sip_service.py +17 -0
  8. livekit_api-0.8.0/livekit/api/version.py +1 -0
  9. {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit/api/webhook.py +2 -0
  10. {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit_api.egg-info/PKG-INFO +2 -2
  11. {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit_api.egg-info/SOURCES.txt +1 -0
  12. {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit_api.egg-info/requires.txt +1 -1
  13. {livekit_api-0.7.0 → livekit_api-0.8.0}/setup.py +1 -1
  14. {livekit_api-0.7.0 → livekit_api-0.8.0}/tests/test_access_token.py +42 -0
  15. livekit_api-0.7.0/livekit/api/version.py +0 -1
  16. {livekit_api-0.7.0 → livekit_api-0.8.0}/README.md +0 -0
  17. {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit/api/egress_service.py +0 -0
  18. {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit/api/ingress_service.py +0 -0
  19. {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit/api/py.typed +0 -0
  20. {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit/api/room_service.py +0 -0
  21. {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit/api/twirp_client.py +0 -0
  22. {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit_api.egg-info/dependency_links.txt +0 -0
  23. {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit_api.egg-info/top_level.txt +0 -0
  24. {livekit_api-0.7.0 → livekit_api-0.8.0}/pyproject.toml +0 -0
  25. {livekit_api-0.7.0 → livekit_api-0.8.0}/setup.cfg +0 -0
  26. {livekit_api-0.7.0 → livekit_api-0.8.0}/tests/test_webhook.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: livekit-api
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: Python Server API for LiveKit
5
5
  Home-page: https://github.com/livekit/python-sdks
6
6
  License: Apache-2.0
@@ -25,7 +25,7 @@ Requires-Dist: pyjwt>=2.0.0
25
25
  Requires-Dist: aiohttp>=3.9.0
26
26
  Requires-Dist: protobuf>=3
27
27
  Requires-Dist: types-protobuf<5,>=4
28
- Requires-Dist: livekit-protocol<2,>=0.5.1
28
+ Requires-Dist: livekit-protocol<2,>=0.7.0
29
29
 
30
30
  # LiveKit Server APIs
31
31
 
@@ -16,6 +16,8 @@
16
16
 
17
17
  # flake8: noqa
18
18
  # re-export packages from protocol
19
+ from livekit.protocol.agent_dispatch import *
20
+ from livekit.protocol.agent import *
19
21
  from livekit.protocol.egress import *
20
22
  from livekit.protocol.ingress import *
21
23
  from livekit.protocol.models import *
@@ -18,11 +18,13 @@ class Service(ABC):
18
18
  self.api_secret = api_secret
19
19
 
20
20
  def _auth_header(
21
- self, grants: VideoGrants, sip: SIPGrants | None = None
21
+ self, grants: VideoGrants | None, sip: SIPGrants | None = None
22
22
  ) -> Dict[str, str]:
23
- tok = AccessToken(self.api_key, self.api_secret).with_grants(grants)
23
+ tok = AccessToken(self.api_key, self.api_secret)
24
+ if grants:
25
+ tok.with_grants(grants)
24
26
  if sip is not None:
25
- tok = tok.with_sip_grants(sip)
27
+ tok.with_sip_grants(sip)
26
28
 
27
29
  token = tok.to_jwt()
28
30
 
@@ -18,7 +18,10 @@ import re
18
18
  import datetime
19
19
  import os
20
20
  import jwt
21
- from typing import Optional, List
21
+ from typing import Optional, List, Literal
22
+ from google.protobuf.json_format import MessageToDict, ParseDict
23
+
24
+ from livekit.protocol.room import RoomConfiguration
22
25
 
23
26
  DEFAULT_TTL = datetime.timedelta(hours=6)
24
27
  DEFAULT_LEEWAY = datetime.timedelta(minutes=1)
@@ -27,13 +30,13 @@ DEFAULT_LEEWAY = datetime.timedelta(minutes=1)
27
30
  @dataclasses.dataclass
28
31
  class VideoGrants:
29
32
  # actions on rooms
30
- room_create: bool = False
31
- room_list: bool = False
32
- room_record: bool = False
33
+ room_create: Optional[bool] = None
34
+ room_list: Optional[bool] = None
35
+ room_record: Optional[bool] = None
33
36
 
34
37
  # actions on a particular room
35
- room_admin: bool = False
36
- room_join: bool = False
38
+ room_admin: Optional[bool] = None
39
+ room_join: Optional[bool] = None
37
40
  room: str = ""
38
41
 
39
42
  # permissions within a room
@@ -42,25 +45,24 @@ class VideoGrants:
42
45
  can_publish_data: bool = True
43
46
 
44
47
  # TrackSource types that a participant may publish.
45
- # When set, it supercedes CanPublish. Only sources explicitly set here can be
48
+ # When set, it supersedes CanPublish. Only sources explicitly set here can be
46
49
  # published
47
- can_publish_sources: List[str] = dataclasses.field(default_factory=list)
50
+ can_publish_sources: Optional[List[str]] = None
48
51
 
49
52
  # by default, a participant is not allowed to update its own metadata
50
- can_update_own_metadata: bool = False
53
+ can_update_own_metadata: Optional[bool] = None
51
54
 
52
55
  # actions on ingresses
53
- ingress_admin: bool = False # applies to all ingress
56
+ ingress_admin: Optional[bool] = None # applies to all ingress
54
57
 
55
58
  # participant is not visible to other participants (useful when making bots)
56
- hidden: bool = False
59
+ hidden: Optional[bool] = None
57
60
 
58
- # indicates to the room that current participant is a recorder
59
- recorder: bool = False
61
+ # [deprecated] indicates to the room that current participant is a recorder
62
+ recorder: Optional[bool] = None
60
63
 
61
64
  # indicates that the holder can register as an Agent framework worker
62
- # it is also set on all participants that are joining as Agent
63
- agent: bool = False
65
+ agent: Optional[bool] = None
64
66
 
65
67
 
66
68
  @dataclasses.dataclass
@@ -75,14 +77,33 @@ class SIPGrants:
75
77
  class Claims:
76
78
  identity: str = ""
77
79
  name: str = ""
78
- video: VideoGrants = dataclasses.field(default_factory=VideoGrants)
79
- sip: SIPGrants = dataclasses.field(default_factory=SIPGrants)
80
- attributes: dict[str, str] = dataclasses.field(default_factory=dict)
80
+ kind: str = ""
81
81
  metadata: str = ""
82
- sha256: str = ""
82
+ video: Optional[VideoGrants] = None
83
+ sip: Optional[SIPGrants] = None
84
+ attributes: Optional[dict[str, str]] = None
85
+ sha256: Optional[str] = None
86
+ room_preset: Optional[str] = None
87
+ room_config: Optional[RoomConfiguration] = None
88
+
89
+ def asdict(self) -> dict:
90
+ # in order to produce minimal JWT size, exclude None or empty values
91
+ claims = dataclasses.asdict(
92
+ self,
93
+ dict_factory=lambda items: {
94
+ snake_to_lower_camel(k): v
95
+ for k, v in items
96
+ if v is not None and v != ""
97
+ },
98
+ )
99
+ if self.room_config:
100
+ claims["roomConfig"] = MessageToDict(self.room_config)
101
+ return claims
83
102
 
84
103
 
85
104
  class AccessToken:
105
+ ParticipantKind = Literal["standard", "egress", "ingress", "sip", "agent"]
106
+
86
107
  def __init__(
87
108
  self,
88
109
  api_key: Optional[str] = None,
@@ -118,6 +139,10 @@ class AccessToken:
118
139
  self.identity = identity
119
140
  return self
120
141
 
142
+ def with_kind(self, kind: ParticipantKind) -> "AccessToken":
143
+ self.claims.kind = kind
144
+ return self
145
+
121
146
  def with_name(self, name: str) -> "AccessToken":
122
147
  self.claims.name = name
123
148
  return self
@@ -134,26 +159,36 @@ class AccessToken:
134
159
  self.claims.sha256 = sha256
135
160
  return self
136
161
 
162
+ def with_room_preset(self, preset: str) -> "AccessToken":
163
+ self.claims.room_preset = preset
164
+ return self
165
+
166
+ def with_room_config(self, config: RoomConfiguration) -> "AccessToken":
167
+ self.claims.room_config = config
168
+ return self
169
+
137
170
  def to_jwt(self) -> str:
138
171
  video = self.claims.video
139
- if video.room_join and (not self.identity or not video.room):
172
+ if video and video.room_join and (not self.identity or not video.room):
140
173
  raise ValueError("identity and room must be set when joining a room")
141
174
 
142
- claims = dataclasses.asdict(
143
- self.claims,
144
- dict_factory=lambda items: {snake_to_lower_camel(k): v for k, v in items},
145
- )
146
- claims.update(
175
+ # we want to exclude None values from the token
176
+ jwt_claims = self.claims.asdict()
177
+ jwt_claims.update(
147
178
  {
148
179
  "sub": self.identity,
149
180
  "iss": self.api_key,
150
- "nbf": calendar.timegm(datetime.datetime.utcnow().utctimetuple()),
181
+ "nbf": calendar.timegm(
182
+ datetime.datetime.now(datetime.timezone.utc).utctimetuple()
183
+ ),
151
184
  "exp": calendar.timegm(
152
- (datetime.datetime.utcnow() + self.ttl).utctimetuple()
185
+ (
186
+ datetime.datetime.now(datetime.timezone.utc) + self.ttl
187
+ ).utctimetuple()
153
188
  ),
154
189
  }
155
190
  )
156
- return jwt.encode(claims, self.api_secret, algorithm="HS256")
191
+ return jwt.encode(jwt_claims, self.api_secret, algorithm="HS256")
157
192
 
158
193
 
159
194
  class TokenVerifier:
@@ -197,7 +232,7 @@ class TokenVerifier:
197
232
  }
198
233
  sip = SIPGrants(**sip_dict)
199
234
 
200
- return Claims(
235
+ grant_claims = Claims(
201
236
  identity=claims.get("sub", ""),
202
237
  name=claims.get("name", ""),
203
238
  video=video,
@@ -207,6 +242,17 @@ class TokenVerifier:
207
242
  sha256=claims.get("sha256", ""),
208
243
  )
209
244
 
245
+ if claims.get("roomPreset"):
246
+ grant_claims.room_preset = claims.get("roomPreset")
247
+ if claims.get("roomConfig"):
248
+ grant_claims.room_config = ParseDict(
249
+ claims.get("roomConfig"),
250
+ RoomConfiguration(),
251
+ ignore_unknown_fields=True,
252
+ )
253
+
254
+ return grant_claims
255
+
210
256
 
211
257
  def camel_to_snake(t: str):
212
258
  return re.sub(r"(?<!^)(?=[A-Z])", "_", t).lower()
@@ -0,0 +1,108 @@
1
+ import aiohttp
2
+ from typing import Optional
3
+ from livekit.protocol import agent_dispatch as proto_agent_dispatch
4
+ from ._service import Service
5
+ from .access_token import VideoGrants
6
+
7
+ SVC = "AgentDispatchService"
8
+
9
+
10
+ class AgentDispatchService(Service):
11
+ """Manage agent dispatches. Service APIs require roomAdmin permissions.
12
+
13
+ An easier way to construct this service is via LiveKitAPI.agent_dispatch.
14
+ """
15
+
16
+ def __init__(
17
+ self, session: aiohttp.ClientSession, url: str, api_key: str, api_secret: str
18
+ ):
19
+ super().__init__(session, url, api_key, api_secret)
20
+
21
+ async def create_dispatch(
22
+ self, req: proto_agent_dispatch.CreateAgentDispatchRequest
23
+ ) -> proto_agent_dispatch.AgentDispatch:
24
+ """Create an explicit dispatch for an agent to join a room.
25
+
26
+ To use explicit dispatch, your agent must be registered with an `agentName`.
27
+
28
+ Args:
29
+ req (CreateAgentDispatchRequest): Request containing dispatch creation parameters
30
+
31
+ Returns:
32
+ AgentDispatch: The created agent dispatch object
33
+ """
34
+ return await self._client.request(
35
+ SVC,
36
+ "CreateDispatch",
37
+ req,
38
+ self._auth_header(VideoGrants(room_admin=True, room=req.room)),
39
+ proto_agent_dispatch.AgentDispatch,
40
+ )
41
+
42
+ async def delete_dispatch(
43
+ self, dispatch_id: str, room_name: str
44
+ ) -> proto_agent_dispatch.AgentDispatch:
45
+ """Delete an explicit dispatch for an agent in a room.
46
+
47
+ Args:
48
+ dispatch_id (str): ID of the dispatch to delete
49
+ room_name (str): Name of the room containing the dispatch
50
+
51
+ Returns:
52
+ AgentDispatch: The deleted agent dispatch object
53
+ """
54
+ return await self._client.request(
55
+ SVC,
56
+ "DeleteDispatch",
57
+ proto_agent_dispatch.DeleteAgentDispatchRequest(
58
+ dispatch_id=dispatch_id,
59
+ room=room_name,
60
+ ),
61
+ self._auth_header(VideoGrants(room_admin=True, room=room_name)),
62
+ proto_agent_dispatch.AgentDispatch,
63
+ )
64
+
65
+ async def list_dispatch(
66
+ self, room_name: str
67
+ ) -> list[proto_agent_dispatch.AgentDispatch]:
68
+ """List all agent dispatches in a room.
69
+
70
+ Args:
71
+ room_name (str): Name of the room to list dispatches from
72
+
73
+ Returns:
74
+ list[AgentDispatch]: List of agent dispatch objects in the room
75
+ """
76
+ res = await self._client.request(
77
+ SVC,
78
+ "ListDispatch",
79
+ proto_agent_dispatch.ListAgentDispatchRequest(room=room_name),
80
+ self._auth_header(VideoGrants(room_admin=True, room=room_name)),
81
+ proto_agent_dispatch.ListAgentDispatchResponse,
82
+ )
83
+ return list(res.agent_dispatches)
84
+
85
+ async def get_dispatch(
86
+ self, dispatch_id: str, room_name: str
87
+ ) -> Optional[proto_agent_dispatch.AgentDispatch]:
88
+ """Get an Agent dispatch by ID
89
+
90
+ Args:
91
+ dispatch_id (str): ID of the dispatch to retrieve
92
+ room_name (str): Name of the room containing the dispatch
93
+
94
+ Returns:
95
+ Optional[AgentDispatch]: The requested agent dispatch object if found, None otherwise
96
+ """
97
+ res = await self._client.request(
98
+ SVC,
99
+ "ListDispatch",
100
+ proto_agent_dispatch.ListAgentDispatchRequest(
101
+ dispatch_id=dispatch_id, room=room_name
102
+ ),
103
+ self._auth_header(VideoGrants(room_admin=True, room=room_name)),
104
+ proto_agent_dispatch.ListAgentDispatchResponse,
105
+ )
106
+ if len(res.agent_dispatches) > 0:
107
+ return res.agent_dispatches[0]
108
+ return None
@@ -4,6 +4,7 @@ from .room_service import RoomService
4
4
  from .egress_service import EgressService
5
5
  from .ingress_service import IngressService
6
6
  from .sip_service import SipService
7
+ from .agent_dispatch_service import AgentDispatchService
7
8
  from typing import Optional
8
9
 
9
10
 
@@ -31,6 +32,13 @@ class LiveKitAPI:
31
32
  self._ingress = IngressService(self._session, url, api_key, api_secret)
32
33
  self._egress = EgressService(self._session, url, api_key, api_secret)
33
34
  self._sip = SipService(self._session, url, api_key, api_secret)
35
+ self._agent_dispatch = AgentDispatchService(
36
+ self._session, url, api_key, api_secret
37
+ )
38
+
39
+ @property
40
+ def agent_dispatch(self):
41
+ return self._agent_dispatch
34
42
 
35
43
  @property
36
44
  def room(self):
@@ -132,3 +132,20 @@ class SipService(Service):
132
132
  self._auth_header(VideoGrants(), sip=SIPGrants(call=True)),
133
133
  proto_sip.SIPParticipantInfo,
134
134
  )
135
+
136
+ async def transfer_sip_participant(
137
+ self, transfer: proto_sip.TransferSIPParticipantRequest
138
+ ) -> proto_sip.SIPParticipantInfo:
139
+ return await self._client.request(
140
+ SVC,
141
+ "TransferSIPParticipant",
142
+ transfer,
143
+ self._auth_header(
144
+ VideoGrants(
145
+ room_admin=True,
146
+ room=transfer.room_name,
147
+ ),
148
+ sip=SIPGrants(call=True),
149
+ ),
150
+ proto_sip.SIPParticipantInfo,
151
+ )
@@ -0,0 +1 @@
1
+ __version__ = "0.8.0"
@@ -11,6 +11,8 @@ class WebhookReceiver:
11
11
 
12
12
  def receive(self, body: str, auth_token: str) -> proto_webhook.WebhookEvent:
13
13
  claims = self._verifier.verify(auth_token)
14
+ if claims.sha256 is None:
15
+ raise Exception("sha256 was not found in the token")
14
16
 
15
17
  body_hash = hashlib.sha256(body.encode()).digest()
16
18
  claims_hash = base64.b64decode(claims.sha256)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: livekit-api
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: Python Server API for LiveKit
5
5
  Home-page: https://github.com/livekit/python-sdks
6
6
  License: Apache-2.0
@@ -25,7 +25,7 @@ Requires-Dist: pyjwt>=2.0.0
25
25
  Requires-Dist: aiohttp>=3.9.0
26
26
  Requires-Dist: protobuf>=3
27
27
  Requires-Dist: types-protobuf<5,>=4
28
- Requires-Dist: livekit-protocol<2,>=0.5.1
28
+ Requires-Dist: livekit-protocol<2,>=0.7.0
29
29
 
30
30
  # LiveKit Server APIs
31
31
 
@@ -4,6 +4,7 @@ setup.py
4
4
  livekit/api/__init__.py
5
5
  livekit/api/_service.py
6
6
  livekit/api/access_token.py
7
+ livekit/api/agent_dispatch_service.py
7
8
  livekit/api/egress_service.py
8
9
  livekit/api/ingress_service.py
9
10
  livekit/api/livekit_api.py
@@ -2,4 +2,4 @@ pyjwt>=2.0.0
2
2
  aiohttp>=3.9.0
3
3
  protobuf>=3
4
4
  types-protobuf<5,>=4
5
- livekit-protocol<2,>=0.5.1
5
+ livekit-protocol<2,>=0.7.0
@@ -53,7 +53,7 @@ setuptools.setup(
53
53
  "aiohttp>=3.9.0",
54
54
  "protobuf>=3",
55
55
  "types-protobuf>=4,<5",
56
- "livekit-protocol>=0.5.1,<2",
56
+ "livekit-protocol>=0.7.0,<2",
57
57
  ],
58
58
  package_data={
59
59
  "livekit.api": ["py.typed", "*.pyi", "**/*.pyi"],
@@ -2,6 +2,8 @@ import datetime
2
2
 
3
3
  import pytest # type: ignore
4
4
  from livekit.api import AccessToken, TokenVerifier, VideoGrants, SIPGrants
5
+ from livekit.protocol.room import RoomConfiguration
6
+ from livekit.protocol.agent_dispatch import RoomAgentDispatch
5
7
 
6
8
  TEST_API_KEY = "myapikey"
7
9
  TEST_API_SECRET = "thiskeyistotallyunsafe"
@@ -32,6 +34,46 @@ def test_verify_token():
32
34
  assert claims.attributes["key2"] == "value2"
33
35
 
34
36
 
37
+ def test_agent_config():
38
+ token = (
39
+ AccessToken(TEST_API_KEY, TEST_API_SECRET)
40
+ .with_identity("test_identity")
41
+ .with_grants(VideoGrants(room_join=True, room="test_room"))
42
+ .with_room_config(
43
+ RoomConfiguration(
44
+ agents=[RoomAgentDispatch(agent_name="test-agent")],
45
+ ),
46
+ )
47
+ .to_jwt()
48
+ )
49
+
50
+ token_verifier = TokenVerifier(TEST_API_KEY, TEST_API_SECRET)
51
+ claims = token_verifier.verify(token)
52
+ # Verify the decoded claims match
53
+ assert claims.room_config.agents[0].agent_name == "test-agent"
54
+
55
+ # Split token into header.payload.signature
56
+ parts = token.split(".")
57
+ import base64
58
+ import json
59
+
60
+ # Decode the payload (middle part)
61
+ payload = parts[1]
62
+ # Add padding if needed
63
+ padding = len(payload) % 4
64
+ if padding:
65
+ payload += "=" * (4 - padding)
66
+ decoded = base64.b64decode(payload)
67
+ payload_json = json.loads(decoded)
68
+ print(decoded)
69
+
70
+ # Verify the room_config and agents were encoded correctly
71
+ assert "roomConfig" in payload_json
72
+ assert "agents" in payload_json["roomConfig"]
73
+ assert len(payload_json["roomConfig"]["agents"]) == 1
74
+ assert payload_json["roomConfig"]["agents"][0]["agentName"] == "test-agent"
75
+
76
+
35
77
  def test_verify_token_invalid():
36
78
  token = (
37
79
  AccessToken(TEST_API_KEY, TEST_API_SECRET)
@@ -1 +0,0 @@
1
- __version__ = "0.7.0"
File without changes
File without changes
File without changes