livekit-api 0.8.2__tar.gz → 1.0.1__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 (28) hide show
  1. {livekit_api-0.8.2 → livekit_api-1.0.1}/PKG-INFO +5 -5
  2. {livekit_api-0.8.2 → livekit_api-1.0.1}/livekit/api/__init__.py +2 -0
  3. {livekit_api-0.8.2 → livekit_api-1.0.1}/livekit/api/_service.py +2 -5
  4. {livekit_api-0.8.2 → livekit_api-1.0.1}/livekit/api/access_token.py +6 -18
  5. {livekit_api-0.8.2 → livekit_api-1.0.1}/livekit/api/agent_dispatch_service.py +2 -6
  6. {livekit_api-0.8.2 → livekit_api-1.0.1}/livekit/api/egress_service.py +20 -12
  7. {livekit_api-0.8.2 → livekit_api-1.0.1}/livekit/api/ingress_service.py +1 -3
  8. {livekit_api-0.8.2 → livekit_api-1.0.1}/livekit/api/livekit_api.py +15 -6
  9. {livekit_api-0.8.2 → livekit_api-1.0.1}/livekit/api/room_service.py +25 -9
  10. livekit_api-1.0.1/livekit/api/sip_service.py +447 -0
  11. {livekit_api-0.8.2 → livekit_api-1.0.1}/livekit/api/twirp_client.py +37 -4
  12. livekit_api-1.0.1/livekit/api/version.py +1 -0
  13. {livekit_api-0.8.2 → livekit_api-1.0.1}/livekit_api.egg-info/PKG-INFO +5 -5
  14. livekit_api-1.0.1/livekit_api.egg-info/requires.txt +5 -0
  15. {livekit_api-0.8.2 → livekit_api-1.0.1}/setup.py +3 -3
  16. {livekit_api-0.8.2 → livekit_api-1.0.1}/tests/test_access_token.py +1 -5
  17. livekit_api-0.8.2/livekit/api/sip_service.py +0 -182
  18. livekit_api-0.8.2/livekit/api/version.py +0 -1
  19. livekit_api-0.8.2/livekit_api.egg-info/requires.txt +0 -5
  20. {livekit_api-0.8.2 → livekit_api-1.0.1}/README.md +0 -0
  21. {livekit_api-0.8.2 → livekit_api-1.0.1}/livekit/api/py.typed +0 -0
  22. {livekit_api-0.8.2 → livekit_api-1.0.1}/livekit/api/webhook.py +0 -0
  23. {livekit_api-0.8.2 → livekit_api-1.0.1}/livekit_api.egg-info/SOURCES.txt +0 -0
  24. {livekit_api-0.8.2 → livekit_api-1.0.1}/livekit_api.egg-info/dependency_links.txt +0 -0
  25. {livekit_api-0.8.2 → livekit_api-1.0.1}/livekit_api.egg-info/top_level.txt +0 -0
  26. {livekit_api-0.8.2 → livekit_api-1.0.1}/pyproject.toml +0 -0
  27. {livekit_api-0.8.2 → livekit_api-1.0.1}/setup.cfg +0 -0
  28. {livekit_api-0.8.2 → livekit_api-1.0.1}/tests/test_webhook.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: livekit-api
3
- Version: 0.8.2
3
+ Version: 1.0.1
4
4
  Summary: Python Server API for LiveKit
5
5
  Home-page: https://github.com/livekit/python-sdks
6
6
  License: Apache-2.0
@@ -23,9 +23,9 @@ Requires-Python: >=3.9.0
23
23
  Description-Content-Type: text/markdown
24
24
  Requires-Dist: pyjwt>=2.0.0
25
25
  Requires-Dist: aiohttp>=3.9.0
26
- Requires-Dist: protobuf>=3
27
- Requires-Dist: types-protobuf<5,>=4
28
- Requires-Dist: livekit-protocol<2,>=0.8.2
26
+ Requires-Dist: protobuf>=4
27
+ Requires-Dist: types-protobuf>=4
28
+ Requires-Dist: livekit-protocol~=1.0
29
29
  Dynamic: classifier
30
30
  Dynamic: description
31
31
  Dynamic: description-content-type
@@ -52,4 +52,6 @@ __all__ = [
52
52
  "AccessToken",
53
53
  "TokenVerifier",
54
54
  "WebhookReceiver",
55
+ "TwirpError",
56
+ "TwirpErrorCode",
55
57
  ]
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Dict
4
3
  import aiohttp
5
4
  from abc import ABC
6
5
  from .twirp_client import TwirpClient
@@ -10,16 +9,14 @@ AUTHORIZATION = "authorization"
10
9
 
11
10
 
12
11
  class Service(ABC):
13
- def __init__(
14
- self, session: aiohttp.ClientSession, host: str, api_key: str, api_secret: str
15
- ):
12
+ def __init__(self, session: aiohttp.ClientSession, host: str, api_key: str, api_secret: str):
16
13
  self._client = TwirpClient(session, host, "livekit")
17
14
  self.api_key = api_key
18
15
  self.api_secret = api_secret
19
16
 
20
17
  def _auth_header(
21
18
  self, grants: VideoGrants | None, sip: SIPGrants | None = None
22
- ) -> Dict[str, str]:
19
+ ) -> dict[str, str]:
23
20
  tok = AccessToken(self.api_key, self.api_secret)
24
21
  if grants:
25
22
  tok.with_grants(grants)
@@ -91,9 +91,7 @@ class Claims:
91
91
  claims = dataclasses.asdict(
92
92
  self,
93
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 != ""
94
+ snake_to_lower_camel(k): v for k, v in items if v is not None and v != ""
97
95
  },
98
96
  )
99
97
  if self.room_config:
