getstream 3.3.0__tar.gz → 3.3.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.
Files changed (118) hide show
  1. {getstream-3.3.0 → getstream-3.3.2}/PKG-INFO +1 -1
  2. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/connection_manager.py +40 -1
  3. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/reconnection.py +5 -2
  4. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/signaling.py +57 -4
  5. {getstream-3.3.0 → getstream-3.3.2}/.cursor/worktrees.json +0 -0
  6. {getstream-3.3.0 → getstream-3.3.2}/.env.example +0 -0
  7. {getstream-3.3.0 → getstream-3.3.2}/.github/actions/python-uv-setup/action.yml +0 -0
  8. {getstream-3.3.0 → getstream-3.3.2}/.github/workflows/ci.yml +0 -0
  9. {getstream-3.3.0 → getstream-3.3.2}/.github/workflows/release.yml +0 -0
  10. {getstream-3.3.0 → getstream-3.3.2}/.github/workflows/run_tests.yml +0 -0
  11. {getstream-3.3.0 → getstream-3.3.2}/.github/workflows/stream-py.code-workspace +0 -0
  12. {getstream-3.3.0 → getstream-3.3.2}/.gitignore +0 -0
  13. {getstream-3.3.0 → getstream-3.3.2}/.gitmodules +0 -0
  14. {getstream-3.3.0 → getstream-3.3.2}/.pre-commit-config.yaml +0 -0
  15. {getstream-3.3.0 → getstream-3.3.2}/AGENTS.md +0 -0
  16. {getstream-3.3.0 → getstream-3.3.2}/CHANGELOG.md +0 -0
  17. {getstream-3.3.0 → getstream-3.3.2}/DEVELOPMENT.md +0 -0
  18. {getstream-3.3.0 → getstream-3.3.2}/LICENSE.md +0 -0
  19. {getstream-3.3.0 → getstream-3.3.2}/MIGRATION_v2_to_v3.md +0 -0
  20. {getstream-3.3.0 → getstream-3.3.2}/Makefile +0 -0
  21. {getstream-3.3.0 → getstream-3.3.2}/README.md +0 -0
  22. {getstream-3.3.0 → getstream-3.3.2}/dev.py +0 -0
  23. {getstream-3.3.0 → getstream-3.3.2}/docs/migration-from-stream-chat-python/01-setup-and-auth.md +0 -0
  24. {getstream-3.3.0 → getstream-3.3.2}/docs/migration-from-stream-chat-python/02-users.md +0 -0
  25. {getstream-3.3.0 → getstream-3.3.2}/docs/migration-from-stream-chat-python/03-channels.md +0 -0
  26. {getstream-3.3.0 → getstream-3.3.2}/docs/migration-from-stream-chat-python/04-messages-and-reactions.md +0 -0
  27. {getstream-3.3.0 → getstream-3.3.2}/docs/migration-from-stream-chat-python/05-moderation.md +0 -0
  28. {getstream-3.3.0 → getstream-3.3.2}/docs/migration-from-stream-chat-python/06-devices.md +0 -0
  29. {getstream-3.3.0 → getstream-3.3.2}/docs/migration-from-stream-chat-python/README.md +0 -0
  30. {getstream-3.3.0 → getstream-3.3.2}/generate.sh +0 -0
  31. {getstream-3.3.0 → getstream-3.3.2}/generate_webrtc.sh +0 -0
  32. {getstream-3.3.0 → getstream-3.3.2}/getstream/__init__.py +0 -0
  33. {getstream-3.3.0 → getstream-3.3.2}/getstream/base.py +0 -0
  34. {getstream-3.3.0 → getstream-3.3.2}/getstream/chat/__init__.py +0 -0
  35. {getstream-3.3.0 → getstream-3.3.2}/getstream/chat/async_channel.py +0 -0
  36. {getstream-3.3.0 → getstream-3.3.2}/getstream/chat/async_client.py +0 -0
  37. {getstream-3.3.0 → getstream-3.3.2}/getstream/chat/async_rest_client.py +0 -0
  38. {getstream-3.3.0 → getstream-3.3.2}/getstream/chat/channel.py +0 -0
  39. {getstream-3.3.0 → getstream-3.3.2}/getstream/chat/client.py +0 -0
  40. {getstream-3.3.0 → getstream-3.3.2}/getstream/chat/rest_client.py +0 -0
  41. {getstream-3.3.0 → getstream-3.3.2}/getstream/common/__init__.py +0 -0
  42. {getstream-3.3.0 → getstream-3.3.2}/getstream/common/async_client.py +0 -0
  43. {getstream-3.3.0 → getstream-3.3.2}/getstream/common/async_rest_client.py +0 -0
  44. {getstream-3.3.0 → getstream-3.3.2}/getstream/common/client.py +0 -0
  45. {getstream-3.3.0 → getstream-3.3.2}/getstream/common/rest_client.py +0 -0
  46. {getstream-3.3.0 → getstream-3.3.2}/getstream/common/telemetry.py +0 -0
  47. {getstream-3.3.0 → getstream-3.3.2}/getstream/config.py +0 -0
  48. {getstream-3.3.0 → getstream-3.3.2}/getstream/feeds/__init__.py +0 -0
  49. {getstream-3.3.0 → getstream-3.3.2}/getstream/feeds/client.py +0 -0
  50. {getstream-3.3.0 → getstream-3.3.2}/getstream/feeds/feeds.py +0 -0
  51. {getstream-3.3.0 → getstream-3.3.2}/getstream/feeds/rest_client.py +0 -0
  52. {getstream-3.3.0 → getstream-3.3.2}/getstream/generic.py +0 -0
  53. {getstream-3.3.0 → getstream-3.3.2}/getstream/meta.py +0 -0
  54. {getstream-3.3.0 → getstream-3.3.2}/getstream/models/__init__.py +0 -0
  55. {getstream-3.3.0 → getstream-3.3.2}/getstream/moderation/__init__.py +0 -0
  56. {getstream-3.3.0 → getstream-3.3.2}/getstream/moderation/async_client.py +0 -0
  57. {getstream-3.3.0 → getstream-3.3.2}/getstream/moderation/async_rest_client.py +0 -0
  58. {getstream-3.3.0 → getstream-3.3.2}/getstream/moderation/client.py +0 -0
  59. {getstream-3.3.0 → getstream-3.3.2}/getstream/moderation/rest_client.py +0 -0
  60. {getstream-3.3.0 → getstream-3.3.2}/getstream/rate_limit.py +0 -0
  61. {getstream-3.3.0 → getstream-3.3.2}/getstream/stream.py +0 -0
  62. {getstream-3.3.0 → getstream-3.3.2}/getstream/stream_response.py +0 -0
  63. {getstream-3.3.0 → getstream-3.3.2}/getstream/tests/test_webhook.py +0 -0
  64. {getstream-3.3.0 → getstream-3.3.2}/getstream/utils/__init__.py +0 -0
  65. {getstream-3.3.0 → getstream-3.3.2}/getstream/utils/event_emitter.py +0 -0
  66. {getstream-3.3.0 → getstream-3.3.2}/getstream/utils/retry.py +0 -0
  67. {getstream-3.3.0 → getstream-3.3.2}/getstream/version.py +0 -0
  68. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/__init__.py +0 -0
  69. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/async_call.py +0 -0
  70. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/async_client.py +0 -0
  71. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/async_rest_client.py +0 -0
  72. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/call.py +0 -0
  73. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/client.py +0 -0
  74. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/openai.py +0 -0
  75. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rest_client.py +0 -0
  76. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/README.md +0 -0
  77. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/__init__.py +0 -0
  78. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/audio_track.py +0 -0
  79. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/connection_utils.py +0 -0
  80. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/coordinator/__init__.py +0 -0
  81. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/coordinator/backoff.py +0 -0
  82. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/coordinator/errors.py +0 -0
  83. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/coordinator/ws.py +0 -0
  84. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/coordinator_api.py +0 -0
  85. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/encoders_patches.py +0 -0
  86. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/g711.py +0 -0
  87. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/location_discovery.py +0 -0
  88. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/models.py +0 -0
  89. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/network_monitor.py +0 -0
  90. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/participants.py +0 -0
  91. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/pb/__init__.py +0 -0
  92. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/pb/stream/__init__.py +0 -0
  93. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/pb/stream/video/__init__.py +0 -0
  94. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/pb/stream/video/sfu/__init__.py +0 -0
  95. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/pb/stream/video/sfu/event/__init__.py +0 -0
  96. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/pb/stream/video/sfu/event/events_pb2.py +0 -0
  97. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/pb/stream/video/sfu/event/events_pb2.pyi +0 -0
  98. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/pb/stream/video/sfu/models/__init__.py +0 -0
  99. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/pb/stream/video/sfu/models/models_pb2.py +0 -0
  100. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/pb/stream/video/sfu/models/models_pb2.pyi +0 -0
  101. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/pb/stream/video/sfu/signal_rpc/__init__.py +0 -0
  102. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/pb/stream/video/sfu/signal_rpc/signal_pb2.py +0 -0
  103. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/pb/stream/video/sfu/signal_rpc/signal_pb2.pyi +0 -0
  104. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/pb/stream/video/sfu/signal_rpc/signal_twirp.py +0 -0
  105. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/pc.py +0 -0
  106. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/peer_connection.py +0 -0
  107. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/recording.py +0 -0
  108. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/stats_reporter.py +0 -0
  109. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/stats_tracer.py +0 -0
  110. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/tracer.py +0 -0
  111. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/track_util.py +0 -0
  112. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/tracks.py +0 -0
  113. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/twirp_client_wrapper.py +0 -0
  114. {getstream-3.3.0 → getstream-3.3.2}/getstream/video/rtc/utils.py +0 -0
  115. {getstream-3.3.0 → getstream-3.3.2}/getstream/webhook.py +0 -0
  116. {getstream-3.3.0 → getstream-3.3.2}/pyproject.toml +0 -0
  117. {getstream-3.3.0 → getstream-3.3.2}/pytest.ini +0 -0
  118. {getstream-3.3.0 → getstream-3.3.2}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: getstream
