getstream 3.3.1__tar.gz → 3.3.3__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.1 → getstream-3.3.3}/PKG-INFO +1 -1
  2. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/connection_manager.py +40 -1
  3. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/reconnection.py +8 -2
  4. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/signaling.py +56 -6
  5. {getstream-3.3.1 → getstream-3.3.3}/.cursor/worktrees.json +0 -0
  6. {getstream-3.3.1 → getstream-3.3.3}/.env.example +0 -0
  7. {getstream-3.3.1 → getstream-3.3.3}/.github/actions/python-uv-setup/action.yml +0 -0
  8. {getstream-3.3.1 → getstream-3.3.3}/.github/workflows/ci.yml +0 -0
  9. {getstream-3.3.1 → getstream-3.3.3}/.github/workflows/release.yml +0 -0
  10. {getstream-3.3.1 → getstream-3.3.3}/.github/workflows/run_tests.yml +0 -0
  11. {getstream-3.3.1 → getstream-3.3.3}/.github/workflows/stream-py.code-workspace +0 -0
  12. {getstream-3.3.1 → getstream-3.3.3}/.gitignore +0 -0
  13. {getstream-3.3.1 → getstream-3.3.3}/.gitmodules +0 -0
  14. {getstream-3.3.1 → getstream-3.3.3}/.pre-commit-config.yaml +0 -0
  15. {getstream-3.3.1 → getstream-3.3.3}/AGENTS.md +0 -0
  16. {getstream-3.3.1 → getstream-3.3.3}/CHANGELOG.md +0 -0
  17. {getstream-3.3.1 → getstream-3.3.3}/DEVELOPMENT.md +0 -0
  18. {getstream-3.3.1 → getstream-3.3.3}/LICENSE.md +0 -0
  19. {getstream-3.3.1 → getstream-3.3.3}/MIGRATION_v2_to_v3.md +0 -0
  20. {getstream-3.3.1 → getstream-3.3.3}/Makefile +0 -0
  21. {getstream-3.3.1 → getstream-3.3.3}/README.md +0 -0
  22. {getstream-3.3.1 → getstream-3.3.3}/dev.py +0 -0
  23. {getstream-3.3.1 → getstream-3.3.3}/docs/migration-from-stream-chat-python/01-setup-and-auth.md +0 -0
  24. {getstream-3.3.1 → getstream-3.3.3}/docs/migration-from-stream-chat-python/02-users.md +0 -0
  25. {getstream-3.3.1 → getstream-3.3.3}/docs/migration-from-stream-chat-python/03-channels.md +0 -0
  26. {getstream-3.3.1 → getstream-3.3.3}/docs/migration-from-stream-chat-python/04-messages-and-reactions.md +0 -0
  27. {getstream-3.3.1 → getstream-3.3.3}/docs/migration-from-stream-chat-python/05-moderation.md +0 -0
  28. {getstream-3.3.1 → getstream-3.3.3}/docs/migration-from-stream-chat-python/06-devices.md +0 -0
  29. {getstream-3.3.1 → getstream-3.3.3}/docs/migration-from-stream-chat-python/README.md +0 -0
  30. {getstream-3.3.1 → getstream-3.3.3}/generate.sh +0 -0
  31. {getstream-3.3.1 → getstream-3.3.3}/generate_webrtc.sh +0 -0
  32. {getstream-3.3.1 → getstream-3.3.3}/getstream/__init__.py +0 -0
  33. {getstream-3.3.1 → getstream-3.3.3}/getstream/base.py +0 -0
  34. {getstream-3.3.1 → getstream-3.3.3}/getstream/chat/__init__.py +0 -0
  35. {getstream-3.3.1 → getstream-3.3.3}/getstream/chat/async_channel.py +0 -0
  36. {getstream-3.3.1 → getstream-3.3.3}/getstream/chat/async_client.py +0 -0
  37. {getstream-3.3.1 → getstream-3.3.3}/getstream/chat/async_rest_client.py +0 -0
  38. {getstream-3.3.1 → getstream-3.3.3}/getstream/chat/channel.py +0 -0
  39. {getstream-3.3.1 → getstream-3.3.3}/getstream/chat/client.py +0 -0
  40. {getstream-3.3.1 → getstream-3.3.3}/getstream/chat/rest_client.py +0 -0
  41. {getstream-3.3.1 → getstream-3.3.3}/getstream/common/__init__.py +0 -0
  42. {getstream-3.3.1 → getstream-3.3.3}/getstream/common/async_client.py +0 -0
  43. {getstream-3.3.1 → getstream-3.3.3}/getstream/common/async_rest_client.py +0 -0
  44. {getstream-3.3.1 → getstream-3.3.3}/getstream/common/client.py +0 -0
  45. {getstream-3.3.1 → getstream-3.3.3}/getstream/common/rest_client.py +0 -0
  46. {getstream-3.3.1 → getstream-3.3.3}/getstream/common/telemetry.py +0 -0
  47. {getstream-3.3.1 → getstream-3.3.3}/getstream/config.py +0 -0
  48. {getstream-3.3.1 → getstream-3.3.3}/getstream/feeds/__init__.py +0 -0
  49. {getstream-3.3.1 → getstream-3.3.3}/getstream/feeds/client.py +0 -0
  50. {getstream-3.3.1 → getstream-3.3.3}/getstream/feeds/feeds.py +0 -0
  51. {getstream-3.3.1 → getstream-3.3.3}/getstream/feeds/rest_client.py +0 -0
  52. {getstream-3.3.1 → getstream-3.3.3}/getstream/generic.py +0 -0
  53. {getstream-3.3.1 → getstream-3.3.3}/getstream/meta.py +0 -0
  54. {getstream-3.3.1 → getstream-3.3.3}/getstream/models/__init__.py +0 -0
  55. {getstream-3.3.1 → getstream-3.3.3}/getstream/moderation/__init__.py +0 -0
  56. {getstream-3.3.1 → getstream-3.3.3}/getstream/moderation/async_client.py +0 -0
  57. {getstream-3.3.1 → getstream-3.3.3}/getstream/moderation/async_rest_client.py +0 -0
  58. {getstream-3.3.1 → getstream-3.3.3}/getstream/moderation/client.py +0 -0
  59. {getstream-3.3.1 → getstream-3.3.3}/getstream/moderation/rest_client.py +0 -0
  60. {getstream-3.3.1 → getstream-3.3.3}/getstream/rate_limit.py +0 -0
  61. {getstream-3.3.1 → getstream-3.3.3}/getstream/stream.py +0 -0
  62. {getstream-3.3.1 → getstream-3.3.3}/getstream/stream_response.py +0 -0
  63. {getstream-3.3.1 → getstream-3.3.3}/getstream/tests/test_webhook.py +0 -0
  64. {getstream-3.3.1 → getstream-3.3.3}/getstream/utils/__init__.py +0 -0
  65. {getstream-3.3.1 → getstream-3.3.3}/getstream/utils/event_emitter.py +0 -0
  66. {getstream-3.3.1 → getstream-3.3.3}/getstream/utils/retry.py +0 -0
  67. {getstream-3.3.1 → getstream-3.3.3}/getstream/version.py +0 -0
  68. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/__init__.py +0 -0
  69. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/async_call.py +0 -0
  70. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/async_client.py +0 -0
  71. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/async_rest_client.py +0 -0
  72. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/call.py +0 -0
  73. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/client.py +0 -0
  74. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/openai.py +0 -0
  75. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rest_client.py +0 -0
  76. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/README.md +0 -0
  77. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/__init__.py +0 -0
  78. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/audio_track.py +0 -0
  79. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/connection_utils.py +0 -0
  80. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/coordinator/__init__.py +0 -0
  81. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/coordinator/backoff.py +0 -0
  82. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/coordinator/errors.py +0 -0
  83. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/coordinator/ws.py +0 -0
  84. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/coordinator_api.py +0 -0
  85. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/encoders_patches.py +0 -0
  86. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/g711.py +0 -0
  87. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/location_discovery.py +0 -0
  88. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/models.py +0 -0
  89. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/network_monitor.py +0 -0
  90. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/participants.py +0 -0
  91. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/pb/__init__.py +0 -0
  92. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/pb/stream/__init__.py +0 -0
  93. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/pb/stream/video/__init__.py +0 -0
  94. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/pb/stream/video/sfu/__init__.py +0 -0
  95. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/pb/stream/video/sfu/event/__init__.py +0 -0
  96. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/pb/stream/video/sfu/event/events_pb2.py +0 -0
  97. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/pb/stream/video/sfu/event/events_pb2.pyi +0 -0
  98. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/pb/stream/video/sfu/models/__init__.py +0 -0
  99. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/pb/stream/video/sfu/models/models_pb2.py +0 -0
  100. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/pb/stream/video/sfu/models/models_pb2.pyi +0 -0
  101. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/pb/stream/video/sfu/signal_rpc/__init__.py +0 -0
  102. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/pb/stream/video/sfu/signal_rpc/signal_pb2.py +0 -0
  103. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/pb/stream/video/sfu/signal_rpc/signal_pb2.pyi +0 -0
  104. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/pb/stream/video/sfu/signal_rpc/signal_twirp.py +0 -0
  105. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/pc.py +0 -0
  106. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/peer_connection.py +0 -0
  107. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/recording.py +0 -0
  108. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/stats_reporter.py +0 -0
  109. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/stats_tracer.py +0 -0
  110. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/tracer.py +0 -0
  111. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/track_util.py +0 -0
  112. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/tracks.py +0 -0
  113. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/twirp_client_wrapper.py +0 -0
  114. {getstream-3.3.1 → getstream-3.3.3}/getstream/video/rtc/utils.py +0 -0
  115. {getstream-3.3.1 → getstream-3.3.3}/getstream/webhook.py +0 -0
  116. {getstream-3.3.1 → getstream-3.3.3}/pyproject.toml +0 -0
  117. {getstream-3.3.1 → getstream-3.3.3}/pytest.ini +0 -0
  118. {getstream-3.3.1 → getstream-3.3.3}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: getstream