@@ -178,13 +176,9 @@ class AccessToken:
178
176
  {
179
177
  "sub": self.identity,
180
178
  "iss": self.api_key,
181
- "nbf": calendar.timegm(
182
- datetime.datetime.now(datetime.timezone.utc).utctimetuple()
183
- ),
179
+ "nbf": calendar.timegm(datetime.datetime.now(datetime.timezone.utc).utctimetuple()),
184
180
  "exp": calendar.timegm(
185
- (
186
- datetime.datetime.now(datetime.timezone.utc) + self.ttl
187
- ).utctimetuple()
181
+ (datetime.datetime.now(datetime.timezone.utc) + self.ttl).utctimetuple()
188
182
  ),
189
183
  }
190
184
  )
@@ -220,16 +214,12 @@ class TokenVerifier:
220
214
 
221
215
  video_dict = claims.get("video", dict())
222
216
  video_dict = {camel_to_snake(k): v for k, v in video_dict.items()}
223
- video_dict = {
224
- k: v for k, v in video_dict.items() if k in VideoGrants.__dataclass_fields__
225
- }
217
+ video_dict = {k: v for k, v in video_dict.items() if k in VideoGrants.__dataclass_fields__}
226
218
  video = VideoGrants(**video_dict)
227
219
 
228
220
  sip_dict = claims.get("sip", dict())
229
221
  sip_dict = {camel_to_snake(k): v for k, v in sip_dict.items()}
230
- sip_dict = {
231
- k: v for k, v in sip_dict.items() if k in SIPGrants.__dataclass_fields__
232
- }
222
+ sip_dict = {k: v for k, v in sip_dict.items() if k in SIPGrants.__dataclass_fields__}
233
223
  sip = SIPGrants(**sip_dict)
234
224
 
