phoenix-channels-python-client 0.2.4__tar.gz → 0.3.0__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.3.0/PKG-INFO +383 -0
- phoenix_channels_python_client-0.3.0/README.md +352 -0
- {phoenix_channels_python_client-0.2.4 → phoenix_channels_python_client-0.3.0}/phoenix_channels_python_client/__init__.py +5 -4
- {phoenix_channels_python_client-0.2.4 → phoenix_channels_python_client-0.3.0}/phoenix_channels_python_client/client.py +71 -65
- {phoenix_channels_python_client-0.2.4 → phoenix_channels_python_client-0.3.0}/phoenix_channels_python_client/client_state_machine.py +1 -3
- phoenix_channels_python_client-0.3.0/phoenix_channels_python_client/client_types.py +73 -0
- {phoenix_channels_python_client-0.2.4 → phoenix_channels_python_client-0.3.0}/phoenix_channels_python_client/exceptions.py +0 -2
- {phoenix_channels_python_client-0.2.4 → phoenix_channels_python_client-0.3.0}/phoenix_channels_python_client/phx_messages.py +16 -9
- {phoenix_channels_python_client-0.2.4 → phoenix_channels_python_client-0.3.0}/phoenix_channels_python_client/protocol_handler.py +107 -68
- {phoenix_channels_python_client-0.2.4 → phoenix_channels_python_client-0.3.0}/phoenix_channels_python_client/reconnect_controller.py +58 -27
- phoenix_channels_python_client-0.3.0/phoenix_channels_python_client/shutdown_signals.py +120 -0
- phoenix_channels_python_client-0.3.0/phoenix_channels_python_client/supervisor.py +476 -0
- phoenix_channels_python_client-0.3.0/phoenix_channels_python_client/topic_runtime.py +783 -0
- {phoenix_channels_python_client-0.2.4 → phoenix_channels_python_client-0.3.0}/phoenix_channels_python_client/topic_subscription.py +21 -6
- {phoenix_channels_python_client-0.2.4 → phoenix_channels_python_client-0.3.0}/phoenix_channels_python_client/utils.py +33 -15
- phoenix_channels_python_client-0.3.0/phoenix_channels_python_client.egg-info/PKG-INFO +383 -0
- {phoenix_channels_python_client-0.2.4 → phoenix_channels_python_client-0.3.0}/phoenix_channels_python_client.egg-info/SOURCES.txt +14 -2
- {phoenix_channels_python_client-0.2.4 → phoenix_channels_python_client-0.3.0}/phoenix_channels_python_client.egg-info/requires.txt +4 -2
- phoenix_channels_python_client-0.3.0/pyproject.toml +87 -0
- phoenix_channels_python_client-0.3.0/tests/test_cancel_and_wait.py +53 -0
- phoenix_channels_python_client-0.3.0/tests/test_channel_recovery.py +350 -0
- phoenix_channels_python_client-0.3.0/tests/test_client_state.py +77 -0
- phoenix_channels_python_client-0.3.0/tests/test_connection.py +250 -0
- phoenix_channels_python_client-0.3.0/tests/test_heartbeat.py +77 -0
- phoenix_channels_python_client-0.3.0/tests/test_protocol_handler.py +228 -0
- phoenix_channels_python_client-0.3.0/tests/test_reconnect.py +627 -0
- phoenix_channels_python_client-0.3.0/tests/test_reconnect_policy_invariants.py +177 -0
- phoenix_channels_python_client-0.3.0/tests/test_reconnect_stress.py +185 -0
- phoenix_channels_python_client-0.3.0/tests/test_run_forever_signals.py +308 -0
- phoenix_channels_python_client-0.3.0/tests/test_shutdown_races.py +321 -0
- phoenix_channels_python_client-0.3.0/tests/test_subscriptions.py +445 -0
- phoenix_channels_python_client-0.3.0/tests/test_supervisor_internals.py +178 -0
- phoenix_channels_python_client-0.3.0/tests/test_topic_runtime_internals.py +303 -0
- phoenix_channels_python_client-0.2.4/PKG-INFO +0 -269
- phoenix_channels_python_client-0.2.4/README.md +0 -240
- phoenix_channels_python_client-0.2.4/phoenix_channels_python_client/client_types.py +0 -102
- phoenix_channels_python_client-0.2.4/phoenix_channels_python_client/supervisor.py +0 -448
- phoenix_channels_python_client-0.2.4/phoenix_channels_python_client/topic_runtime.py +0 -581
- phoenix_channels_python_client-0.2.4/phoenix_channels_python_client.egg-info/PKG-INFO +0 -269
- phoenix_channels_python_client-0.2.4/pyproject.toml +0 -52
- phoenix_channels_python_client-0.2.4/tests/test_internal_components.py +0 -1138
- phoenix_channels_python_client-0.2.4/tests/test_reconnect_policy_invariants.py +0 -124
- phoenix_channels_python_client-0.2.4/tests/test_reconnect_stress.py +0 -136
- {phoenix_channels_python_client-0.2.4 → phoenix_channels_python_client-0.3.0}/LICENSE +0 -0
- {phoenix_channels_python_client-0.2.4 → phoenix_channels_python_client-0.3.0}/MANIFEST.in +0 -0
- {phoenix_channels_python_client-0.2.4 → phoenix_channels_python_client-0.3.0}/phoenix_channels_python_client.egg-info/dependency_links.txt +0 -0
- {phoenix_channels_python_client-0.2.4 → phoenix_channels_python_client-0.3.0}/phoenix_channels_python_client.egg-info/top_level.txt +0 -0
- {phoenix_channels_python_client-0.2.4 → phoenix_channels_python_client-0.3.0}/setup.cfg +0 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: phoenix-channels-python-client
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: A Python client library for connecting to Phoenix Channels
|
|
5
|
+
Author: Phoenix Channels Python Client
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: pydantic>=2.8
|
|
18
|
+
Requires-Dist: websockets>=14.2
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest-asyncio>=1.4; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
23
|
+
Requires-Dist: pre-commit>=3.0.0; extra == "dev"
|
|
24
|
+
Requires-Dist: commitizen>=3.13.0; extra == "dev"
|
|
25
|
+
Requires-Dist: pyrefly; extra == "dev"
|
|
26
|
+
Requires-Dist: ruff; extra == "dev"
|
|
27
|
+
Provides-Extra: test
|
|
28
|
+
Requires-Dist: pytest; extra == "test"
|
|
29
|
+
Requires-Dist: pytest-asyncio>=1.4; extra == "test"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# Phoenix Channels Python Client
|
|
33
|
+
|
|
34
|
+
[](https://pypi.org/project/phoenix-channels-python-client/)
|
|
35
|
+
[](https://www.python.org/downloads/)
|
|
36
|
+
[](https://opensource.org/licenses/MIT)
|
|
37
|
+
|
|
38
|
+
A modern, async Python client library for connecting to [Phoenix Channels](https://hexdocs.pm/phoenix/channels.html) - the real-time WebSocket layer of the Phoenix Framework.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
### For Users
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install phoenix-channels-python-client
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or from source:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
git clone https://github.com/band-ai/phoenix-channels-python-client.git
|
|
52
|
+
cd phoenix-channels-python-client
|
|
53
|
+
python3 -m venv venv
|
|
54
|
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
55
|
+
pip install .
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### For Development
|
|
59
|
+
|
|
60
|
+
Set up development environment:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
git clone https://github.com/band-ai/phoenix-channels-python-client.git
|
|
64
|
+
cd phoenix-channels-python-client
|
|
65
|
+
uv sync --extra dev
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Run tests:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
uv run pytest
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Dependencies
|
|
75
|
+
|
|
76
|
+
- Python 3.11+
|
|
77
|
+
- `pydantic>=2.8`
|
|
78
|
+
- `websockets>=14.2`
|
|
79
|
+
|
|
80
|
+
## Quick Start
|
|
81
|
+
|
|
82
|
+
Here's a minimal example to get you started:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
import asyncio
|
|
86
|
+
from phoenix_channels_python_client import PHXChannelsClient
|
|
87
|
+
|
|
88
|
+
async def main():
|
|
89
|
+
# Connect to Phoenix WebSocket server
|
|
90
|
+
async with PHXChannelsClient(
|
|
91
|
+
websocket_url="ws://localhost:4000/socket/websocket",
|
|
92
|
+
api_key="your-api-key"
|
|
93
|
+
) as client:
|
|
94
|
+
|
|
95
|
+
# Define message handler
|
|
96
|
+
async def handle_message(message):
|
|
97
|
+
print(f"Received: {message}")
|
|
98
|
+
|
|
99
|
+
# Subscribe to a topic
|
|
100
|
+
await client.subscribe_to_topic("room:lobby", handle_message)
|
|
101
|
+
|
|
102
|
+
# Ctrl+C or SIGTERM shuts the client down and run_forever() returns.
|
|
103
|
+
# The previous handlers are restored first, so a second signal reaches them.
|
|
104
|
+
await client.run_forever()
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
asyncio.run(main())
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Owning process signals yourself
|
|
111
|
+
|
|
112
|
+
If your application owns its process signals (a service, desktop app or test
|
|
113
|
+
runner), call `client.run_forever(install_signal_handlers=False)`. It then
|
|
114
|
+
leaves signal handlers alone; stop the client by scheduling
|
|
115
|
+
`client.shutdown(reason)` on the client's event loop, and `run_forever()`
|
|
116
|
+
returns `None`:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
loop = asyncio.get_running_loop()
|
|
120
|
+
async with asyncio.TaskGroup() as tasks:
|
|
121
|
+
loop.add_signal_handler(
|
|
122
|
+
signal.SIGTERM, lambda: tasks.create_task(client.shutdown("SIGTERM"))
|
|
123
|
+
)
|
|
124
|
+
try:
|
|
125
|
+
await client.run_forever(install_signal_handlers=False)
|
|
126
|
+
finally:
|
|
127
|
+
# Once the group exits, a later SIGTERM must not reach the closed group.
|
|
128
|
+
loop.remove_signal_handler(signal.SIGTERM)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
From a plain `signal.signal` handler or another thread, use
|
|
132
|
+
`loop.call_soon_threadsafe(...)` instead: a bare `asyncio.create_task` there
|
|
133
|
+
doesn't wake a loop blocked on I/O, so the shutdown can stall until the next
|
|
134
|
+
network activity. See [`examples/embedded_host_signals.py`](examples/embedded_host_signals.py).
|
|
135
|
+
|
|
136
|
+
With the default, handlers are installed only when `run_forever()` runs on the
|
|
137
|
+
main thread; elsewhere it just waits. A handler you registered with
|
|
138
|
+
`loop.add_signal_handler` before calling `run_forever()` still fires for the
|
|
139
|
+
same signal; one registered while it runs replaces the client's.
|
|
140
|
+
|
|
141
|
+
## Phoenix System Events
|
|
142
|
+
|
|
143
|
+
Phoenix reserves `phx_join`, `phx_reply`, `phx_leave`, `phx_close` and
|
|
144
|
+
`phx_error`. Don't use these names for your own events.
|
|
145
|
+
|
|
146
|
+
- The client sends `phx_join` and `phx_leave` and consumes their `phx_reply`.
|
|
147
|
+
- The client handles `phx_error` and `phx_close` itself, and neither reaches
|
|
148
|
+
your handlers: a crashed channel is rejoined, and a channel the server
|
|
149
|
+
closes loses its topic (see [Channel recovery](#channel-recovery)).
|
|
150
|
+
- `add_event_handler` raises `ValueError` for `PHXEvent.error` and
|
|
151
|
+
`PHXEvent.close`. Use `on_topic_lost` and `is_topic_joined()` instead.
|
|
152
|
+
|
|
153
|
+
## Protocol Versions
|
|
154
|
+
|
|
155
|
+
Phoenix Channels supports two protocol versions. Choose based on your Phoenix server version:
|
|
156
|
+
|
|
157
|
+
### Protocol v2.0 (Default)
|
|
158
|
+
```python
|
|
159
|
+
from phoenix_channels_python_client import PHXChannelsClient
|
|
160
|
+
|
|
161
|
+
async with PHXChannelsClient(
|
|
162
|
+
websocket_url="ws://localhost:4000/socket/websocket",
|
|
163
|
+
api_key="your-api-key"
|
|
164
|
+
# protocol_version defaults to v2.0
|
|
165
|
+
) as client:
|
|
166
|
+
... # your code here
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Protocol v1.0
|
|
170
|
+
```python
|
|
171
|
+
from phoenix_channels_python_client import PHXChannelsClient, PhoenixChannelsProtocolVersion
|
|
172
|
+
|
|
173
|
+
async with PHXChannelsClient(
|
|
174
|
+
websocket_url="ws://localhost:4000/socket/websocket",
|
|
175
|
+
api_key="your-api-key",
|
|
176
|
+
protocol_version=PhoenixChannelsProtocolVersion.V1
|
|
177
|
+
) as client:
|
|
178
|
+
... # your code here
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The client adds `vsn=2.0.0` (V2) or `vsn=1.0.0` (V1) to the socket URL.
|
|
182
|
+
|
|
183
|
+
## Usage Examples
|
|
184
|
+
|
|
185
|
+
### Basic Topic Subscription
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
import asyncio
|
|
189
|
+
from phoenix_channels_python_client import PHXChannelsClient
|
|
190
|
+
|
|
191
|
+
async def message_handler(message):
|
|
192
|
+
print(f"Topic: {message.topic}")
|
|
193
|
+
print(f"Event: {message.event}")
|
|
194
|
+
print(f"Payload: {message.payload}")
|
|
195
|
+
|
|
196
|
+
async def main():
|
|
197
|
+
async with PHXChannelsClient("ws://localhost:4000/socket/websocket", "api-key") as client:
|
|
198
|
+
await client.subscribe_to_topic("chat:general", message_handler)
|
|
199
|
+
|
|
200
|
+
# Use built-in method to keep connection alive
|
|
201
|
+
await client.run_forever()
|
|
202
|
+
|
|
203
|
+
asyncio.run(main())
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Message Handlers vs Event-Specific Handlers
|
|
207
|
+
|
|
208
|
+
The library provides two complementary ways to handle incoming messages:
|
|
209
|
+
|
|
210
|
+
**Message Handler** - Receives ALL messages for a topic:
|
|
211
|
+
- Set when subscribing to a topic or separately with `set_message_handler()`
|
|
212
|
+
- Gets the complete message object with topic, event, payload, etc.
|
|
213
|
+
- Good for logging, debugging, or handling any message type
|
|
214
|
+
|
|
215
|
+
**Event-Specific Handlers** - Receive only messages with specific event names:
|
|
216
|
+
- Added with `add_event_handler()` for particular events like "user_join", "chat_message", etc.
|
|
217
|
+
- Only get the payload (data) part of the message
|
|
218
|
+
- Perfect for handling specific application events
|
|
219
|
+
|
|
220
|
+
**Execution order** when you have both types of handlers:
|
|
221
|
+
1. **Message handler runs first** (if set) - receives the full message
|
|
222
|
+
2. **Event-specific handler runs second** (if matching event) - receives just the payload
|
|
223
|
+
|
|
224
|
+
All handlers must be `async def`. You can add, remove, or change either type of handler at any time during your connection.
|
|
225
|
+
|
|
226
|
+
### Event-Specific Handlers
|
|
227
|
+
|
|
228
|
+
You can register handlers for specific events on a topic:
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
import asyncio
|
|
232
|
+
from phoenix_channels_python_client import PHXChannelsClient
|
|
233
|
+
|
|
234
|
+
async def handle_user_join(payload):
|
|
235
|
+
print(f"User joined: {payload}")
|
|
236
|
+
|
|
237
|
+
async def handle_user_leave(payload):
|
|
238
|
+
print(f"User left: {payload}")
|
|
239
|
+
|
|
240
|
+
async def main():
|
|
241
|
+
async with PHXChannelsClient("ws://localhost:4000/socket/websocket", "api-key") as client:
|
|
242
|
+
# Subscribe to topic first
|
|
243
|
+
await client.subscribe_to_topic("room:lobby")
|
|
244
|
+
|
|
245
|
+
# Add event-specific handlers for custom events
|
|
246
|
+
client.add_event_handler("room:lobby", "user_join", handle_user_join)
|
|
247
|
+
client.add_event_handler("room:lobby", "user_leave", handle_user_leave)
|
|
248
|
+
|
|
249
|
+
# Use built-in method to keep connection alive
|
|
250
|
+
await client.run_forever()
|
|
251
|
+
|
|
252
|
+
asyncio.run(main())
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Unsubscribing from Topics
|
|
256
|
+
|
|
257
|
+
You can unsubscribe from topics to stop receiving messages and clean up resources:
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
import asyncio
|
|
261
|
+
from phoenix_channels_python_client import PHXChannelsClient
|
|
262
|
+
|
|
263
|
+
async def on_message(msg):
|
|
264
|
+
print(f"Message: {msg.payload}")
|
|
265
|
+
|
|
266
|
+
async def main():
|
|
267
|
+
async with PHXChannelsClient("ws://localhost:4000/socket/websocket", "api-key") as client:
|
|
268
|
+
# Subscribe to a topic
|
|
269
|
+
await client.subscribe_to_topic("room:lobby", on_message)
|
|
270
|
+
|
|
271
|
+
# Do some work...
|
|
272
|
+
await asyncio.sleep(5)
|
|
273
|
+
|
|
274
|
+
# Unsubscribe when no longer needed
|
|
275
|
+
await client.unsubscribe_from_topic("room:lobby")
|
|
276
|
+
print("Unsubscribed from room:lobby")
|
|
277
|
+
|
|
278
|
+
# Continue with other work or subscribe to different topics
|
|
279
|
+
await client.run_forever()
|
|
280
|
+
|
|
281
|
+
asyncio.run(main())
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Notes on unsubscription:**
|
|
285
|
+
- Removes all handlers (both message and event-specific) for that topic
|
|
286
|
+
- Sends `phx_leave` and waits for the reply (`leave_timeout_s`, default 5 s)
|
|
287
|
+
- Raises `PHXTopicError` if the topic isn't subscribed or the leave times out, and `PHXConnectionError` while disconnected
|
|
288
|
+
|
|
289
|
+
## Configuration
|
|
290
|
+
|
|
291
|
+
Everything after `api_key` is keyword-only:
|
|
292
|
+
|
|
293
|
+
| Option | Default | Meaning |
|
|
294
|
+
|---|---|---|
|
|
295
|
+
| `protocol_version` | `V2` | Phoenix Channels protocol version |
|
|
296
|
+
| `auto_reconnect` | `True` | Reconnect and rejoin topics after a disconnect |
|
|
297
|
+
| `reconnect_policy` | `ReconnectPolicy()` | Backoff and close-code handling (see below) |
|
|
298
|
+
| `heartbeat_interval_s` | `30.0` | Heartbeat period; `None` disables heartbeats |
|
|
299
|
+
| `join_timeout_s` | `10.0` | Wait for a join reply |
|
|
300
|
+
| `leave_timeout_s` | `5.0` | Wait for a leave reply |
|
|
301
|
+
| `max_topic_queue_size` | `1000` | Per-topic buffer; the oldest message is dropped when full |
|
|
302
|
+
| `callback_drain_timeout_s` | `2.0` | How long the message being handled (its message handler, then its event handler) may finish before a rejoin cancels it; queued messages are dropped |
|
|
303
|
+
| `on_reconnect` | `None` | `async () -> None`, called after topics are rejoined |
|
|
304
|
+
| `on_disconnect` | `None` | `async (error: Exception \| None) -> None`; `None` on a clean close |
|
|
305
|
+
| `on_heartbeat_ack` | `None` | Synchronous `() -> None`; runs on the message path, so keep it fast |
|
|
306
|
+
| `on_topic_lost` | `None` | `async (topic: str, error: Exception) -> None`, called when the client drops a topic you didn't unsubscribe |
|
|
307
|
+
| `additional_headers` | `None` | Extra handshake headers sent on every (re)connect |
|
|
308
|
+
|
|
309
|
+
Exceptions raised in callbacks are logged, not raised.
|
|
310
|
+
|
|
311
|
+
## Connection Lifecycle and Errors
|
|
312
|
+
|
|
313
|
+
- `async with` waits for the first connection. With `auto_reconnect=True` it keeps retrying; with `auto_reconnect=False` a failed first connection raises `PHXConnectionError`.
|
|
314
|
+
- `run_forever()` returns `None` after `shutdown()`, a signal, or a close that doesn't reconnect. It raises `PHXConnectionError` on a terminal close, after repeated rapid disconnects, or when the client has never entered `async with`.
|
|
315
|
+
- `subscribe_to_topic()` raises `PHXTopicError` on a rejected join, a join timeout or a duplicate subscription, and `PHXConnectionError` while disconnected or if the connection drops before the join completes.
|
|
316
|
+
- `await client.close_connection(reason)` force-closes the current connection; the client then reconnects if `auto_reconnect` is on. It does nothing if the client isn't connected or the connection is already closing.
|
|
317
|
+
- Exceptions live in `phoenix_channels_python_client.exceptions`.
|
|
318
|
+
|
|
319
|
+
### Reconnects
|
|
320
|
+
|
|
321
|
+
| Close code | Behaviour (defaults) |
|
|
322
|
+
|---|---|
|
|
323
|
+
| 1000, 1001 | Stop, unless `reconnect_on_normal_close=True` |
|
|
324
|
+
| 1008 | Stop with `PHXConnectionError` (`policy_violation_is_terminal=True`) |
|
|
325
|
+
| 1012 | Reconnect after at least a random 1–5 s |
|
|
326
|
+
| 1013 | Reconnect after at least a random 30–60 s |
|
|
327
|
+
| Anything else, or no close frame | Reconnect with jittered backoff: 0.5 s × 2ⁿ, capped at 30 s, reset after 60 s of uptime |
|
|
328
|
+
|
|
329
|
+
Disconnects within 5 s of connecting count as rapid and get longer minimum delays; ten within 60 s stop the client with `PHXConnectionError`. Tune all of this with `ReconnectPolicy`.
|
|
330
|
+
|
|
331
|
+
A `ReconnectPolicy` is validated when it is built: an out-of-range or unknown field raises `pydantic.ValidationError`, which is a `ValueError`. To derive one policy from another, validate the merged fields:
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
base = ReconnectPolicy()
|
|
335
|
+
policy = ReconnectPolicy.model_validate(base.model_dump() | {"max_delay_s": 10.0})
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Channel recovery
|
|
339
|
+
|
|
340
|
+
A channel can fail while the socket stays up. When its process crashes, the server sends `phx_error`, and the client rejoins that topic on the same socket, waiting the `ReconnectPolicy` backoff (`base_delay_s`, `factor`, `max_delay_s`, with equal jitter) before each attempt. A rejoin that times out is retried the same way, including one after a socket reconnect. A drop of the socket hands the topic to the socket's own rejoin.
|
|
341
|
+
|
|
342
|
+
The client drops the topic and calls `on_topic_lost(topic, error)` once when the server rejects a rejoin, after a crash or a reconnect, closes the channel with `phx_close`, or its message processor fails. It never calls it for `unsubscribe_from_topic()`, `shutdown()` or a failed first `subscribe_to_topic()`, which raises instead. Subscribing to the topic again from the callback works. `shutdown()` cancels the callback, including when the callback itself called `shutdown()`, so code after that call does not run.
|
|
343
|
+
|
|
344
|
+
`get_current_subscriptions()` lists the subscribed topics. `is_topic_joined(topic)` tells whether a topic's channel is joined right now: it's false while the socket is down or a crashed channel waits to rejoin. Unsubscribing a topic whose channel is being recovered completes without waiting for a leave reply.
|
|
345
|
+
|
|
346
|
+
### Heartbeats
|
|
347
|
+
|
|
348
|
+
The client sends a `heartbeat` on the `phoenix` topic every `heartbeat_interval_s`. A missed reply only logs a warning; to act on it, combine `on_heartbeat_ack` with `close_connection()`.
|
|
349
|
+
|
|
350
|
+
## Logging
|
|
351
|
+
|
|
352
|
+
```python
|
|
353
|
+
import logging
|
|
354
|
+
from phoenix_channels_python_client import setup_logging
|
|
355
|
+
|
|
356
|
+
setup_logging(logging.DEBUG) # loggers live under "phoenix_channels_python_client"
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
## Examples
|
|
360
|
+
|
|
361
|
+
- [`examples/minimal_phx_events_client.py`](examples/minimal_phx_events_client.py): subscribe to a topic and log messages until Ctrl+C or SIGTERM.
|
|
362
|
+
- [`examples/embedded_host_signals.py`](examples/embedded_host_signals.py): run the client in a host that owns its process signals.
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## Security Considerations
|
|
367
|
+
|
|
368
|
+
**API Key in URL**: This client passes the API key as a URL query parameter when connecting to the WebSocket (e.g., `wss://server.com/socket?api_key=xxx`). Even over WSS (encrypted in transit), the API key may be logged by:
|
|
369
|
+
|
|
370
|
+
- Server access logs
|
|
371
|
+
- Reverse proxies and load balancers
|
|
372
|
+
- Network monitoring tools
|
|
373
|
+
|
|
374
|
+
**Recommendations:**
|
|
375
|
+
- Ensure your infrastructure does not log full URLs in production
|
|
376
|
+
- Use short-lived, rotatable tokens rather than long-lived API keys
|
|
377
|
+
- Consider this limitation when evaluating this client for sensitive environments
|
|
378
|
+
|
|
379
|
+
**Note:** The official Phoenix JS client (v1.8+) supports header-based authentication via the `authToken` option, which avoids URL logging. This client always sends `api_key` in the URL (logged URLs show `api_key=***`). Pass `additional_headers={"x-api-key": key}` to also send it as a handshake header on every (re)connect, e.g. for a proxy that reads it from there.
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
**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.
|