3
- Version: 3.3.1
3
+ Version: 3.3.3
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,16 @@ 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
219
+ if self.connection_manager.join_response
220
+ else None,
221
+ ws_url=self.connection_manager.join_response.credentials.server.ws_endpoint
216
222
  if self.connection_manager.join_response
217
223
  else None,
218
224
  session_id=self.connection_manager.session_id,
@@ -67,6 +67,11 @@ class WebSocketClient(StreamAsyncIOEventEmitter):
67
67
  self.thread = None
68
68
  self.running = False
69
69
  self.closed = False
70
+ self._connection_lost_sent = False
71
+ # Set when a `call_ended` SFU event arrives. Read by `_on_close` to
72
+ # suppress `connection_lost`, since the close frame that follows carries
73
+ # no distinguishing status code or reason.
74
+ self._call_ended = False
70
75
 
71
76
  # For ping/health check mechanism
72
77
  self.ping_thread = None
@@ -167,6 +172,9 @@ class WebSocketClient(StreamAsyncIOEventEmitter):
167
172
  event_type = event.WhichOneof("event_payload")
168
173
  logger.debug(f"WebSocket message received {event_type}")
169
174
 
175
+ if event_type and event_type == "call_ended":
176
+ self._call_ended = True
177
+
170
178
  if event.HasField("health_check_response"):
