phoenix-channels-python-client 0.4.3__tar.gz → 0.4.4__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 (50) hide show
  1. {phoenix_channels_python_client-0.4.3/phoenix_channels_python_client.egg-info → phoenix_channels_python_client-0.4.4}/PKG-INFO +1 -1
  2. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/phoenix_channels_python_client/__init__.py +1 -1
  3. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/phoenix_channels_python_client/client.py +2 -1
  4. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4/phoenix_channels_python_client.egg-info}/PKG-INFO +1 -1
  5. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/pyproject.toml +1 -1
  6. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/tests/test_shutdown_races.py +107 -0
  7. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/LICENSE +0 -0
  8. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/MANIFEST.in +0 -0
  9. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/README.md +0 -0
  10. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/phoenix_channels_python_client/authentication.py +0 -0
  11. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/phoenix_channels_python_client/client_state_machine.py +0 -0
  12. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/phoenix_channels_python_client/client_types.py +0 -0
  13. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/phoenix_channels_python_client/default_decider.py +0 -0
  14. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/phoenix_channels_python_client/diagnostics.py +0 -0
  15. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/phoenix_channels_python_client/exceptions.py +0 -0
  16. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/phoenix_channels_python_client/phx_messages.py +0 -0
  17. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/phoenix_channels_python_client/protocol_handler.py +0 -0
  18. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/phoenix_channels_python_client/reconnect_controller.py +0 -0
  19. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/phoenix_channels_python_client/shutdown_signals.py +0 -0
  20. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/phoenix_channels_python_client/supervisor.py +0 -0
  21. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/phoenix_channels_python_client/topic_runtime.py +0 -0
  22. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/phoenix_channels_python_client/topic_subscription.py +0 -0
  23. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/phoenix_channels_python_client/utils.py +0 -0
  24. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/phoenix_channels_python_client.egg-info/SOURCES.txt +0 -0
  25. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/phoenix_channels_python_client.egg-info/dependency_links.txt +0 -0
  26. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/phoenix_channels_python_client.egg-info/requires.txt +0 -0
  27. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/phoenix_channels_python_client.egg-info/top_level.txt +0 -0
  28. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/setup.cfg +0 -0
  29. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/tests/test_cancel_and_wait.py +0 -0
  30. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/tests/test_channel_recovery.py +0 -0
  31. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/tests/test_cleanup_ownership.py +0 -0
  32. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/tests/test_client_state.py +0 -0
  33. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/tests/test_connection.py +0 -0
  34. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/tests/test_heartbeat.py +0 -0
  35. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/tests/test_heartbeat_timeout.py +0 -0
  36. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/tests/test_join_installation.py +0 -0
  37. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/tests/test_join_lifecycle.py +0 -0
  38. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/tests/test_join_responses.py +0 -0
  39. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/tests/test_optional_lifecycle.py +0 -0
  40. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/tests/test_optional_reconnect.py +0 -0
  41. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/tests/test_protocol_handler.py +0 -0
  42. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/tests/test_reconnect.py +0 -0
  43. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/tests/test_reconnect_decider.py +0 -0
  44. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/tests/test_reconnect_policy_invariants.py +0 -0
  45. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/tests/test_reconnect_stress.py +0 -0
  46. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/tests/test_run_forever_signals.py +0 -0
  47. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/tests/test_security.py +0 -0
  48. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/tests/test_subscriptions.py +0 -0
  49. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/tests/test_supervisor_internals.py +0 -0
  50. {phoenix_channels_python_client-0.4.3 → phoenix_channels_python_client-0.4.4}/tests/test_topic_runtime_internals.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phoenix-channels-python-client
3
- Version: 0.4.3
3
+ Version: 0.4.4
4
4
  Summary: A Python client library for connecting to Phoenix Channels
5
5
  Author: Phoenix Channels Python Client
6
6
  License-Expression: MIT
@@ -29,7 +29,7 @@ from phoenix_channels_python_client.protocol_handler import (
29
29
  )
30
30
  from phoenix_channels_python_client.utils import setup_logging
31
31
 
32
- __version__ = "0.4.3"
32
+ __version__ = "0.4.4"
33
33
  __author__ = "Phoenix Channels Python Client"
34
34
 