3
- Version: 3.3.0
3
+ Version: 3.3.2
4
4
  Summary: GetStream Python SDK - Build scalable activity feeds, chat, and video calling applications
5
5
  Author-email: sachaarbonel <sacha.arbonel@hotmail.fr>, tbarbugli <tbarbugli@gmail.com>
6
6
  License-File: LICENSE.md
@@ -36,7 +36,7 @@ from getstream.video.rtc.network_monitor import NetworkMonitor
36
36
  from getstream.video.rtc.recording import RecordingManager
37
37
  from getstream.video.rtc.participants import ParticipantsState
38
38
  from getstream.video.rtc.tracks import SubscriptionConfig, SubscriptionManager
39
- from getstream.video.rtc.reconnection import ReconnectionManager
39
+ from getstream.video.rtc.reconnection import ReconnectionManager, ReconnectionStrategy
40
40
  from getstream.video.rtc.peer_connection import PeerConnectionManager
41
41
  from getstream.video.rtc.models import JoinCallResponse
42
42
  from getstream.video.rtc.tracer import Tracer
@@ -198,6 +198,17 @@ class ConnectionManager(StreamAsyncIOEventEmitter):
198
198
  async def _on_subscriber_offer(self, event: events_pb2.SubscriberOffer):
199
199
  logger.info("Subscriber offer received")