171
179
  logger.debug("received health check response")
172
180
  self.last_health_check_time = time.time()
@@ -198,27 +206,65 @@ class WebSocketClient(StreamAsyncIOEventEmitter):
198
206
 
199
207
  def _on_error(self, ws, error):
200
208
  """Handle WebSocket error."""
201
- if (
209
+ is_close_frame = (
202
210
  isinstance(error, websocket.ABNF)
203
211
  and error.opcode == websocket.ABNF.OPCODE_CLOSE
204
- ):
205
- # For some reason, websockets lib propagates closing frame as an error.
206
- # Simply log a debug here if that happens.
207
- logger.debug(f"WebSocket closed by server: {error}")
212
+ )
213
+ if is_close_frame or not self.running:
214
+ # Close-frame-as-error or post-disconnect teardown noise.
215
+ logger.debug(f"WebSocket error after disconnect: {error}")
208
216
  else:
209
217
  logger.error(f"WebSocket error: {error}")
210
218
 
211
219
  if not self.first_message_event.is_set():
212
- # Create an error event
220
+ # Mid-handshake failure: unblock `connect()` with a synthetic
221
+ # error event instead of leaving it parked on `first_message_event`.
213
222
  error_event = events_pb2.SfuEvent()
214
223
  error_event.error.error.message = str(error)