235
225
  grant_claims = Claims(
@@ -259,6 +249,4 @@ def camel_to_snake(t: str):
259
249
 
260
250
 
261
251
  def snake_to_lower_camel(t: str):
262
- return "".join(
263
- word.capitalize() if i else word for i, word in enumerate(t.split("_"))
264
- )
252
+ return "".join(word.capitalize() if i else word for i, word in enumerate(t.split("_")))
@@ -26,9 +26,7 @@ class AgentDispatchService(Service):
26
26
  ```
27
27
  """
28
28
 
29
- def __init__(
30
- self, session: aiohttp.ClientSession, url: str, api_key: str, api_secret: str
31
- ):
29
+ def __init__(self, session: aiohttp.ClientSession, url: str, api_key: str, api_secret: str):
32
30
  super().__init__(session, url, api_key, api_secret)
33
31
 
34
32
  async def create_dispatch(self, req: CreateAgentDispatchRequest) -> AgentDispatch:
@@ -89,9 +87,7 @@ class AgentDispatchService(Service):
89
87
  )
90
88
  return list(res.agent_dispatches)
91
89
 
92
- async def get_dispatch(
93
- self, dispatch_id: str, room_name: str
94
- ) -> Optional[AgentDispatch]:
90
+ async def get_dispatch(self, dispatch_id: str, room_name: str) -> Optional[AgentDispatch]:
95
91
  """Get an Agent dispatch by ID
96
92
 
97
93
  Args:
@@ -33,14 +33,11 @@ class EgressService(Service):
33
33
  Also see https://docs.livekit.io/home/egress/overview/
34
34
  """
35
35
 
36
- def __init__(
37
- self, session: aiohttp.ClientSession, url: str, api_key: str, api_secret: str
38
- ):
36
+ def __init__(self, session: aiohttp.ClientSession, url: str, api_key: str, api_secret: str):
39
37
  super().__init__(session, url, api_key, api_secret)
40
38
 
41
- async def start_room_composite_egress(
42
- self, start: RoomCompositeEgressRequest
43
- ) -> EgressInfo:
39
+ async def start_room_composite_egress(self, start: RoomCompositeEgressRequest) -> EgressInfo:
40
+ """Starts a composite recording of a room."""
44
41
  return await self._client.request(
45
42
  SVC,
46
43
  "StartRoomCompositeEgress",
@@ -50,6 +47,7 @@ class EgressService(Service):
50
47
  )
51
48
 
52
49
  async def start_web_egress(self, start: WebEgressRequest) -> EgressInfo:
50
+ """Starts a recording of a web page."""
53
51
  return await self._client.request(
54
52
  SVC,
55
53
  "StartWebEgress",
@@ -58,9 +56,8 @@ class EgressService(Service):
58
56
  EgressInfo,
59
57
  )
60
58
 
61
- async def start_participant_egress(
62
- self, start: ParticipantEgressRequest
63
- ) -> EgressInfo:
59
+ async def start_participant_egress(self, start: ParticipantEgressRequest) -> EgressInfo:
60
+ """Starts a recording of a participant."""
64
61
  return await self._client.request(
65
62
  SVC,
66
63
  "StartParticipantEgress",
@@ -69,9 +66,8 @@ class EgressService(Service):
69
66
  EgressInfo,
70
67
  )
71
68
 
72
- async def start_track_composite_egress(
73
- self, start: TrackCompositeEgressRequest
74
- ) -> EgressInfo:
69
+ async def start_track_composite_egress(self, start: TrackCompositeEgressRequest) -> EgressInfo:
70
+ """Starts a composite recording with audio and video tracks."""
75
71
  return await self._client.request(
76
72
  SVC,
77
73
  "StartTrackCompositeEgress",
@@ -81,6 +77,7 @@ class EgressService(Service):
81
77
  )
82
78
 
83
79
  async def start_track_egress(self, start: TrackEgressRequest) -> EgressInfo:
80
+ """Starts a recording of a single track."""
84
81
  return await self._client.request(
85
82
  SVC,
86
83
  "StartTrackEgress",
@@ -90,6 +87,7 @@ class EgressService(Service):
90
87
  )
91
88
 
92
89
  async def update_layout(self, update: UpdateLayoutRequest) -> EgressInfo:
90
+ """Updates the layout of a composite recording."""
93
91
  return await self._client.request(
94
92
  SVC,
95
93
  "UpdateLayout",
@@ -99,6 +97,7 @@ class EgressService(Service):
99
97
  )
100
98
 
101
99
  async def update_stream(self, update: UpdateStreamRequest) -> EgressInfo:
100
+ """Updates the stream of a RoomComposite, Web, or Participant recording."""
102
101
  return await self._client.request(
103
102
  SVC,
104
103
  "UpdateStream",
@@ -108,6 +107,14 @@ class EgressService(Service):
108
107
  )
109
108
 
110
109
  async def list_egress(self, list: ListEgressRequest) -> ListEgressResponse:
110
+ """Lists all active egress and recently completed recordings.
111
+
112
+ Args:
113
+ list (ListEgressRequest): arg contains optional filters:
114
+ - room_name: str - List all egresses for a specific room
115
+ - egress_id: str - Only list egress with matching ID
116
+ - active: bool - Only list active egresses
117
+ """
111
118
  return await self._client.request(
112
119
  SVC,
113
120
  "ListEgress",
@@ -117,6 +124,7 @@ class EgressService(Service):
117
124
  )
118
125
 
119
126
  async def stop_egress(self, stop: StopEgressRequest) -> EgressInfo:
127
+ """Stops an active egress recording."""
120
128
  return await self._client.request(
121
129
  SVC,
122
130
  "StopEgress",
@@ -28,9 +28,7 @@ class IngressService(Service):
28
28
  Also see https://docs.livekit.io/home/ingress/overview/
29
29
  """
30
30
 
31
- def __init__(
32
- self, session: aiohttp.ClientSession, url: str, api_key: str, api_secret: str
33
- ):
31
+ def __init__(self, session: aiohttp.ClientSession, url: str, api_key: str, api_secret: str):
34
32
  super().__init__(session, url, api_key, api_secret)
35
33
 
36
34
  async def create_ingress(self, create: CreateIngressRequest) -> IngressInfo:
@@ -28,7 +28,8 @@ class LiveKitAPI:
28
28
  api_key: Optional[str] = None,
29
29
  api_secret: Optional[str] = None,
30
30
  *,
31
- timeout: aiohttp.ClientTimeout = aiohttp.ClientTimeout(total=60), # 60 seconds
31
+ timeout: Optional[aiohttp.ClientTimeout] = None,
32
+ session: Optional[aiohttp.ClientSession] = None,
32
33
  ):
33
34
  """Create a new LiveKitAPI instance.
34
35
 
@@ -37,6 +38,7 @@ class LiveKitAPI:
37
38
  api_key: API key (read from `LIVEKIT_API_KEY` environment variable if not provided)
38
39
  api_secret: API secret (read from `LIVEKIT_API_SECRET` environment variable if not provided)
39
40
  timeout: Request timeout (default: 60 seconds)
41
+ session: aiohttp.ClientSession instance to use for requests, if not provided, a new one will be created
40
42
  """
41
43
  url = url or os.getenv("LIVEKIT_URL")
42
44
  api_key = api_key or os.getenv("LIVEKIT_API_KEY")
@@ -48,14 +50,19 @@ class LiveKitAPI:
48
50
  if not api_key or not api_secret:
49
51
  raise ValueError("api_key and api_secret must be set")
50
52
 
51
- self._session = aiohttp.ClientSession(timeout=timeout)
53
+ self._custom_session = True
54
+ self._session = session
55
+ if not self._session:
56
+ self._custom_session = False
57
+ if not timeout:
58
+ timeout = aiohttp.ClientTimeout(total=60)
59
+ self._session = aiohttp.ClientSession(timeout=timeout)
60
+
52
61
  self._room = RoomService(self._session, url, api_key, api_secret)
53
62
  self._ingress = IngressService(self._session, url, api_key, api_secret)
54
63
  self._egress = EgressService(self._session, url, api_key, api_secret)
55
64
  self._sip = SipService(self._session, url, api_key, api_secret)
56
- self._agent_dispatch = AgentDispatchService(
57
- self._session, url, api_key, api_secret
58
- )
65
+ self._agent_dispatch = AgentDispatchService(self._session, url, api_key, api_secret)
59
66
 
60
67
  @property
61
68
  def agent_dispatch(self) -> AgentDispatchService:
@@ -86,7 +93,9 @@ class LiveKitAPI:
86
93
  """Close the API client
87
94
 
88
95
  Call this before your application exits or when the API client is no longer needed."""
89
- await self._session.close()
96
+ # we do not close custom sessions, that's up to the caller
97
+ if not self._custom_session:
98
+ await self._session.close()
90
99
 
91
100
  async def __aenter__(self):
92
101
  """@private
@@ -18,6 +18,8 @@ from livekit.protocol.room import (
18
18
  UpdateRoomMetadataRequest,
19
19
  RemoveParticipantResponse,
20
20
  UpdateSubscriptionsResponse,
21
+ ForwardParticipantRequest,
22
+ ForwardParticipantResponse,
21
23
  )
22
24
  from livekit.protocol.models import Room, ParticipantInfo
23
25
  from ._service import Service
@@ -41,9 +43,7 @@ class RoomService(Service):
41
43
  Also see https://docs.livekit.io/home/server/managing-rooms/ and https://docs.livekit.io/home/server/managing-participants/
42
44
  """
43
45
 
44
- def __init__(
45
- self, session: aiohttp.ClientSession, url: str, api_key: str, api_secret: str
46
- ):
46
+ def __init__(self, session: aiohttp.ClientSession, url: str, api_key: str, api_secret: str):
47
47
  super().__init__(session, url, api_key, api_secret)
48
48
 
49
49
  async def create_room(
@@ -130,9 +130,7 @@ class RoomService(Service):
130
130
  Room,
131
131
  )
132
132
 
133
- async def list_participants(
134
- self, list: ListParticipantsRequest
135
- ) -> ListParticipantsResponse:
133
+ async def list_participants(self, list: ListParticipantsRequest) -> ListParticipantsResponse:
136
134
  """Lists all participants in a room.
137
135
 
138
136
  Args:
@@ -201,6 +199,26 @@ class RoomService(Service):
201
199
  RemoveParticipantResponse,
202
200
  )
203
201
 
202
+ async def forward_participant(self, forward: ForwardParticipantRequest) -> None:
203
+ """Forwards a participant and their published tracks from one room to another.
204
+
205
+ This feature is only available for LiveKit Cloud/Private Cloud.
206
+
207
+ Args:
208
+ forward (ForwardParticipantRequest): arg containing:
209
+ - room: str - Room name
210
+ - identity: str - identity of Participant to forward
211
+ - destination_room: str - Destination room name
212
+ """
213
+ # currently nothing is returned
214
+ await self._client.request(
215
+ SVC,
216
+ "ForwardParticipant",
217
+ forward,
218
+ self._auth_header(VideoGrants(room_admin=True, room=forward.room)),
219
+ ForwardParticipantResponse,
220
+ )
221
+
204
222
  async def mute_published_track(
205
223
  self,
206
224
  update: MuteRoomTrackRequest,
@@ -226,9 +244,7 @@ class RoomService(Service):
226
244
  MuteRoomTrackResponse,
227
245
  )
228
246
 
229
- async def update_participant(
230
- self, update: UpdateParticipantRequest
231
- ) -> ParticipantInfo:
247
+ async def update_participant(self, update: UpdateParticipantRequest) -> ParticipantInfo:
232
248
  """Updates a participant's metadata or permissions.
233
249
 
234
250
  Args:
@@ -0,0 +1,447 @@
1
+ import aiohttp
2
+ from typing import Optional
3
+
4
+ from livekit.protocol.models import ListUpdate
5
+ from livekit.protocol.sip import (
6
+ SIPTrunkInfo,
7
+ CreateSIPInboundTrunkRequest,
8
+ UpdateSIPInboundTrunkRequest,
9
+ SIPInboundTrunkInfo,
10
+ SIPInboundTrunkUpdate,
11
+ CreateSIPOutboundTrunkRequest,
12
+ UpdateSIPOutboundTrunkRequest,
13
+ SIPOutboundTrunkInfo,
14
+ SIPOutboundTrunkUpdate,
15
+ ListSIPInboundTrunkRequest,
16
+ ListSIPInboundTrunkResponse,
17
+ ListSIPOutboundTrunkRequest,
18
+ ListSIPOutboundTrunkResponse,
19
+ DeleteSIPTrunkRequest,
20
+ SIPDispatchRule,
21
+ SIPDispatchRuleInfo,
22
+ SIPDispatchRuleUpdate,
23
+ CreateSIPDispatchRuleRequest,
24
+ UpdateSIPDispatchRuleRequest,
25
+ ListSIPDispatchRuleRequest,
26
+ ListSIPDispatchRuleResponse,
27
+ DeleteSIPDispatchRuleRequest,
28
+ CreateSIPParticipantRequest,
29
+ TransferSIPParticipantRequest,
30
+ SIPParticipantInfo,
31
+ SIPTransport,
32
+ )
33
+ from ._service import Service
34
+ from .access_token import VideoGrants, SIPGrants
35
+
36
+ SVC = "SIP"
37
+ """@private"""
38
+
39
+
40
+ class SipService(Service):
41
+ """Client for LiveKit SIP Service API
42
+
43
+ Recommended way to use this service is via `livekit.api.LiveKitAPI`:
44
+
45
+ ```python
46
+ from livekit import api
47
+ lkapi = api.LiveKitAPI()
48
+ sip_service = lkapi.sip
49
+ ```
50
+ """
51
+
52
+ def __init__(self, session: aiohttp.ClientSession, url: str, api_key: str, api_secret: str):
53
+ super().__init__(session, url, api_key, api_secret)
54
+
55
+ async def create_sip_inbound_trunk(
56
+ self, create: CreateSIPInboundTrunkRequest
57
+ ) -> SIPInboundTrunkInfo:
58
+ """Create a new SIP inbound trunk.
59
+
60
+ Args:
61
+ create: Request containing trunk details
62
+
63
+ Returns:
64
+ Created SIP inbound trunk
65
+ """
66
+ return await self._client.request(
67
+ SVC,
68
+ "CreateSIPInboundTrunk",
69
+ create,
70
+ self._admin_headers(),
71
+ SIPInboundTrunkInfo,
72
+ )
73
+
74
+ async def update_sip_inbound_trunk(
75
+ self,
76
+ trunk_id: str,
77
+ trunk: SIPInboundTrunkInfo,
78
+ ) -> SIPInboundTrunkInfo:
79
+ """Updates an existing SIP inbound trunk by replacing it entirely.
80
+
81
+ Args:
82
+ trunk_id: ID of the SIP inbound trunk to update
83
+ trunk: SIP inbound trunk to update with
84
+
85
+ Returns:
86
+ Updated SIP inbound trunk
87
+ """
88
+ return await self._client.request(
89
+ SVC,
90
+ "UpdateSIPInboundTrunk",
91
+ UpdateSIPInboundTrunkRequest(
92
+ sip_trunk_id=trunk_id,
93
+ replace=trunk,
94
+ ),
95
+ self._admin_headers(),
96
+ SIPInboundTrunkInfo,
97
+ )
98
+
99
+ async def update_sip_inbound_trunk_fields(
100
+ self,
101
+ trunk_id: str,
102
+ *,
103
+ numbers: Optional[list[str]] = None,
104
+ allowed_addresses: Optional[list[str]] = None,
105
+ allowed_numbers: Optional[list[str]] = None,
106
+ auth_username: Optional[str] = None,
107
+ auth_password: Optional[str] = None,
108
+ name: Optional[str] = None,
109
+ metadata: Optional[str] = None,
110
+ ) -> SIPInboundTrunkInfo:
111
+ """Updates specific fields of an existing SIP inbound trunk.
112
+
113
+ Only provided fields will be updated.
114
+ """
115
+ update = SIPInboundTrunkUpdate(
116
+ auth_username=auth_username,
117
+ auth_password=auth_password,
118
+ name=name,
119
+ metadata=metadata,
120
+ )
121
+ if numbers is not None:
122
+ update.numbers = ListUpdate(set=numbers)
123
+ if allowed_addresses is not None:
124
+ update.allowed_addresses = ListUpdate(set=allowed_addresses)
125
+ if allowed_numbers is not None:
126
+ update.allowed_numbers = ListUpdate(set=allowed_numbers)
127
+
128
+ return await self._client.request(
129
+ SVC,
130
+ "UpdateSIPInboundTrunk",
131
+ UpdateSIPInboundTrunkRequest(
132
+ sip_trunk_id=trunk_id,
133
+ update=update,
134
+ ),
135
+ self._admin_headers(),
136
+ SIPInboundTrunkInfo,
137
+ )
138
+
139
+ async def create_sip_outbound_trunk(
140
+ self, create: CreateSIPOutboundTrunkRequest
141
+ ) -> SIPOutboundTrunkInfo:
142
+ """Create a new SIP outbound trunk.
143
+
144
+ Args:
145
+ create: Request containing trunk details
146
+
147
+ Returns:
148
+ Created SIP outbound trunk
149
+ """
150
+ return await self._client.request(
151
+ SVC,
152
+ "CreateSIPOutboundTrunk",
153
+ create,
154
+ self._admin_headers(),
155
+ SIPOutboundTrunkInfo,
156
+ )
157
+
158
+ async def update_sip_outbound_trunk(
159
+ self,
160
+ trunk_id: str,
161
+ trunk: SIPOutboundTrunkInfo,
162
+ ) -> SIPOutboundTrunkInfo:
163
+ """Updates an existing SIP outbound trunk by replacing it entirely.
164
+
165
+ Args:
166
+ trunk_id: ID of the SIP outbound trunk to update
167
+ trunk: SIP outbound trunk to update with
168
+
169
+ Returns:
170
+ Updated SIP outbound trunk
171
+ """
172
+ return await self._client.request(
173
+ SVC,
174
+ "UpdateSIPOutboundTrunk",
175
+ UpdateSIPOutboundTrunkRequest(
176
+ sip_trunk_id=trunk_id,
177
+ replace=trunk,
178
+ ),
179
+ self._admin_headers(),
180
+ SIPOutboundTrunkInfo,
181
+ )
182
+
183
+ async def update_sip_outbound_trunk_fields(
184
+ self,
185
+ trunk_id: str,
186
+ *,
187
+ address: Optional[str] = None,
188
+ transport: Optional[SIPTransport] = None,
189
+ numbers: Optional[list[str]] = None,
190
+ auth_username: Optional[str] = None,
191
+ auth_password: Optional[str] = None,
192
+ name: Optional[str] = None,
193
+ metadata: Optional[str] = None,
194
+ ) -> SIPOutboundTrunkInfo:
195
+ """Updates specific fields of an existing SIP outbound trunk.
196
+
197
+ Only provided fields will be updated.
198
+ """
199
+ update = SIPOutboundTrunkUpdate(
200
+ address=address,
201
+ transport=transport,
202
+ auth_username=auth_username,
203
+ auth_password=auth_password,
204
+ name=name,
205
+ metadata=metadata,
206
+ )
207
+ if numbers is not None:
208
+ update.numbers = ListUpdate(set=numbers)
209
+ return await self._client.request(
210
+ SVC,
211
+ "UpdateSIPOutboundTrunk",
212
+ UpdateSIPOutboundTrunkRequest(
213
+ sip_trunk_id=trunk_id,
214
+ update=update,
215
+ ),
216
+ self._admin_headers(),
217
+ SIPOutboundTrunkInfo,
218
+ )
219
+
220
+ async def list_sip_inbound_trunk(
221
+ self, list: ListSIPInboundTrunkRequest
222
+ ) -> ListSIPInboundTrunkResponse:
223
+ """List SIP inbound trunks with optional filtering.
224
+
225
+ Args:
226
+ list: Request with optional filtering parameters
227
+
228
+ Returns:
229
+ Response containing list of SIP inbound trunks
230
+ """
231
+ return await self._client.request(
232
+ SVC,
233
+ "ListSIPInboundTrunk",
234
+ list,
235
+ self._admin_headers(),
236
+ ListSIPInboundTrunkResponse,
237
+ )
238
+
239
+ async def list_sip_outbound_trunk(
240
+ self, list: ListSIPOutboundTrunkRequest
241
+ ) -> ListSIPOutboundTrunkResponse:
242
+ """List SIP outbound trunks with optional filtering.
243
+
244
+ Args:
245
+ list: Request with optional filtering parameters
246
+
247
+ Returns:
248
+ Response containing list of SIP outbound trunks
249
+ """
250
+ return await self._client.request(
251
+ SVC,
252
+ "ListSIPOutboundTrunk",
253
+ list,
254
+ self._admin_headers(),
255
+ ListSIPOutboundTrunkResponse,
256
+ )
257
+
258
+ async def delete_sip_trunk(self, delete: DeleteSIPTrunkRequest) -> SIPTrunkInfo:
259
+ """Delete a SIP trunk.
260
+
261
+ Args:
262
+ delete: Request containing trunk ID to delete
263
+
264
+ Returns:
265
+ Deleted trunk information
266
+ """
267
+ return await self._client.request(
268
+ SVC,
269
+ "DeleteSIPTrunk",
270
+ delete,
271
+ self._admin_headers(),
272
+ SIPTrunkInfo,
273
+ )
274
+
275
+ async def create_sip_dispatch_rule(
276
+ self, create: CreateSIPDispatchRuleRequest
277
+ ) -> SIPDispatchRuleInfo:
278
+ """Create a new SIP dispatch rule.
279
+
280
+ Args:
281
+ create: Request containing rule details
282
+
283
+ Returns:
284
+ Created SIP dispatch rule
285
+ """
286
+ return await self._client.request(
287
+ SVC,
288
+ "CreateSIPDispatchRule",
289
+ create,
290
+ self._admin_headers(),
291
+ SIPDispatchRuleInfo,
292
+ )
293
+
294
+ async def update_sip_dispatch_rule(
295
+ self,
296
+ rule_id: str,
297
+ rule: SIPDispatchRuleInfo,
298
+ ) -> SIPDispatchRuleInfo:
299
+ """Updates an existing SIP dispatch rule by replacing it entirely.
300
+
301
+ Args:
302
+ rule_id: ID of the SIP dispatch rule to update
303
+ rule: SIP dispatch rule to update with
304
+
305
+ Returns:
306
+ Updated SIP dispatch rule
307
+ """
308
+ return await self._client.request(
309
+ SVC,
310
+ "UpdateSIPDispatchRule",
311
+ UpdateSIPDispatchRuleRequest(sip_dispatch_rule_id=rule_id, replace=rule),
312
+ self._admin_headers(),
313
+ SIPDispatchRuleInfo,
314
+ )
315
+
316
+ async def update_sip_dispatch_rule_fields(
317
+ self,
318
+ rule_id: str,
319
+ *,
320
+ trunk_ids: Optional[list[str]] = None,
321
+ rule: Optional[SIPDispatchRule] = None,
322
+ name: Optional[str] = None,
323
+ metadata: Optional[str] = None,
324
+ attributes: Optional[dict[str, str]] = None,
325
+ ) -> SIPDispatchRuleInfo:
326
+ """Updates specific fields of an existing SIP dispatch rule.
327
+
328
+ Only provided fields will be updated.
329
+ """
330
+ update = SIPDispatchRuleUpdate(
331
+ name=name, metadata=metadata, rule=rule, attributes=attributes
332
+ )
333
+ if trunk_ids is not None:
334
+ update.trunk_ids = ListUpdate(set=trunk_ids)
335
+ return await self._client.request(
336
+ SVC,
337
+ "UpdateSIPDispatchRule",
338
+ UpdateSIPDispatchRuleRequest(sip_dispatch_rule_id=rule_id, update=update),
339
+ self._admin_headers(),
340
+ SIPDispatchRuleInfo,
341
+ )
342
+
343
+ async def list_sip_dispatch_rule(
344
+ self, list: ListSIPDispatchRuleRequest
345
+ ) -> ListSIPDispatchRuleResponse:
346
+ """List SIP dispatch rules with optional filtering.
347
+
348
+ Args:
349
+ list: Request with optional filtering parameters
350
+
351
+ Returns:
352
+ Response containing list of SIP dispatch rules
353
+ """
354
+ return await self._client.request(
355
+ SVC,
356
+ "ListSIPDispatchRule",
357
+ list,
358
+ self._admin_headers(),
359
+ ListSIPDispatchRuleResponse,
360
+ )
361
+
362
+ async def delete_sip_dispatch_rule(
363
+ self, delete: DeleteSIPDispatchRuleRequest
364
+ ) -> SIPDispatchRuleInfo:
365
+ """Delete a SIP dispatch rule.
366
+
367
+ Args:
368
+ delete: Request containing rule ID to delete
369
+
370
+ Returns:
371
+ Deleted rule information
372
+ """
373
+ return await self._client.request(
374
+ SVC,
375
+ "DeleteSIPDispatchRule",
376
+ delete,
377
+ self._admin_headers(),
378
+ SIPDispatchRuleInfo,
379
+ )
380
+
381
+ async def create_sip_participant(
382
+ self,
383
+ create: CreateSIPParticipantRequest,
384
+ *,
385
+ timeout: Optional[float] = None,
386
+ ) -> SIPParticipantInfo:
387
+ """Create a new SIP participant.
388
+
389
+ Args:
390
+ create: Request containing participant details
391
+ timeout: Optional request timeout in seconds
392
+
393
+ Returns:
394
+ Created SIP participant
395
+
396
+ Raises:
397
+ SIPError: If the SIP operation fails
398
+ """
399
+ client_timeout: Optional[aiohttp.ClientTimeout] = None
400
+ if timeout:
401
+ # obay user specified timeout
402
+ client_timeout = aiohttp.ClientTimeout(total=timeout)
403
+ elif create.wait_until_answered:
404
+ # ensure default timeout isn't too short when using sync mode
405
+ if (
406
+ self._client._session.timeout
407
+ and self._client._session.timeout.total
408
+ and self._client._session.timeout.total < 20
409
+ ):
410
+ client_timeout = aiohttp.ClientTimeout(total=20)
411
+
412
+ return await self._client.request(
413
+ SVC,
414
+ "CreateSIPParticipant",
415
+ create,
416
+ self._auth_header(VideoGrants(), sip=SIPGrants(call=True)),
417
+ SIPParticipantInfo,
418
+ timeout=client_timeout,
419
+ )
420
+
421
+ async def transfer_sip_participant(
422
+ self, transfer: TransferSIPParticipantRequest
423
+ ) -> SIPParticipantInfo:
424
+ """Transfer a SIP participant to a different room.
425
+
426
+ Args:
427
+ transfer: Request containing transfer details
428
+
429
+ Returns:
430
+ Updated SIP participant information
431
+ """
432
+ return await self._client.request(
433
+ SVC,
434
+ "TransferSIPParticipant",
435
+ transfer,
436
+ self._auth_header(
437
+ VideoGrants(
438
+ room_admin=True,
439
+ room=transfer.room_name,
440
+ ),
441
+ sip=SIPGrants(call=True),
442
+ ),
443
+ SIPParticipantInfo,
444
+ )
445
+
446
+ def _admin_headers(self) -> dict[str, str]:
447
+ return self._auth_header(VideoGrants(), sip=SIPGrants(admin=True))
@@ -12,7 +12,7 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- from typing import Dict, Type, TypeVar
15
+ from typing import Dict, Type, TypeVar, Optional
16
16
 
17
17
  import aiohttp
18
18
  from google.protobuf.message import Message
@@ -22,9 +22,18 @@ DEFAULT_PREFIX = "twirp"
22
22
 
23
23
 
24
24
  class TwirpError(Exception):
25
- def __init__(self, code: str, msg: str) -> None:
25
+ def __init__(
26
+ self,
27
+ code: str,
28
+ msg: str,
29
+ *,
30
+ status: int,
31
+ metadata: Optional[Dict[str, str]] = None,
32
+ ) -> None:
26
33
  self._code = code
27
34
  self._msg = msg
35
+ self._status = status
36
+ self._metadata = metadata or {}
28
37
 
29
38
  @property
30
39
  def code(self) -> str:
@@ -34,6 +43,23 @@ class TwirpError(Exception):
34
43
  def message(self) -> str:
35
44
  return self._msg
36
45
 
46
+ @property
47
+ def status(self) -> int:
48
+ """HTTP status code"""
49
+ return self._status
50
+
51
+ @property
52
+ def metadata(self) -> Dict[str, str]:
53
+ """Twirp metadata"""
54
+ return self._metadata
55
+
56
+ def __str__(self) -> str:
57
+ result = f"TwirpError(code={self.code}, message={self.message}, status={self.status}"
58
+ if self.metadata:
59
+ result += f", metadata={self.metadata}"
60
+ result += ")"
61
+ return result
62
+
37
63
 
38
64
  class TwirpErrorCode:
39
65
  CANCELED = "canceled"
@@ -85,17 +111,24 @@ class TwirpClient:
85
111
  data: Message,
86
112
  headers: Dict[str, str],
87
113
  response_class: Type[T],
114
+ *,
115
+ timeout: Optional[aiohttp.ClientTimeout] = None,
88
116
  ) -> T:
89
117
  url = f"{self.host}/{self.prefix}/{self.pkg}.{service}/{method}"
90
118
  headers["Content-Type"] = "application/protobuf"
91
119
 
92
120
  serialized_data = data.SerializeToString()
93
121
  async with self._session.post(
94
- url, headers=headers, data=serialized_data
122
+ url, headers=headers, data=serialized_data, timeout=timeout
95
123
  ) as resp:
96
124
  if resp.status == 200:
97
125
  return response_class.FromString(await resp.read())
98
126
  else:
99
127
  # when we have an error, Twirp always encode it in json
100
128
  error_data = await resp.json()
101
- raise TwirpError(error_data["code"], error_data["msg"])
129
+ raise TwirpError(
130
+ error_data.get("code", "unknown"),
131
+ error_data.get("msg", ""),
132
+ status=resp.status,
133
+ metadata=error_data.get("meta"),
134
+ )
@@ -0,0 +1 @@
1
+ __version__ = "1.0.1"
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: livekit-api
3
- Version: 0.8.2
3
+ Version: 1.0.1
4
4
  Summary: Python Server API for LiveKit
5
5
  Home-page: https://github.com/livekit/python-sdks
6
6
  License: Apache-2.0
@@ -23,9 +23,9 @@ Requires-Python: >=3.9.0
23
23
  Description-Content-Type: text/markdown
24
24
  Requires-Dist: pyjwt>=2.0.0
25
25
  Requires-Dist: aiohttp>=3.9.0
26
- Requires-Dist: protobuf>=3
27
- Requires-Dist: types-protobuf<5,>=4
28
- Requires-Dist: livekit-protocol<2,>=0.8.2
26
+ Requires-Dist: protobuf>=4
27
+ Requires-Dist: types-protobuf>=4
28
+ Requires-Dist: livekit-protocol~=1.0
29
29
  Dynamic: classifier
30
30
  Dynamic: description
31
31
  Dynamic: description-content-type
@@ -0,0 +1,5 @@
1
+ pyjwt>=2.0.0
2
+ aiohttp>=3.9.0
3
+ protobuf>=4
4
+ types-protobuf>=4
5
+ livekit-protocol~=1.0
@@ -51,9 +51,9 @@ setuptools.setup(
51
51
  install_requires=[
52
52
  "pyjwt>=2.0.0",
53
53
  "aiohttp>=3.9.0",
54
- "protobuf>=3",
55
- "types-protobuf>=4,<5",
56
- "livekit-protocol>=0.8.2,<2",
54
+ "protobuf>=4",
55
+ "types-protobuf>=4",
56
+ "livekit-protocol~=1.0",
57
57
  ],
58
58
  package_data={
59
59
  "livekit.api": ["py.typed", "*.pyi", "**/*.pyi"],
@@ -75,11 +75,7 @@ def test_agent_config():
75
75
 
76
76
 
77
77
  def test_verify_token_invalid():
78
- token = (
79
- AccessToken(TEST_API_KEY, TEST_API_SECRET)
80
- .with_identity("test_identity")
81
- .to_jwt()
82
- )
78
+ token = AccessToken(TEST_API_KEY, TEST_API_SECRET).with_identity("test_identity").to_jwt()
83
79
 
84
80
  token_verifier = TokenVerifier(TEST_API_KEY, "invalid_secret")
85
81
  with pytest.raises(Exception):
@@ -1,182 +0,0 @@
1
- import aiohttp
2
- from livekit.protocol.sip import (
3
- CreateSIPTrunkRequest,
4
- SIPTrunkInfo,
5
- CreateSIPInboundTrunkRequest,
6
- SIPInboundTrunkInfo,
7
- CreateSIPOutboundTrunkRequest,
8
- SIPOutboundTrunkInfo,
9
- ListSIPTrunkRequest,
10
- ListSIPTrunkResponse,
11
- ListSIPInboundTrunkRequest,
12
- ListSIPInboundTrunkResponse,
13
- ListSIPOutboundTrunkRequest,
14
- ListSIPOutboundTrunkResponse,
15
- DeleteSIPTrunkRequest,
16
- SIPDispatchRuleInfo,
17
- CreateSIPDispatchRuleRequest,
18
- ListSIPDispatchRuleRequest,
19
- ListSIPDispatchRuleResponse,
20
- DeleteSIPDispatchRuleRequest,
21
- CreateSIPParticipantRequest,
22
- TransferSIPParticipantRequest,
23
- SIPParticipantInfo,
24
- )
25
- from ._service import Service
26
- from .access_token import VideoGrants, SIPGrants
27
-
28
- SVC = "SIP"
29
- """@private"""
30
-
31
-
32
- class SipService(Service):
33
- """Client for LiveKit SIP Service API
34
-
35
- Recommended way to use this service is via `livekit.api.LiveKitAPI`:
36
-
37
- ```python
38
- from livekit import api
39
- lkapi = api.LiveKitAPI()
40
- sip_service = lkapi.sip
41
- ```
42
- """
43
-
44
- def __init__(
45
- self, session: aiohttp.ClientSession, url: str, api_key: str, api_secret: str
46
- ):
47
- super().__init__(session, url, api_key, api_secret)
48
-
49
- async def create_sip_trunk(self, create: CreateSIPTrunkRequest) -> SIPTrunkInfo:
50
- """
51
- @deprecated Use create_sip_inbound_trunk or create_sip_outbound_trunk instead
52
- """
53
- return await self._client.request(
54
- SVC,
55
- "CreateSIPTrunk",
56
- create,
57
- self._auth_header(VideoGrants(), sip=SIPGrants(admin=True)),
58
- SIPTrunkInfo,
59
- )
60
-
61
- async def create_sip_inbound_trunk(
62
- self, create: CreateSIPInboundTrunkRequest
63
- ) -> SIPInboundTrunkInfo:
64
- return await self._client.request(
65
- SVC,
66
- "CreateSIPInboundTrunk",
67
- create,
68
- self._auth_header(VideoGrants(), sip=SIPGrants(admin=True)),
69
- SIPInboundTrunkInfo,
70
- )
71
-
72
- async def create_sip_outbound_trunk(
73
- self, create: CreateSIPOutboundTrunkRequest
74
- ) -> SIPOutboundTrunkInfo:
75
- return await self._client.request(
76
- SVC,
77
- "CreateSIPOutboundTrunk",
78
- create,
79
- self._auth_header(VideoGrants(), sip=SIPGrants(admin=True)),
80
- SIPOutboundTrunkInfo,
81
- )
82
-
83
- async def list_sip_trunk(self, list: ListSIPTrunkRequest) -> ListSIPTrunkResponse:
84
- return await self._client.request(
85
- SVC,
86
- "ListSIPTrunk",
87
- list,
88
- self._auth_header(VideoGrants(), sip=SIPGrants(admin=True)),
89
- ListSIPTrunkResponse,
90
- )
91
-
92
- async def list_sip_inbound_trunk(
93
- self, list: ListSIPInboundTrunkRequest
94
- ) -> ListSIPInboundTrunkResponse:
95
- return await self._client.request(
96
- SVC,
97
- "ListSIPInboundTrunk",
98
- list,
99
- self._auth_header(VideoGrants(), sip=SIPGrants(admin=True)),
100
- ListSIPInboundTrunkResponse,
101
- )
102
-
103
- async def list_sip_outbound_trunk(
104
- self, list: ListSIPOutboundTrunkRequest
105
- ) -> ListSIPOutboundTrunkResponse:
106
- return await self._client.request(
107
- SVC,
108
- "ListSIPOutboundTrunk",
109
- list,
110
- self._auth_header(VideoGrants(), sip=SIPGrants(admin=True)),
111
- ListSIPOutboundTrunkResponse,
112
- )
113
-
114
- async def delete_sip_trunk(self, delete: DeleteSIPTrunkRequest) -> SIPTrunkInfo:
115
- return await self._client.request(
116
- SVC,
117
- "DeleteSIPTrunk",
118
- delete,
119
- self._auth_header(VideoGrants(), sip=SIPGrants(admin=True)),
120
- SIPTrunkInfo,
121
- )
122
-
123
- async def create_sip_dispatch_rule(
124
- self, create: CreateSIPDispatchRuleRequest
125
- ) -> SIPDispatchRuleInfo:
126
- return await self._client.request(
127
- SVC,
128
- "CreateSIPDispatchRule",
129
- create,
130
- self._auth_header(VideoGrants(), sip=SIPGrants(admin=True)),
131
- SIPDispatchRuleInfo,
132
- )
133
-
134
- async def list_sip_dispatch_rule(
135
- self, list: ListSIPDispatchRuleRequest
136
- ) -> ListSIPDispatchRuleResponse:
137
- return await self._client.request(
138
- SVC,
139
- "ListSIPDispatchRule",
140
- list,
141
- self._auth_header(VideoGrants(), sip=SIPGrants(admin=True)),
142
- ListSIPDispatchRuleResponse,
143
- )
144
-
145
- async def delete_sip_dispatch_rule(
146
- self, delete: DeleteSIPDispatchRuleRequest
147
- ) -> SIPDispatchRuleInfo:
148
- return await self._client.request(
149
- SVC,
150
- "DeleteSIPDispatchRule",
151
- delete,
152
- self._auth_header(VideoGrants(), sip=SIPGrants(admin=True)),
153
- SIPDispatchRuleInfo,
154
- )
155
-
156
- async def create_sip_participant(
157
- self, create: CreateSIPParticipantRequest
158
- ) -> SIPParticipantInfo:
159
- return await self._client.request(
160
- SVC,
161
- "CreateSIPParticipant",
162
- create,
163
- self._auth_header(VideoGrants(), sip=SIPGrants(call=True)),
164
- SIPParticipantInfo,
165
- )
166
-
167
- async def transfer_sip_participant(
168
- self, transfer: TransferSIPParticipantRequest
169
- ) -> SIPParticipantInfo:
170
- return await self._client.request(
171
- SVC,
172
- "TransferSIPParticipant",
173
- transfer,
174
- self._auth_header(
175
- VideoGrants(
176
- room_admin=True,
177
- room=transfer.room_name,
178
- ),
179
- sip=SIPGrants(call=True),
180
- ),
181
- SIPParticipantInfo,
182
- )
@@ -1 +0,0 @@
1
- __version__ = "0.8.2"
@@ -1,5 +0,0 @@
1
- pyjwt>=2.0.0
2
- aiohttp>=3.9.0
3
- protobuf>=3
4
- types-protobuf<5,>=4
5
- livekit-protocol<2,>=0.8.2
File without changes
File without changes
File without changes