200
200
 
201
+ # Offers can arrive after the subscriber peer connection has been
202
+ # torn down (slow asyncio loop under load, SFU sending a late
203
+ # renegotiation). `setRemoteDescription` would raise
204
+ # `InvalidStateError: Cannot handle offer in signaling state "closed"`
205
+ # and the exception propagates through the pyee error path, killing
206
+ # the session. Drop the offer instead — there is nothing to
207
+ # negotiate with a closed connection.
208
+ if self.subscriber_pc is None or self.subscriber_pc.signalingState == "closed":
209
+ logger.debug("Subscriber offer arrived after PC closed; dropping")
210
+ return
211
+
201
212
  with telemetry.start_as_current_span("rtc.on_subscriber_offer") as span:
202
213
  await self.subscriber_negotiation_lock.acquire()
203
214
 
@@ -266,6 +277,25 @@ class ConnectionManager(StreamAsyncIOEventEmitter):
266
277
  finally:
267
278
  self.subscriber_negotiation_lock.release()
268
279
 
280
+ async def _on_signaling_connection_lost(self, reason: str) -> None:
281
+ """Reconnect when the signaling WebSocket drops unexpectedly.
282
+
283
+ The WebSocketClient itself only logs the error and stops; it has
284
+ no reconnect of its own. This handler bridges that gap by routing
285
+ the loss into the existing `ReconnectionManager`, so a transient
286
+ TCP reset or a missed health check no longer means a dead session.
287
+ """
288
+ if not self.running:
289
+ return
290
+ logger.warning(f"Signaling WS lost; triggering reconnect: {reason}")
291
+ try:
292
+ await self._reconnector.reconnect(
293
+ strategy=ReconnectionStrategy.FAST,
294
+ reason=f"signaling ws lost: {reason}",
295
+ )
296
+ except Exception:
297
+ logger.exception("Reconnect after signaling WS loss failed")
298
+
269
299
  async def _connect_coordinator_ws(self):