215
224
  self.first_message = error_event
216
225
  self.first_message_event.set()
226
+ elif not self.closed and not is_close_frame:
227
+ self._notify_connection_lost(f"error: {error}")
217
228
 
218
229
  def _on_close(self, ws, close_status_code, close_msg):
219
230
  """Handle WebSocket close event."""
220
231
  logger.debug(f"WebSocket connection closed: {close_status_code} {close_msg}")
232
+ was_unexpected = not self.closed and not self._call_ended
221
233
  self.running = False
234
+ if was_unexpected:
235
+ self._notify_connection_lost(
236
+ f"closed by remote (code={close_status_code} msg={close_msg})"
237
+ )
238
+
239
+ def _notify_connection_lost(self, reason: str) -> None:
240
+ """Schedule a ``connection_lost`` emit on the main loop.
241
+
242
+ Idempotent per ``WebSocketClient`` instance — only the first call
243
+ per disconnect actually emits. Callers run on the WS worker thread
244
+ or ``_ping_loop`` thread; pyee schedules async listeners via
245
+ ``loop.create_task``, which is not thread-safe, hence the hop.
246
+ Same pattern as ``_on_message`` for SFU events.
247
+ """
248
+ if not self._claim_connection_lost():
249
+ return
250
+ try:
251
+ asyncio.run_coroutine_threadsafe(
252
+ self._emit_connection_lost(reason),
253
+ self.main_loop,
254
+ )
255
+ except Exception:
256
+ logger.exception("Failed to schedule connection_lost emit")
257
+
258
+ def _claim_connection_lost(self) -> bool:
259
+ """Return True if this is the first connection-lost notification."""
260
+ if self._connection_lost_sent:
261
+ return False
262
+ self._connection_lost_sent = True
263
+ return True
264
+
265
+ async def _emit_connection_lost(self, reason: str) -> None:
266
+ with telemetry.attach_span(self.parent_span):
267
+ self.emit("connection_lost", reason)
222
268
 
223
269
  def _start_ping_handler(self):
224
270
  """Start the ping mechanism in a background thread."""
@@ -242,6 +288,10 @@ class WebSocketClient(StreamAsyncIOEventEmitter):
242
288
  current_time = time.time()
243
289
  if current_time - self.last_health_check_time > self.ping_interval * 2:
244
290
  logger.warning("Health check failed, closing connection")
291
+ # Notify before close() so the owner can reconnect; close()
292
+ # itself sets `self.closed=True` and would suppress the
293
+ # notification in `_on_close`.
294
+ self._notify_connection_lost("health check timeout")
245
295
  self.close()
246
296
  return
247
297
 
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