35
35
  __all__ = [
@@ -241,7 +241,8 @@ class PHXChannelsClient(SupervisorMixin, TopicRuntimeMixin, ReconnectControllerM
241
241
 
242
242
  try:
243
243
  await self._initial_connection_future
244
- except Exception:
244
+ except (Exception, asyncio.CancelledError):
245
+ # Not BaseException: awaiting cleanup on GeneratorExit raises RuntimeError.
245
246
  await self.shutdown("Initial connection failed")
246
247
  raise
247
248
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phoenix-channels-python-client
3
- Version: 0.4.3
3
+ Version: 0.4.4
4
4
  Summary: A Python client library for connecting to Phoenix Channels
5
5
  Author: Phoenix Channels Python Client
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "phoenix-channels-python-client"
7
- version = "0.4.3"
7
+ version = "0.4.4"
8
8
  description = "A Python client library for connecting to Phoenix Channels"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -47,6 +47,9 @@ HELD_BACKOFF = derive_policy(
47
47
  FAST_RECONNECT, base_delay_s=HELD_BACKOFF_S, max_delay_s=HELD_BACKOFF_S
48
48
  )
49
49
 
50
+ # More refusals than any test reaches, so only the client's own stop ends them.
51
+ REFUSE_EVERY_HANDSHAKE = 1_000_000
52
+
50
53
 
51
54
  class HeldCallback:
52
55
  """A topic callback that, once cancelled, holds its unwinding until released.
@@ -378,3 +381,107 @@ async def test_shutdown_during_a_close_still_closes_the_socket(
378
381
  await asyncio.wait_for(client.shutdown(STOP_REASON), ASYNC_TIMEOUT_S)
379
382
 
380
383
  assert socket.state is State.CLOSED
384
+
385
+
386
+ async def a_handshake_was_refused(server: FakePhoenixServer) -> None:
387
+ assert await wait_for_condition(lambda: server.refused_handshakes >= 1)
388
+
389
+
390
+ async def expire_on_first_refusal(
391
+ server: FakePhoenixServer, client: PHXChannelsClient, deadline: asyncio.Timeout
392
+ ) -> asyncio.Task[None] | None:
393
+ """Expire ``deadline`` once a handshake was refused; return entry's supervisor."""
394
+ await a_handshake_was_refused(server)
395
+ supervisor = client._supervisor_task
396
+ deadline.reschedule(asyncio.get_running_loop().time())
397
+ return supervisor
398
+
399
+
400
+ async def time_out_entry_on_first_refusal(
401
+ server: FakePhoenixServer, client: PHXChannelsClient
402
+ ) -> asyncio.Task[None] | None:
403
+ """Enter under a deadline that expires on the first refused handshake.
404
+
405
+ Returns the supervisor entry started; fails unless the deadline ended entry.
406
+ The deadline starts at ``ASYNC_TIMEOUT_S``, so a missing refusal fails here
407
+ instead of leaving entry waiting forever.
408
+ """
409
+ expiry: asyncio.Task[asyncio.Task[None] | None] | None = None
410
+ try:
411
+ async with asyncio.timeout(ASYNC_TIMEOUT_S) as deadline:
412
+ expiry = asyncio.create_task(
413
+ expire_on_first_refusal(server, client, deadline)
414
+ )
415
+ await client.__aenter__()
416
+ except TimeoutError:
417
+ assert expiry is not None
418
+ return expiry.result()
419
+ finally:
420
+ if expiry is not None:
421
+ expiry.cancel()
422
+ pytest.fail("entry ended before its deadline")
423
+
424
+
425
+ async def test_a_timed_out_entry_stops_connecting(
426
+ phoenix_server: FakePhoenixServer,
427
+ ) -> None:
428
+ client = make_client(phoenix_server, reconnect_policy=FAST_RECONNECT)
429
+ try:
430
+ with phoenix_server.refusing(REFUSE_EVERY_HANDSHAKE):
431
+ supervisor = await time_out_entry_on_first_refusal(phoenix_server, client)
432
+
433
+ assert supervisor is not None
434
+ assert supervisor.done()
435
+ assert client._state is ClientState.CLOSED
436
+
437
+ async with client: # the stopped client enters again once the server accepts
438
+ assert client.connection is not None
439
+ finally:
440
+ await asyncio.wait_for(client.shutdown(STOP_REASON), ASYNC_TIMEOUT_S)
441
+
442
+
443
+ async def test_a_second_cancel_during_entry_cleanup_still_stops_connecting(
444
+ phoenix_server: FakePhoenixServer,
445
+ ) -> None:
446
+ client = make_client(phoenix_server, reconnect_policy=FAST_RECONNECT)
447
+ try:
448
+ with phoenix_server.refusing(REFUSE_EVERY_HANDSHAKE):
449
+ entering = asyncio.create_task(client.__aenter__())
450
+ await a_handshake_was_refused(phoenix_server)
451
+ supervisor = client._supervisor_task
452
+ assert supervisor is not None
453
+
454
+ entering.cancel()
455
+ await asyncio.sleep(0) # the cancelled entry starts its cleanup shutdown
456
+ assert client._state is ClientState.SHUTTING_DOWN
457
+
458
+ entering.cancel()
459
+ with pytest.raises(asyncio.CancelledError):
460
+ await asyncio.wait_for(entering, ASYNC_TIMEOUT_S)
461
+
462
+ # Joins the cleanup the entry started; it was not abandoned.
463
+ await asyncio.wait_for(client.shutdown(STOP_REASON), ASYNC_TIMEOUT_S)
464
+ assert supervisor.done()
465
+ assert client._state is ClientState.CLOSED
466
+ assert client.connection is None
467
+ finally:
468
+ await asyncio.wait_for(client.shutdown(STOP_REASON), ASYNC_TIMEOUT_S)
469
+
470
+
471
+ async def test_closing_a_pending_entry_is_silent(
472
+ phoenix_server: FakePhoenixServer,
473
+ ) -> None:
474
+ client = make_client(phoenix_server)
475
+ phoenix_server.handshake_gate.clear()
476
+ entry = client.__aenter__()
477
+ try:
478
+ entry.send(None) # suspends at the initial-connection await
479
+ entry.close() # raises if entry awaits while handling GeneratorExit
480
+ finally:
481
+ phoenix_server.handshake_gate.set()
482
+ await asyncio.wait_for(client.shutdown(STOP_REASON), ASYNC_TIMEOUT_S)
483
+
484
+ # The closed entry no longer awaits its outcome; retrieve it here.
485
+ initial_connection = client._initial_connection_future
486
+ assert initial_connection is not None
487
+ assert isinstance(initial_connection.exception(), PHXConnectionError)