270
300
  """
271
301
  Connects to the coordinator websocket and subscribes to events.
@@ -403,6 +433,15 @@ class ConnectionManager(StreamAsyncIOEventEmitter):
403
433
  # Connect subscriber offer event to handle SDP negotiation
404
434
  self._ws_client.on_event("subscriber_offer", self._on_subscriber_offer)
405
435
 
436
+ # Drive reconnection when the signaling WS drops outside of an
437
+ # SFU-level error event (raw socket close, health-check timeout,
438
+ # transport-level exceptions). Without this handler the
439
+ # WebSocketClient just logs and stops; the session sits hanging
440
+ # until the frontend times out and tears it down.
441
+ self._ws_client.on_event(
442
+ "connection_lost", self._on_signaling_connection_lost
443
+ )
444
+
406
445
  # Re-emit the events so they can be subscribed to on the ConnectionManager
407
446
  self._ws_client.on_wildcard("*", self.emit)
408
447
 
@@ -209,10 +209,13 @@ class ReconnectionManager:
209
209
  self.connection_manager._connection_options.fast_reconnect = True
210
210
  previous_ws_client = self.connection_manager.ws_client
211
211
 
212
- # Use _connect_internal with existing connection info
212
+ # Use _connect_internal with existing connection info.
213
+ # `self.join_response` is already the unwrapped data payload
214
+ # (see ConnectionManager._connect_internal: it stores
215
+ # `join_response.data`), so credentials live at the top level.
213
216
  await self.connection_manager._connect_internal(
214
217
  region=self.connection_manager._connection_options.region,
215
- token=self.connection_manager.join_response.data.credentials.token
218
+ token=self.connection_manager.join_response.credentials.token
216
219
  if self.connection_manager.join_response
217
220
  else None,
218
221
  session_id=self.connection_manager.session_id,
@@ -1,12 +1,14 @@
1
1
  import asyncio
2
- import threading
3
- import websocket
4
2
  import logging
3
+ import threading
5
4
  import time
6
- from typing import TYPE_CHECKING, Any, Callable, Awaitable, Optional, Set
5
+ from typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional, Set
6
+
7
+ import websocket
7
8
 
8
9
  from getstream.common import telemetry
9
10
  from getstream.utils import StreamAsyncIOEventEmitter
11
+
10
12
  from .pb.stream.video.sfu.event import events_pb2
11
13
 
12
14
  if TYPE_CHECKING:
@@ -65,6 +67,7 @@ class WebSocketClient(StreamAsyncIOEventEmitter):
65
67
  self.thread = None
66
68
  self.running = False
67
69
  self.closed = False
70
+ self._connection_lost_sent = False
68
71
 
69
72
  # For ping/health check mechanism
70
73
  self.ping_thread = None
@@ -196,18 +199,64 @@ class WebSocketClient(StreamAsyncIOEventEmitter):
196
199
 
197
200
  def _on_error(self, ws, error):
198
201
  """Handle WebSocket error."""
199
- logger.error(f"WebSocket error: {str(error)}")
202
+ if (
203
+ isinstance(error, websocket.ABNF)
204
+ and error.opcode == websocket.ABNF.OPCODE_CLOSE
205
+ ):
206
+ # For some reason, websockets lib propagates closing frame as an error.
207
+ # Simply log a debug here if that happens.
208
+ logger.debug(f"WebSocket closed by server: {error}")
209
+ else:
210
+ logger.error(f"WebSocket error: {error}")
211
+
200
212
  if not self.first_message_event.is_set():
201
213
  # Create an error event
202
214
  error_event = events_pb2.SfuEvent()
203
215
  error_event.error.error.message = str(error)
204
216
  self.first_message = error_event
205
217
  self.first_message_event.set()
218
+ elif not self.closed:
219
+ self._notify_connection_lost(f"error: {error}")
206
220
 
207
221
  def _on_close(self, ws, close_status_code, close_msg):
208
222
  """Handle WebSocket close event."""
209
223
  logger.debug(f"WebSocket connection closed: {close_status_code} {close_msg}")
224
+ was_unexpected = not self.closed
210
225
  self.running = False
226
+ if was_unexpected:
227
+ self._notify_connection_lost(
228
+ f"closed by remote (code={close_status_code} msg={close_msg})"
229
+ )
230
+
231
+ def _notify_connection_lost(self, reason: str) -> None:
232
+ """Schedule a ``connection_lost`` emit on the main loop.
233
+
234
+ Idempotent per ``WebSocketClient`` instance — only the first call
235
+ per disconnect actually emits. Callers run on the WS worker thread
236
+ or ``_ping_loop`` thread; pyee schedules async listeners via
237
+ ``loop.create_task``, which is not thread-safe, hence the hop.
238
+ Same pattern as ``_on_message`` for SFU events.
239
+ """
240
+ if not self._claim_connection_lost():
241
+ return
242
+ try:
243
+ asyncio.run_coroutine_threadsafe(
244
+ self._emit_connection_lost(reason),
245
+ self.main_loop,
246
+ )
247
+ except Exception:
248
+ logger.exception("Failed to schedule connection_lost emit")
249
+
250
+ def _claim_connection_lost(self) -> bool:
251
+ """Return True iff this is the first connection-lost notification."""
252
+ if self._connection_lost_sent:
253
+ return False
254
+ self._connection_lost_sent = True
255
+ return True
256
+
257
+ async def _emit_connection_lost(self, reason: str) -> None:
258
+ with telemetry.attach_span(self.parent_span):
259
+ self.emit("connection_lost", reason)
211
260
 
212
261
  def _start_ping_handler(self):
213
262
  """Start the ping mechanism in a background thread."""
@@ -231,6 +280,10 @@ class WebSocketClient(StreamAsyncIOEventEmitter):
231
280
  current_time = time.time()
232
281
  if current_time - self.last_health_check_time > self.ping_interval * 2:
233
282
  logger.warning("Health check failed, closing connection")
283
+ # Notify before close() so the owner can reconnect; close()
284
+ # itself sets `self.closed=True` and would suppress the
285
+ # notification in `_on_close`.
286
+ self._notify_connection_lost("health check timeout")
234
287
  self.close()
235
288
  return
236
289
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes