phoenix-channels-python-client 0.5.0__tar.gz → 0.5.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.
- {phoenix_channels_python_client-0.5.0/phoenix_channels_python_client.egg-info → phoenix_channels_python_client-0.5.1}/PKG-INFO +76 -74
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/README.md +75 -73
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/phoenix_channels_python_client/__init__.py +1 -1
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1/phoenix_channels_python_client.egg-info}/PKG-INFO +76 -74
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/pyproject.toml +1 -1
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/LICENSE +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/MANIFEST.in +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/phoenix_channels_python_client/authentication.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/phoenix_channels_python_client/channel.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/phoenix_channels_python_client/client.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/phoenix_channels_python_client/client_state_machine.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/phoenix_channels_python_client/client_types.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/phoenix_channels_python_client/codec.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/phoenix_channels_python_client/default_decider.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/phoenix_channels_python_client/diagnostics.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/phoenix_channels_python_client/exceptions.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/phoenix_channels_python_client/messages.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/phoenix_channels_python_client/protocol.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/phoenix_channels_python_client/protocol_handler.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/phoenix_channels_python_client/py.typed +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/phoenix_channels_python_client/reconnect_controller.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/phoenix_channels_python_client/replies.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/phoenix_channels_python_client/send_session.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/phoenix_channels_python_client/shutdown_signals.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/phoenix_channels_python_client/supervisor.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/phoenix_channels_python_client/topic_runtime.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/phoenix_channels_python_client/topic_subscription.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/phoenix_channels_python_client/utils.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/phoenix_channels_python_client.egg-info/SOURCES.txt +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/phoenix_channels_python_client.egg-info/dependency_links.txt +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/phoenix_channels_python_client.egg-info/requires.txt +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/phoenix_channels_python_client.egg-info/top_level.txt +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/setup.cfg +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_binary.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_cancel_and_wait.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_channel_api.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_channel_recovery.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_cleanup_ownership.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_client_state.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_codec.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_connection.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_heartbeat.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_heartbeat_timeout.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_join_installation.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_join_lifecycle.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_join_params.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_join_params_serialization.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_join_responses.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_optional_lifecycle.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_optional_reconnect.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_package_typing.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_protocol_handler.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_public_api.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_push.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_reconnect.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_reconnect_decider.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_reconnect_policy_invariants.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_reconnect_stress.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_run_forever_signals.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_security.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_send_session.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_shutdown_races.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_subscriptions.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_supervisor_internals.py +0 -0
- {phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/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.5.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: A Python client library for connecting to Phoenix Channels
|
|
5
5
|
Author: Phoenix Channels Python Client
|
|
6
6
|
License-Expression: MIT
|
|
@@ -181,10 +181,12 @@ async with asyncio.TaskGroup() as tasks:
|
|
|
181
181
|
loop.remove_signal_handler(signal.SIGTERM)
|
|
182
182
|
```
|
|
183
183
|
|
|
184
|
+
See [`examples/embedded_host_signals.py`](https://github.com/band-ai/phoenix-channels-python-client/blob/main/examples/embedded_host_signals.py) for a complete program.
|
|
185
|
+
|
|
184
186
|
From a plain `signal.signal` handler or another thread, use
|
|
185
187
|
`loop.call_soon_threadsafe(...)` instead: a bare `asyncio.create_task` there
|
|
186
188
|
doesn't wake a loop blocked on I/O, so the shutdown can stall until the next
|
|
187
|
-
network activity.
|
|
189
|
+
network activity.
|
|
188
190
|
|
|
189
191
|
With the default, handlers are installed only when `run_forever()` runs on the
|
|
190
192
|
main thread; elsewhere it just waits. A handler you registered with
|
|
@@ -237,7 +239,7 @@ string values `"1.0"` and `"2.0"`. Other values raise `ValueError`. It adds
|
|
|
237
239
|
|
|
238
240
|
## Usage Examples
|
|
239
241
|
|
|
240
|
-
###
|
|
242
|
+
### Joining a channel
|
|
241
243
|
|
|
242
244
|
Use `on_any` when you need the complete `Message`:
|
|
243
245
|
|
|
@@ -254,7 +256,7 @@ async def main() -> None:
|
|
|
254
256
|
async def message_handler(message: Message) -> None:
|
|
255
257
|
print(f"Topic: {message.topic}")
|
|
256
258
|
print(f"Event: {message.event}")
|
|
257
|
-
print(f"Payload: {message.payload}")
|
|
259
|
+
print(f"Payload: {message.payload!r}")
|
|
258
260
|
|
|
259
261
|
async with room:
|
|
260
262
|
await client.run_forever()
|
|
@@ -262,7 +264,7 @@ async def main() -> None:
|
|
|
262
264
|
asyncio.run(main())
|
|
263
265
|
```
|
|
264
266
|
|
|
265
|
-
##
|
|
267
|
+
## Catch-all vs event handlers
|
|
266
268
|
|
|
267
269
|
A channel supports one catch-all handler and one handler per event:
|
|
268
270
|
|
|
@@ -315,7 +317,7 @@ async def main() -> None:
|
|
|
315
317
|
asyncio.run(main())
|
|
316
318
|
```
|
|
317
319
|
|
|
318
|
-
##
|
|
320
|
+
## Leaving a channel
|
|
319
321
|
|
|
320
322
|
Leaving a channel stops delivery and cleans up its membership. Prefer a context
|
|
321
323
|
manager so exceptions and cancellation also trigger cleanup:
|
|
@@ -331,7 +333,7 @@ async def main() -> None:
|
|
|
331
333
|
|
|
332
334
|
@room.on_any
|
|
333
335
|
async def on_message(message: Message) -> None:
|
|
334
|
-
print(f"Message: {message.payload}")
|
|
336
|
+
print(f"Message: {message.payload!r}")
|
|
335
337
|
|
|
336
338
|
async with room:
|
|
337
339
|
await asyncio.sleep(5)
|
|
@@ -376,6 +378,47 @@ frame. Providers must be nonblocking and return a JSON-serializable dict;
|
|
|
376
378
|
provider/encoding failure raises `ChannelError` with its cause. A provider failure
|
|
377
379
|
during recovery permanently loses only that channel instead of retrying forever.
|
|
378
380
|
|
|
381
|
+
### Ordered join responses
|
|
382
|
+
|
|
383
|
+
Pass `on_join_response` when creating a channel to install a snapshot from every
|
|
384
|
+
initial join, socket rejoin and same-socket recovery:
|
|
385
|
+
|
|
386
|
+
```python
|
|
387
|
+
state = None
|
|
388
|
+
|
|
389
|
+
def install_snapshot(response):
|
|
390
|
+
global state
|
|
391
|
+
state = validate_snapshot(response)
|
|
392
|
+
|
|
393
|
+
room = client.channel("room:123", on_join_response=install_snapshot)
|
|
394
|
+
room.on_any(on_message)
|
|
395
|
+
async with room:
|
|
396
|
+
await client.run_forever()
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
`JoinResponseCallback` is exported from the package. The hook receives decoded
|
|
400
|
+
successful `payload.response`; a missing response is `None`. It runs synchronously
|
|
401
|
+
before the channel becomes joined, the join completes, or later events are
|
|
402
|
+
delivered. The client adds no application schema or empty snapshot. Join and leave
|
|
403
|
+
replies are handled separately from the bounded application-event queue.
|
|
404
|
+
|
|
405
|
+
Hooks must be short, nonblocking and return `None`. Async callables, awaitables
|
|
406
|
+
and other non-`None` results fail installation with `ChannelError`, as do hook
|
|
407
|
+
exceptions (including a directly raised `CancelledError`); the original cause is
|
|
408
|
+
preserved. Arbitrary prior hook effects are not rolled back. Do not wait on locks,
|
|
409
|
+
perform I/O, defer installation or change client lifecycle inside the hook. An
|
|
410
|
+
asyncio deadline cannot preempt synchronous code that blocks the loop.
|
|
411
|
+
|
|
412
|
+
Initial failure retires the admitted handle. Installation failure on an established
|
|
413
|
+
channel is reported once through `on_topic_lost`. Queue overflow and recovery can
|
|
414
|
+
still discard application events, so applications needing durable state must
|
|
415
|
+
resynchronize rather than assume replay.
|
|
416
|
+
|
|
417
|
+
Join replies match request refs on both protocols. V2 additionally checks a
|
|
418
|
+
present `join_ref`; a missing join_ref is accepted, including for broadcasts.
|
|
419
|
+
Membership controls use their available refs for correlation. V1 cannot provide
|
|
420
|
+
V2 isolation for arbitrary unsolicited events from old memberships.
|
|
421
|
+
|
|
379
422
|
## Pushing Events
|
|
380
423
|
|
|
381
424
|
Use `await channel.push(event, payload, timeout_s=10.0)` to send an application
|
|
@@ -396,7 +439,7 @@ async def main() -> None:
|
|
|
396
439
|
if reply.status == "ok":
|
|
397
440
|
print(reply.response)
|
|
398
441
|
else:
|
|
399
|
-
print(f"Server replied {reply.status}: {reply.response}")
|
|
442
|
+
print(f"Server replied {reply.status}: {reply.response!r}")
|
|
400
443
|
|
|
401
444
|
asyncio.run(main())
|
|
402
445
|
```
|
|
@@ -506,7 +549,7 @@ Only `websocket_url` may be positional; all other constructor options are keywor
|
|
|
506
549
|
| `join_timeout_s` | `10.0` | Finite positive join-operation deadline |
|
|
507
550
|
| `leave_timeout_s` | `5.0` | Finite positive leave-operation deadline |
|
|
508
551
|
| `max_topic_queue_size` | `1000` | Per-topic buffer; the oldest message is dropped when full |
|
|
509
|
-
| `callback_drain_timeout_s` | `2.0` | How long the message being handled (its
|
|
552
|
+
| `callback_drain_timeout_s` | `2.0` | How long the message being handled (its catch-all handler, then its event handler) may finish before a rejoin cancels it; queued messages are dropped |
|
|
510
553
|
| `on_reconnect` | `None` | `async () -> None`, called after required first-attempt reconciliation |
|
|
511
554
|
| `on_disconnect` | `None` | `async (error: Exception \| None) -> None`; `None` on a clean close |
|
|
512
555
|
| `on_heartbeat_ack` | `None` | Synchronous `() -> None`; runs on the message path, so keep it fast |
|
|
@@ -597,6 +640,26 @@ or rejoining. `await room.leave()` also works during recovery and stops future
|
|
|
597
640
|
rejoin attempts. Client lifecycle callbacks stay constructor options; `shutdown()`
|
|
598
641
|
cancels them, including a callback that itself called `shutdown()`.
|
|
599
642
|
|
|
643
|
+
### Optional reconnect participation
|
|
644
|
+
|
|
645
|
+
`client.channel(topic, *, params=None, on_join_response=None,
|
|
646
|
+
blocks_reconnect=True)` creates a channel that blocks the reconnect barrier by
|
|
647
|
+
default. Required channels reconcile their first socket-rejoin attempt sequentially
|
|
648
|
+
before `on_reconnect`; transient failures continue through recovery/backoff. The
|
|
649
|
+
barrier does not wait for eventual success of every required channel.
|
|
650
|
+
|
|
651
|
+
Set `blocks_reconnect=False` when creating an optional channel. Its owned recovery
|
|
652
|
+
task attempts an immediate join after required first attempts and retries transient
|
|
653
|
+
failures with the same backoff. Required delivery and `on_reconnect` do not wait for
|
|
654
|
+
optional replies, callback drains or retries. Explicit `await room.join()` still
|
|
655
|
+
waits for that channel's reply and installation.
|
|
656
|
+
|
|
657
|
+
`on_reconnect` does not promise optional readiness. Check each handle's `joined`
|
|
658
|
+
property and use `on_topic_lost` to replace permanent losses. Do not blindly create
|
|
659
|
+
replacement channels on every reconnect. Leave, shutdown and another socket
|
|
660
|
+
disconnect stop or hand over recovery; obsolete attempts cannot revive a left
|
|
661
|
+
handle.
|
|
662
|
+
|
|
600
663
|
### Heartbeats
|
|
601
664
|
|
|
602
665
|
The client sends a `heartbeat` on the `phoenix` topic every `heartbeat_interval_s`. By default, missed replies log warnings and sending continues with a new reference each interval.
|
|
@@ -629,8 +692,8 @@ logging.getLogger("phoenix_channels_python_client").setLevel(logging.DEBUG)
|
|
|
629
692
|
|
|
630
693
|
## Examples
|
|
631
694
|
|
|
632
|
-
- [`examples/minimal_phx_events_client.py`](examples/minimal_phx_events_client.py):
|
|
633
|
-
- [`examples/embedded_host_signals.py`](examples/embedded_host_signals.py): run the client in a host that owns its process signals.
|
|
695
|
+
- [`examples/minimal_phx_events_client.py`](https://github.com/band-ai/phoenix-channels-python-client/blob/main/examples/minimal_phx_events_client.py): join a channel and log messages until Ctrl+C or SIGTERM.
|
|
696
|
+
- [`examples/embedded_host_signals.py`](https://github.com/band-ai/phoenix-channels-python-client/blob/main/examples/embedded_host_signals.py): run the client in a host that owns its process signals.
|
|
634
697
|
|
|
635
698
|
---
|
|
636
699
|
|
|
@@ -659,10 +722,6 @@ async with Client(
|
|
|
659
722
|
...
|
|
660
723
|
```
|
|
661
724
|
|
|
662
|
-
---
|
|
663
|
-
|
|
664
|
-
**Need help?** Open an issue on GitHub or check the [Phoenix Channels documentation](https://hexdocs.pm/phoenix/channels.html) for more information about the Phoenix Channels protocol.
|
|
665
|
-
|
|
666
725
|
### Authentication and diagnostic safety
|
|
667
726
|
|
|
668
727
|
A nonempty `x-api-key` or `x-auth-token` handshake header selects header-only authentication on
|
|
@@ -683,63 +742,6 @@ Logs retain static lifecycle descriptions, native connection states,
|
|
|
683
742
|
and exception types. Application data and caller-visible errors are not changed;
|
|
684
743
|
applications remain responsible for logs outside this connection-owned boundary.
|
|
685
744
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
Pass `on_join_response` when creating a channel to install a snapshot from every
|
|
689
|
-
initial join, socket rejoin and same-socket recovery:
|
|
690
|
-
|
|
691
|
-
```python
|
|
692
|
-
state = None
|
|
693
|
-
|
|
694
|
-
def install_snapshot(response):
|
|
695
|
-
global state
|
|
696
|
-
state = validate_snapshot(response)
|
|
697
|
-
|
|
698
|
-
room = client.channel("room:123", on_join_response=install_snapshot)
|
|
699
|
-
room.on_any(on_message)
|
|
700
|
-
async with room:
|
|
701
|
-
await client.run_forever()
|
|
702
|
-
```
|
|
703
|
-
|
|
704
|
-
`JoinResponseCallback` is exported from the package. The hook receives decoded
|
|
705
|
-
successful `payload.response`; a missing response is `None`. It runs synchronously
|
|
706
|
-
before the channel becomes joined, the join completes, or later events are
|
|
707
|
-
delivered. The client adds no application schema or empty snapshot. Join and leave
|
|
708
|
-
replies are handled separately from the bounded application-event queue.
|
|
709
|
-
|
|
710
|
-
Hooks must be short, nonblocking and return `None`. Async callables, awaitables
|
|
711
|
-
and other non-`None` results fail installation with `ChannelError`, as do hook
|
|
712
|
-
exceptions (including a directly raised `CancelledError`); the original cause is
|
|
713
|
-
preserved. Arbitrary prior hook effects are not rolled back. Do not wait on locks,
|
|
714
|
-
perform I/O, defer installation or change client lifecycle inside the hook. An
|
|
715
|
-
asyncio deadline cannot preempt synchronous code that blocks the loop.
|
|
716
|
-
|
|
717
|
-
Initial failure retires the admitted handle. Installation failure on an established
|
|
718
|
-
channel is reported once through `on_topic_lost`. Queue overflow and recovery can
|
|
719
|
-
still discard application events, so applications needing durable state must
|
|
720
|
-
resynchronize rather than assume replay.
|
|
721
|
-
|
|
722
|
-
Join replies match request refs on both protocols. V2 additionally checks a
|
|
723
|
-
present `join_ref`; a missing join_ref is accepted, including for broadcasts.
|
|
724
|
-
Membership controls use their available refs for correlation. V1 cannot provide
|
|
725
|
-
V2 isolation for arbitrary unsolicited events from old memberships.
|
|
726
|
-
|
|
727
|
-
### Optional reconnect participation
|
|
728
|
-
|
|
729
|
-
`client.channel(topic, *, params=None, on_join_response=None,
|
|
730
|
-
blocks_reconnect=True)` creates a channel that blocks the reconnect barrier by
|
|
731
|
-
default. Required channels reconcile their first socket-rejoin attempt sequentially
|
|
732
|
-
before `on_reconnect`; transient failures continue through recovery/backoff. The
|
|
733
|
-
barrier does not wait for eventual success of every required channel.
|
|
734
|
-
|
|
735
|
-
Set `blocks_reconnect=False` when creating an optional channel. Its owned recovery
|
|
736
|
-
task attempts an immediate join after required first attempts and retries transient
|
|
737
|
-
failures with the same backoff. Required delivery and `on_reconnect` do not wait for
|
|
738
|
-
optional replies, callback drains or retries. Explicit `await room.join()` still
|
|
739
|
-
waits for that channel's reply and installation.
|
|
745
|
+
---
|
|
740
746
|
|
|
741
|
-
|
|
742
|
-
property and use `on_topic_lost` to replace permanent losses. Do not blindly create
|
|
743
|
-
replacement channels on every reconnect. Leave, shutdown and another socket
|
|
744
|
-
disconnect stop or hand over recovery; obsolete attempts cannot revive a left
|
|
745
|
-
handle.
|
|
747
|
+
**Need help?** Open an issue on GitHub or check the [Phoenix Channels documentation](https://hexdocs.pm/phoenix/channels.html) for more information about the Phoenix Channels protocol.
|
|
@@ -148,10 +148,12 @@ async with asyncio.TaskGroup() as tasks:
|
|
|
148
148
|
loop.remove_signal_handler(signal.SIGTERM)
|
|
149
149
|
```
|
|
150
150
|
|
|
151
|
+
See [`examples/embedded_host_signals.py`](https://github.com/band-ai/phoenix-channels-python-client/blob/main/examples/embedded_host_signals.py) for a complete program.
|
|
152
|
+
|
|
151
153
|
From a plain `signal.signal` handler or another thread, use
|
|
152
154
|
`loop.call_soon_threadsafe(...)` instead: a bare `asyncio.create_task` there
|
|
153
155
|
doesn't wake a loop blocked on I/O, so the shutdown can stall until the next
|
|
154
|
-
network activity.
|
|
156
|
+
network activity.
|
|
155
157
|
|
|
156
158
|
With the default, handlers are installed only when `run_forever()` runs on the
|
|
157
159
|
main thread; elsewhere it just waits. A handler you registered with
|
|
@@ -204,7 +206,7 @@ string values `"1.0"` and `"2.0"`. Other values raise `ValueError`. It adds
|
|
|
204
206
|
|
|
205
207
|
## Usage Examples
|
|
206
208
|
|
|
207
|
-
###
|
|
209
|
+
### Joining a channel
|
|
208
210
|
|
|
209
211
|
Use `on_any` when you need the complete `Message`:
|
|
210
212
|
|
|
@@ -221,7 +223,7 @@ async def main() -> None:
|
|
|
221
223
|
async def message_handler(message: Message) -> None:
|
|
222
224
|
print(f"Topic: {message.topic}")
|
|
223
225
|
print(f"Event: {message.event}")
|
|
224
|
-
print(f"Payload: {message.payload}")
|
|
226
|
+
print(f"Payload: {message.payload!r}")
|
|
225
227
|
|
|
226
228
|
async with room:
|
|
227
229
|
await client.run_forever()
|
|
@@ -229,7 +231,7 @@ async def main() -> None:
|
|
|
229
231
|
asyncio.run(main())
|
|
230
232
|
```
|
|
231
233
|
|
|
232
|
-
##
|
|
234
|
+
## Catch-all vs event handlers
|
|
233
235
|
|
|
234
236
|
A channel supports one catch-all handler and one handler per event:
|
|
235
237
|
|
|
@@ -282,7 +284,7 @@ async def main() -> None:
|
|
|
282
284
|
asyncio.run(main())
|
|
283
285
|
```
|
|
284
286
|
|
|
285
|
-
##
|
|
287
|
+
## Leaving a channel
|
|
286
288
|
|
|
287
289
|
Leaving a channel stops delivery and cleans up its membership. Prefer a context
|
|
288
290
|
manager so exceptions and cancellation also trigger cleanup:
|
|
@@ -298,7 +300,7 @@ async def main() -> None:
|
|
|
298
300
|
|
|
299
301
|
@room.on_any
|
|
300
302
|
async def on_message(message: Message) -> None:
|
|
301
|
-
print(f"Message: {message.payload}")
|
|
303
|
+
print(f"Message: {message.payload!r}")
|
|
302
304
|
|
|
303
305
|
async with room:
|
|
304
306
|
await asyncio.sleep(5)
|
|
@@ -343,6 +345,47 @@ frame. Providers must be nonblocking and return a JSON-serializable dict;
|
|
|
343
345
|
provider/encoding failure raises `ChannelError` with its cause. A provider failure
|
|
344
346
|
during recovery permanently loses only that channel instead of retrying forever.
|
|
345
347
|
|
|
348
|
+
### Ordered join responses
|
|
349
|
+
|
|
350
|
+
Pass `on_join_response` when creating a channel to install a snapshot from every
|
|
351
|
+
initial join, socket rejoin and same-socket recovery:
|
|
352
|
+
|
|
353
|
+
```python
|
|
354
|
+
state = None
|
|
355
|
+
|
|
356
|
+
def install_snapshot(response):
|
|
357
|
+
global state
|
|
358
|
+
state = validate_snapshot(response)
|
|
359
|
+
|
|
360
|
+
room = client.channel("room:123", on_join_response=install_snapshot)
|
|
361
|
+
room.on_any(on_message)
|
|
362
|
+
async with room:
|
|
363
|
+
await client.run_forever()
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
`JoinResponseCallback` is exported from the package. The hook receives decoded
|
|
367
|
+
successful `payload.response`; a missing response is `None`. It runs synchronously
|
|
368
|
+
before the channel becomes joined, the join completes, or later events are
|
|
369
|
+
delivered. The client adds no application schema or empty snapshot. Join and leave
|
|
370
|
+
replies are handled separately from the bounded application-event queue.
|
|
371
|
+
|
|
372
|
+
Hooks must be short, nonblocking and return `None`. Async callables, awaitables
|
|
373
|
+
and other non-`None` results fail installation with `ChannelError`, as do hook
|
|
374
|
+
exceptions (including a directly raised `CancelledError`); the original cause is
|
|
375
|
+
preserved. Arbitrary prior hook effects are not rolled back. Do not wait on locks,
|
|
376
|
+
perform I/O, defer installation or change client lifecycle inside the hook. An
|
|
377
|
+
asyncio deadline cannot preempt synchronous code that blocks the loop.
|
|
378
|
+
|
|
379
|
+
Initial failure retires the admitted handle. Installation failure on an established
|
|
380
|
+
channel is reported once through `on_topic_lost`. Queue overflow and recovery can
|
|
381
|
+
still discard application events, so applications needing durable state must
|
|
382
|
+
resynchronize rather than assume replay.
|
|
383
|
+
|
|
384
|
+
Join replies match request refs on both protocols. V2 additionally checks a
|
|
385
|
+
present `join_ref`; a missing join_ref is accepted, including for broadcasts.
|
|
386
|
+
Membership controls use their available refs for correlation. V1 cannot provide
|
|
387
|
+
V2 isolation for arbitrary unsolicited events from old memberships.
|
|
388
|
+
|
|
346
389
|
## Pushing Events
|
|
347
390
|
|
|
348
391
|
Use `await channel.push(event, payload, timeout_s=10.0)` to send an application
|
|
@@ -363,7 +406,7 @@ async def main() -> None:
|
|
|
363
406
|
if reply.status == "ok":
|
|
364
407
|
print(reply.response)
|
|
365
408
|
else:
|
|
366
|
-
print(f"Server replied {reply.status}: {reply.response}")
|
|
409
|
+
print(f"Server replied {reply.status}: {reply.response!r}")
|
|
367
410
|
|
|
368
411
|
asyncio.run(main())
|
|
369
412
|
```
|
|
@@ -473,7 +516,7 @@ Only `websocket_url` may be positional; all other constructor options are keywor
|
|
|
473
516
|
| `join_timeout_s` | `10.0` | Finite positive join-operation deadline |
|
|
474
517
|
| `leave_timeout_s` | `5.0` | Finite positive leave-operation deadline |
|
|
475
518
|
| `max_topic_queue_size` | `1000` | Per-topic buffer; the oldest message is dropped when full |
|
|
476
|
-
| `callback_drain_timeout_s` | `2.0` | How long the message being handled (its
|
|
519
|
+
| `callback_drain_timeout_s` | `2.0` | How long the message being handled (its catch-all handler, then its event handler) may finish before a rejoin cancels it; queued messages are dropped |
|
|
477
520
|
| `on_reconnect` | `None` | `async () -> None`, called after required first-attempt reconciliation |
|
|
478
521
|
| `on_disconnect` | `None` | `async (error: Exception \| None) -> None`; `None` on a clean close |
|
|
479
522
|
| `on_heartbeat_ack` | `None` | Synchronous `() -> None`; runs on the message path, so keep it fast |
|
|
@@ -564,6 +607,26 @@ or rejoining. `await room.leave()` also works during recovery and stops future
|
|
|
564
607
|
rejoin attempts. Client lifecycle callbacks stay constructor options; `shutdown()`
|
|
565
608
|
cancels them, including a callback that itself called `shutdown()`.
|
|
566
609
|
|
|
610
|
+
### Optional reconnect participation
|
|
611
|
+
|
|
612
|
+
`client.channel(topic, *, params=None, on_join_response=None,
|
|
613
|
+
blocks_reconnect=True)` creates a channel that blocks the reconnect barrier by
|
|
614
|
+
default. Required channels reconcile their first socket-rejoin attempt sequentially
|
|
615
|
+
before `on_reconnect`; transient failures continue through recovery/backoff. The
|
|
616
|
+
barrier does not wait for eventual success of every required channel.
|
|
617
|
+
|
|
618
|
+
Set `blocks_reconnect=False` when creating an optional channel. Its owned recovery
|
|
619
|
+
task attempts an immediate join after required first attempts and retries transient
|
|
620
|
+
failures with the same backoff. Required delivery and `on_reconnect` do not wait for
|
|
621
|
+
optional replies, callback drains or retries. Explicit `await room.join()` still
|
|
622
|
+
waits for that channel's reply and installation.
|
|
623
|
+
|
|
624
|
+
`on_reconnect` does not promise optional readiness. Check each handle's `joined`
|
|
625
|
+
property and use `on_topic_lost` to replace permanent losses. Do not blindly create
|
|
626
|
+
replacement channels on every reconnect. Leave, shutdown and another socket
|
|
627
|
+
disconnect stop or hand over recovery; obsolete attempts cannot revive a left
|
|
628
|
+
handle.
|
|
629
|
+
|
|
567
630
|
### Heartbeats
|
|
568
631
|
|
|
569
632
|
The client sends a `heartbeat` on the `phoenix` topic every `heartbeat_interval_s`. By default, missed replies log warnings and sending continues with a new reference each interval.
|
|
@@ -596,8 +659,8 @@ logging.getLogger("phoenix_channels_python_client").setLevel(logging.DEBUG)
|
|
|
596
659
|
|
|
597
660
|
## Examples
|
|
598
661
|
|
|
599
|
-
- [`examples/minimal_phx_events_client.py`](examples/minimal_phx_events_client.py):
|
|
600
|
-
- [`examples/embedded_host_signals.py`](examples/embedded_host_signals.py): run the client in a host that owns its process signals.
|
|
662
|
+
- [`examples/minimal_phx_events_client.py`](https://github.com/band-ai/phoenix-channels-python-client/blob/main/examples/minimal_phx_events_client.py): join a channel and log messages until Ctrl+C or SIGTERM.
|
|
663
|
+
- [`examples/embedded_host_signals.py`](https://github.com/band-ai/phoenix-channels-python-client/blob/main/examples/embedded_host_signals.py): run the client in a host that owns its process signals.
|
|
601
664
|
|
|
602
665
|
---
|
|
603
666
|
|
|
@@ -626,10 +689,6 @@ async with Client(
|
|
|
626
689
|
...
|
|
627
690
|
```
|
|
628
691
|
|
|
629
|
-
---
|
|
630
|
-
|
|
631
|
-
**Need help?** Open an issue on GitHub or check the [Phoenix Channels documentation](https://hexdocs.pm/phoenix/channels.html) for more information about the Phoenix Channels protocol.
|
|
632
|
-
|
|
633
692
|
### Authentication and diagnostic safety
|
|
634
693
|
|
|
635
694
|
A nonempty `x-api-key` or `x-auth-token` handshake header selects header-only authentication on
|
|
@@ -650,63 +709,6 @@ Logs retain static lifecycle descriptions, native connection states,
|
|
|
650
709
|
and exception types. Application data and caller-visible errors are not changed;
|
|
651
710
|
applications remain responsible for logs outside this connection-owned boundary.
|
|
652
711
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
Pass `on_join_response` when creating a channel to install a snapshot from every
|
|
656
|
-
initial join, socket rejoin and same-socket recovery:
|
|
657
|
-
|
|
658
|
-
```python
|
|
659
|
-
state = None
|
|
660
|
-
|
|
661
|
-
def install_snapshot(response):
|
|
662
|
-
global state
|
|
663
|
-
state = validate_snapshot(response)
|
|
664
|
-
|
|
665
|
-
room = client.channel("room:123", on_join_response=install_snapshot)
|
|
666
|
-
room.on_any(on_message)
|
|
667
|
-
async with room:
|
|
668
|
-
await client.run_forever()
|
|
669
|
-
```
|
|
670
|
-
|
|
671
|
-
`JoinResponseCallback` is exported from the package. The hook receives decoded
|
|
672
|
-
successful `payload.response`; a missing response is `None`. It runs synchronously
|
|
673
|
-
before the channel becomes joined, the join completes, or later events are
|
|
674
|
-
delivered. The client adds no application schema or empty snapshot. Join and leave
|
|
675
|
-
replies are handled separately from the bounded application-event queue.
|
|
676
|
-
|
|
677
|
-
Hooks must be short, nonblocking and return `None`. Async callables, awaitables
|
|
678
|
-
and other non-`None` results fail installation with `ChannelError`, as do hook
|
|
679
|
-
exceptions (including a directly raised `CancelledError`); the original cause is
|
|
680
|
-
preserved. Arbitrary prior hook effects are not rolled back. Do not wait on locks,
|
|
681
|
-
perform I/O, defer installation or change client lifecycle inside the hook. An
|
|
682
|
-
asyncio deadline cannot preempt synchronous code that blocks the loop.
|
|
683
|
-
|
|
684
|
-
Initial failure retires the admitted handle. Installation failure on an established
|
|
685
|
-
channel is reported once through `on_topic_lost`. Queue overflow and recovery can
|
|
686
|
-
still discard application events, so applications needing durable state must
|
|
687
|
-
resynchronize rather than assume replay.
|
|
688
|
-
|
|
689
|
-
Join replies match request refs on both protocols. V2 additionally checks a
|
|
690
|
-
present `join_ref`; a missing join_ref is accepted, including for broadcasts.
|
|
691
|
-
Membership controls use their available refs for correlation. V1 cannot provide
|
|
692
|
-
V2 isolation for arbitrary unsolicited events from old memberships.
|
|
693
|
-
|
|
694
|
-
### Optional reconnect participation
|
|
695
|
-
|
|
696
|
-
`client.channel(topic, *, params=None, on_join_response=None,
|
|
697
|
-
blocks_reconnect=True)` creates a channel that blocks the reconnect barrier by
|
|
698
|
-
default. Required channels reconcile their first socket-rejoin attempt sequentially
|
|
699
|
-
before `on_reconnect`; transient failures continue through recovery/backoff. The
|
|
700
|
-
barrier does not wait for eventual success of every required channel.
|
|
701
|
-
|
|
702
|
-
Set `blocks_reconnect=False` when creating an optional channel. Its owned recovery
|
|
703
|
-
task attempts an immediate join after required first attempts and retries transient
|
|
704
|
-
failures with the same backoff. Required delivery and `on_reconnect` do not wait for
|
|
705
|
-
optional replies, callback drains or retries. Explicit `await room.join()` still
|
|
706
|
-
waits for that channel's reply and installation.
|
|
712
|
+
---
|
|
707
713
|
|
|
708
|
-
|
|
709
|
-
property and use `on_topic_lost` to replace permanent losses. Do not blindly create
|
|
710
|
-
replacement channels on every reconnect. Leave, shutdown and another socket
|
|
711
|
-
disconnect stop or hand over recovery; obsolete attempts cannot revive a left
|
|
712
|
-
handle.
|
|
714
|
+
**Need help?** Open an issue on GitHub or check the [Phoenix Channels documentation](https://hexdocs.pm/phoenix/channels.html) for more information about the Phoenix Channels protocol.
|
|
@@ -27,7 +27,7 @@ from phoenix_channels_python_client.messages import Message, Payload, SystemEven
|
|
|
27
27
|
from phoenix_channels_python_client.protocol import ProtocolVersion
|
|
28
28
|
from phoenix_channels_python_client.replies import Reply
|
|
29
29
|
|
|
30
|
-
__version__ = "0.5.
|
|
30
|
+
__version__ = "0.5.1"
|
|
31
31
|
|
|
32
32
|
__all__ = [
|
|
33
33
|
"Channel",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: phoenix-channels-python-client
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: A Python client library for connecting to Phoenix Channels
|
|
5
5
|
Author: Phoenix Channels Python Client
|
|
6
6
|
License-Expression: MIT
|
|
@@ -181,10 +181,12 @@ async with asyncio.TaskGroup() as tasks:
|
|
|
181
181
|
loop.remove_signal_handler(signal.SIGTERM)
|
|
182
182
|
```
|
|
183
183
|
|
|
184
|
+
See [`examples/embedded_host_signals.py`](https://github.com/band-ai/phoenix-channels-python-client/blob/main/examples/embedded_host_signals.py) for a complete program.
|
|
185
|
+
|
|
184
186
|
From a plain `signal.signal` handler or another thread, use
|
|
185
187
|
`loop.call_soon_threadsafe(...)` instead: a bare `asyncio.create_task` there
|
|
186
188
|
doesn't wake a loop blocked on I/O, so the shutdown can stall until the next
|
|
187
|
-
network activity.
|
|
189
|
+
network activity.
|
|
188
190
|
|
|
189
191
|
With the default, handlers are installed only when `run_forever()` runs on the
|
|
190
192
|
main thread; elsewhere it just waits. A handler you registered with
|
|
@@ -237,7 +239,7 @@ string values `"1.0"` and `"2.0"`. Other values raise `ValueError`. It adds
|
|
|
237
239
|
|
|
238
240
|
## Usage Examples
|
|
239
241
|
|
|
240
|
-
###
|
|
242
|
+
### Joining a channel
|
|
241
243
|
|
|
242
244
|
Use `on_any` when you need the complete `Message`:
|
|
243
245
|
|
|
@@ -254,7 +256,7 @@ async def main() -> None:
|
|
|
254
256
|
async def message_handler(message: Message) -> None:
|
|
255
257
|
print(f"Topic: {message.topic}")
|
|
256
258
|
print(f"Event: {message.event}")
|
|
257
|
-
print(f"Payload: {message.payload}")
|
|
259
|
+
print(f"Payload: {message.payload!r}")
|
|
258
260
|
|
|
259
261
|
async with room:
|
|
260
262
|
await client.run_forever()
|
|
@@ -262,7 +264,7 @@ async def main() -> None:
|
|
|
262
264
|
asyncio.run(main())
|
|
263
265
|
```
|
|
264
266
|
|
|
265
|
-
##
|
|
267
|
+
## Catch-all vs event handlers
|
|
266
268
|
|
|
267
269
|
A channel supports one catch-all handler and one handler per event:
|
|
268
270
|
|
|
@@ -315,7 +317,7 @@ async def main() -> None:
|
|
|
315
317
|
asyncio.run(main())
|
|
316
318
|
```
|
|
317
319
|
|
|
318
|
-
##
|
|
320
|
+
## Leaving a channel
|
|
319
321
|
|
|
320
322
|
Leaving a channel stops delivery and cleans up its membership. Prefer a context
|
|
321
323
|
manager so exceptions and cancellation also trigger cleanup:
|
|
@@ -331,7 +333,7 @@ async def main() -> None:
|
|
|
331
333
|
|
|
332
334
|
@room.on_any
|
|
333
335
|
async def on_message(message: Message) -> None:
|
|
334
|
-
print(f"Message: {message.payload}")
|
|
336
|
+
print(f"Message: {message.payload!r}")
|
|
335
337
|
|
|
336
338
|
async with room:
|
|
337
339
|
await asyncio.sleep(5)
|
|
@@ -376,6 +378,47 @@ frame. Providers must be nonblocking and return a JSON-serializable dict;
|
|
|
376
378
|
provider/encoding failure raises `ChannelError` with its cause. A provider failure
|
|
377
379
|
during recovery permanently loses only that channel instead of retrying forever.
|
|
378
380
|
|
|
381
|
+
### Ordered join responses
|
|
382
|
+
|
|
383
|
+
Pass `on_join_response` when creating a channel to install a snapshot from every
|
|
384
|
+
initial join, socket rejoin and same-socket recovery:
|
|
385
|
+
|
|
386
|
+
```python
|
|
387
|
+
state = None
|
|
388
|
+
|
|
389
|
+
def install_snapshot(response):
|
|
390
|
+
global state
|
|
391
|
+
state = validate_snapshot(response)
|
|
392
|
+
|
|
393
|
+
room = client.channel("room:123", on_join_response=install_snapshot)
|
|
394
|
+
room.on_any(on_message)
|
|
395
|
+
async with room:
|
|
396
|
+
await client.run_forever()
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
`JoinResponseCallback` is exported from the package. The hook receives decoded
|
|
400
|
+
successful `payload.response`; a missing response is `None`. It runs synchronously
|
|
401
|
+
before the channel becomes joined, the join completes, or later events are
|
|
402
|
+
delivered. The client adds no application schema or empty snapshot. Join and leave
|
|
403
|
+
replies are handled separately from the bounded application-event queue.
|
|
404
|
+
|
|
405
|
+
Hooks must be short, nonblocking and return `None`. Async callables, awaitables
|
|
406
|
+
and other non-`None` results fail installation with `ChannelError`, as do hook
|
|
407
|
+
exceptions (including a directly raised `CancelledError`); the original cause is
|
|
408
|
+
preserved. Arbitrary prior hook effects are not rolled back. Do not wait on locks,
|
|
409
|
+
perform I/O, defer installation or change client lifecycle inside the hook. An
|
|
410
|
+
asyncio deadline cannot preempt synchronous code that blocks the loop.
|
|
411
|
+
|
|
412
|
+
Initial failure retires the admitted handle. Installation failure on an established
|
|
413
|
+
channel is reported once through `on_topic_lost`. Queue overflow and recovery can
|
|
414
|
+
still discard application events, so applications needing durable state must
|
|
415
|
+
resynchronize rather than assume replay.
|
|
416
|
+
|
|
417
|
+
Join replies match request refs on both protocols. V2 additionally checks a
|
|
418
|
+
present `join_ref`; a missing join_ref is accepted, including for broadcasts.
|
|
419
|
+
Membership controls use their available refs for correlation. V1 cannot provide
|
|
420
|
+
V2 isolation for arbitrary unsolicited events from old memberships.
|
|
421
|
+
|
|
379
422
|
## Pushing Events
|
|
380
423
|
|
|
381
424
|
Use `await channel.push(event, payload, timeout_s=10.0)` to send an application
|
|
@@ -396,7 +439,7 @@ async def main() -> None:
|
|
|
396
439
|
if reply.status == "ok":
|
|
397
440
|
print(reply.response)
|
|
398
441
|
else:
|
|
399
|
-
print(f"Server replied {reply.status}: {reply.response}")
|
|
442
|
+
print(f"Server replied {reply.status}: {reply.response!r}")
|
|
400
443
|
|
|
401
444
|
asyncio.run(main())
|
|
402
445
|
```
|
|
@@ -506,7 +549,7 @@ Only `websocket_url` may be positional; all other constructor options are keywor
|
|
|
506
549
|
| `join_timeout_s` | `10.0` | Finite positive join-operation deadline |
|
|
507
550
|
| `leave_timeout_s` | `5.0` | Finite positive leave-operation deadline |
|
|
508
551
|
| `max_topic_queue_size` | `1000` | Per-topic buffer; the oldest message is dropped when full |
|
|
509
|
-
| `callback_drain_timeout_s` | `2.0` | How long the message being handled (its
|
|
552
|
+
| `callback_drain_timeout_s` | `2.0` | How long the message being handled (its catch-all handler, then its event handler) may finish before a rejoin cancels it; queued messages are dropped |
|
|
510
553
|
| `on_reconnect` | `None` | `async () -> None`, called after required first-attempt reconciliation |
|
|
511
554
|
| `on_disconnect` | `None` | `async (error: Exception \| None) -> None`; `None` on a clean close |
|
|
512
555
|
| `on_heartbeat_ack` | `None` | Synchronous `() -> None`; runs on the message path, so keep it fast |
|
|
@@ -597,6 +640,26 @@ or rejoining. `await room.leave()` also works during recovery and stops future
|
|
|
597
640
|
rejoin attempts. Client lifecycle callbacks stay constructor options; `shutdown()`
|
|
598
641
|
cancels them, including a callback that itself called `shutdown()`.
|
|
599
642
|
|
|
643
|
+
### Optional reconnect participation
|
|
644
|
+
|
|
645
|
+
`client.channel(topic, *, params=None, on_join_response=None,
|
|
646
|
+
blocks_reconnect=True)` creates a channel that blocks the reconnect barrier by
|
|
647
|
+
default. Required channels reconcile their first socket-rejoin attempt sequentially
|
|
648
|
+
before `on_reconnect`; transient failures continue through recovery/backoff. The
|
|
649
|
+
barrier does not wait for eventual success of every required channel.
|
|
650
|
+
|
|
651
|
+
Set `blocks_reconnect=False` when creating an optional channel. Its owned recovery
|
|
652
|
+
task attempts an immediate join after required first attempts and retries transient
|
|
653
|
+
failures with the same backoff. Required delivery and `on_reconnect` do not wait for
|
|
654
|
+
optional replies, callback drains or retries. Explicit `await room.join()` still
|
|
655
|
+
waits for that channel's reply and installation.
|
|
656
|
+
|
|
657
|
+
`on_reconnect` does not promise optional readiness. Check each handle's `joined`
|
|
658
|
+
property and use `on_topic_lost` to replace permanent losses. Do not blindly create
|
|
659
|
+
replacement channels on every reconnect. Leave, shutdown and another socket
|
|
660
|
+
disconnect stop or hand over recovery; obsolete attempts cannot revive a left
|
|
661
|
+
handle.
|
|
662
|
+
|
|
600
663
|
### Heartbeats
|
|
601
664
|
|
|
602
665
|
The client sends a `heartbeat` on the `phoenix` topic every `heartbeat_interval_s`. By default, missed replies log warnings and sending continues with a new reference each interval.
|
|
@@ -629,8 +692,8 @@ logging.getLogger("phoenix_channels_python_client").setLevel(logging.DEBUG)
|
|
|
629
692
|
|
|
630
693
|
## Examples
|
|
631
694
|
|
|
632
|
-
- [`examples/minimal_phx_events_client.py`](examples/minimal_phx_events_client.py):
|
|
633
|
-
- [`examples/embedded_host_signals.py`](examples/embedded_host_signals.py): run the client in a host that owns its process signals.
|
|
695
|
+
- [`examples/minimal_phx_events_client.py`](https://github.com/band-ai/phoenix-channels-python-client/blob/main/examples/minimal_phx_events_client.py): join a channel and log messages until Ctrl+C or SIGTERM.
|
|
696
|
+
- [`examples/embedded_host_signals.py`](https://github.com/band-ai/phoenix-channels-python-client/blob/main/examples/embedded_host_signals.py): run the client in a host that owns its process signals.
|
|
634
697
|
|
|
635
698
|
---
|
|
636
699
|
|
|
@@ -659,10 +722,6 @@ async with Client(
|
|
|
659
722
|
...
|
|
660
723
|
```
|
|
661
724
|
|
|
662
|
-
---
|
|
663
|
-
|
|
664
|
-
**Need help?** Open an issue on GitHub or check the [Phoenix Channels documentation](https://hexdocs.pm/phoenix/channels.html) for more information about the Phoenix Channels protocol.
|
|
665
|
-
|
|
666
725
|
### Authentication and diagnostic safety
|
|
667
726
|
|
|
668
727
|
A nonempty `x-api-key` or `x-auth-token` handshake header selects header-only authentication on
|
|
@@ -683,63 +742,6 @@ Logs retain static lifecycle descriptions, native connection states,
|
|
|
683
742
|
and exception types. Application data and caller-visible errors are not changed;
|
|
684
743
|
applications remain responsible for logs outside this connection-owned boundary.
|
|
685
744
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
Pass `on_join_response` when creating a channel to install a snapshot from every
|
|
689
|
-
initial join, socket rejoin and same-socket recovery:
|
|
690
|
-
|
|
691
|
-
```python
|
|
692
|
-
state = None
|
|
693
|
-
|
|
694
|
-
def install_snapshot(response):
|
|
695
|
-
global state
|
|
696
|
-
state = validate_snapshot(response)
|
|
697
|
-
|
|
698
|
-
room = client.channel("room:123", on_join_response=install_snapshot)
|
|
699
|
-
room.on_any(on_message)
|
|
700
|
-
async with room:
|
|
701
|
-
await client.run_forever()
|
|
702
|
-
```
|
|
703
|
-
|
|
704
|
-
`JoinResponseCallback` is exported from the package. The hook receives decoded
|
|
705
|
-
successful `payload.response`; a missing response is `None`. It runs synchronously
|
|
706
|
-
before the channel becomes joined, the join completes, or later events are
|
|
707
|
-
delivered. The client adds no application schema or empty snapshot. Join and leave
|
|
708
|
-
replies are handled separately from the bounded application-event queue.
|
|
709
|
-
|
|
710
|
-
Hooks must be short, nonblocking and return `None`. Async callables, awaitables
|
|
711
|
-
and other non-`None` results fail installation with `ChannelError`, as do hook
|
|
712
|
-
exceptions (including a directly raised `CancelledError`); the original cause is
|
|
713
|
-
preserved. Arbitrary prior hook effects are not rolled back. Do not wait on locks,
|
|
714
|
-
perform I/O, defer installation or change client lifecycle inside the hook. An
|
|
715
|
-
asyncio deadline cannot preempt synchronous code that blocks the loop.
|
|
716
|
-
|
|
717
|
-
Initial failure retires the admitted handle. Installation failure on an established
|
|
718
|
-
channel is reported once through `on_topic_lost`. Queue overflow and recovery can
|
|
719
|
-
still discard application events, so applications needing durable state must
|
|
720
|
-
resynchronize rather than assume replay.
|
|
721
|
-
|
|
722
|
-
Join replies match request refs on both protocols. V2 additionally checks a
|
|
723
|
-
present `join_ref`; a missing join_ref is accepted, including for broadcasts.
|
|
724
|
-
Membership controls use their available refs for correlation. V1 cannot provide
|
|
725
|
-
V2 isolation for arbitrary unsolicited events from old memberships.
|
|
726
|
-
|
|
727
|
-
### Optional reconnect participation
|
|
728
|
-
|
|
729
|
-
`client.channel(topic, *, params=None, on_join_response=None,
|
|
730
|
-
blocks_reconnect=True)` creates a channel that blocks the reconnect barrier by
|
|
731
|
-
default. Required channels reconcile their first socket-rejoin attempt sequentially
|
|
732
|
-
before `on_reconnect`; transient failures continue through recovery/backoff. The
|
|
733
|
-
barrier does not wait for eventual success of every required channel.
|
|
734
|
-
|
|
735
|
-
Set `blocks_reconnect=False` when creating an optional channel. Its owned recovery
|
|
736
|
-
task attempts an immediate join after required first attempts and retries transient
|
|
737
|
-
failures with the same backoff. Required delivery and `on_reconnect` do not wait for
|
|
738
|
-
optional replies, callback drains or retries. Explicit `await room.join()` still
|
|
739
|
-
waits for that channel's reply and installation.
|
|
745
|
+
---
|
|
740
746
|
|
|
741
|
-
|
|
742
|
-
property and use `on_topic_lost` to replace permanent losses. Do not blindly create
|
|
743
|
-
replacement channels on every reconnect. Leave, shutdown and another socket
|
|
744
|
-
disconnect stop or hand over recovery; obsolete attempts cannot revive a left
|
|
745
|
-
handle.
|
|
747
|
+
**Need help?** Open an issue on GitHub or check the [Phoenix Channels documentation](https://hexdocs.pm/phoenix/channels.html) for more information about the Phoenix Channels protocol.
|
|
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
|
|
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
|
{phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_binary.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_codec.py
RENAMED
|
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
|
{phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_push.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{phoenix_channels_python_client-0.5.0 → phoenix_channels_python_client-0.5.1}/tests/test_security.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|