beatbot-cloud 0.4.0__tar.gz → 0.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {beatbot_cloud-0.4.0/src/beatbot_cloud.egg-info → beatbot_cloud-0.4.2}/PKG-INFO +9 -3
- {beatbot_cloud-0.4.0 → beatbot_cloud-0.4.2}/README.md +8 -2
- {beatbot_cloud-0.4.0 → beatbot_cloud-0.4.2}/pyproject.toml +1 -1
- {beatbot_cloud-0.4.0 → beatbot_cloud-0.4.2}/src/beatbot_cloud/client.py +1 -1
- {beatbot_cloud-0.4.0 → beatbot_cloud-0.4.2}/src/beatbot_cloud/event_client.py +33 -4
- {beatbot_cloud-0.4.0 → beatbot_cloud-0.4.2/src/beatbot_cloud.egg-info}/PKG-INFO +9 -3
- {beatbot_cloud-0.4.0 → beatbot_cloud-0.4.2}/tests/test_client.py +6 -0
- {beatbot_cloud-0.4.0 → beatbot_cloud-0.4.2}/tests/test_event_client.py +70 -0
- {beatbot_cloud-0.4.0 → beatbot_cloud-0.4.2}/LICENSE +0 -0
- {beatbot_cloud-0.4.0 → beatbot_cloud-0.4.2}/setup.cfg +0 -0
- {beatbot_cloud-0.4.0 → beatbot_cloud-0.4.2}/src/beatbot_cloud/__init__.py +0 -0
- {beatbot_cloud-0.4.0 → beatbot_cloud-0.4.2}/src/beatbot_cloud/auth.py +0 -0
- {beatbot_cloud-0.4.0 → beatbot_cloud-0.4.2}/src/beatbot_cloud/const.py +0 -0
- {beatbot_cloud-0.4.0 → beatbot_cloud-0.4.2}/src/beatbot_cloud/exceptions.py +0 -0
- {beatbot_cloud-0.4.0 → beatbot_cloud-0.4.2}/src/beatbot_cloud/models.py +0 -0
- {beatbot_cloud-0.4.0 → beatbot_cloud-0.4.2}/src/beatbot_cloud/protocol.py +0 -0
- {beatbot_cloud-0.4.0 → beatbot_cloud-0.4.2}/src/beatbot_cloud/py.typed +0 -0
- {beatbot_cloud-0.4.0 → beatbot_cloud-0.4.2}/src/beatbot_cloud/websocket.py +0 -0
- {beatbot_cloud-0.4.0 → beatbot_cloud-0.4.2}/src/beatbot_cloud.egg-info/SOURCES.txt +0 -0
- {beatbot_cloud-0.4.0 → beatbot_cloud-0.4.2}/src/beatbot_cloud.egg-info/dependency_links.txt +0 -0
- {beatbot_cloud-0.4.0 → beatbot_cloud-0.4.2}/src/beatbot_cloud.egg-info/requires.txt +0 -0
- {beatbot_cloud-0.4.0 → beatbot_cloud-0.4.2}/src/beatbot_cloud.egg-info/top_level.txt +0 -0
- {beatbot_cloud-0.4.0 → beatbot_cloud-0.4.2}/tests/test_auth.py +0 -0
- {beatbot_cloud-0.4.0 → beatbot_cloud-0.4.2}/tests/test_protocol.py +0 -0
- {beatbot_cloud-0.4.0 → beatbot_cloud-0.4.2}/tests/test_websocket.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: beatbot-cloud
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Asynchronous Python client for the Beatbot cloud API
|
|
5
5
|
Author: Beatbot Robotics
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -46,7 +46,9 @@ events = BeatbotEventClient(
|
|
|
46
46
|
session,
|
|
47
47
|
client.event_stream_url,
|
|
48
48
|
client.async_get_access_token,
|
|
49
|
-
|
|
49
|
+
state_callback=async_handle_state,
|
|
50
|
+
device_added_callback=async_handle_device_added,
|
|
51
|
+
device_removed_callback=async_handle_device_removed,
|
|
50
52
|
reconnect_callback=async_handle_reconnect,
|
|
51
53
|
token_refresh_callback=async_refresh_access_token,
|
|
52
54
|
)
|
|
@@ -56,7 +58,11 @@ await events.async_run()
|
|
|
56
58
|
The caller provides an access token or a synchronous/asynchronous token provider.
|
|
57
59
|
The library owns REST request construction and WebSocket reconnection; applications
|
|
58
60
|
can register callbacks for events, successful reconnections, and rejected-token
|
|
59
|
-
refreshes.
|
|
61
|
+
refreshes. State callbacks receive a `BeatbotEvent`, which can be applied with
|
|
62
|
+
`event.apply_to(device)`. Device-added and device-removed callbacks receive the
|
|
63
|
+
device ID. The library routes cloud event types and ignores unknown types for
|
|
64
|
+
these specialized callbacks. All callbacks may be synchronous or asynchronous.
|
|
65
|
+
The optional positional `event_callback` remains supported and receives all events.
|
|
60
66
|
|
|
61
67
|
## Development
|
|
62
68
|
|
|
@@ -18,7 +18,9 @@ events = BeatbotEventClient(
|
|
|
18
18
|
session,
|
|
19
19
|
client.event_stream_url,
|
|
20
20
|
client.async_get_access_token,
|
|
21
|
-
|
|
21
|
+
state_callback=async_handle_state,
|
|
22
|
+
device_added_callback=async_handle_device_added,
|
|
23
|
+
device_removed_callback=async_handle_device_removed,
|
|
22
24
|
reconnect_callback=async_handle_reconnect,
|
|
23
25
|
token_refresh_callback=async_refresh_access_token,
|
|
24
26
|
)
|
|
@@ -28,7 +30,11 @@ await events.async_run()
|
|
|
28
30
|
The caller provides an access token or a synchronous/asynchronous token provider.
|
|
29
31
|
The library owns REST request construction and WebSocket reconnection; applications
|
|
30
32
|
can register callbacks for events, successful reconnections, and rejected-token
|
|
31
|
-
refreshes.
|
|
33
|
+
refreshes. State callbacks receive a `BeatbotEvent`, which can be applied with
|
|
34
|
+
`event.apply_to(device)`. Device-added and device-removed callbacks receive the
|
|
35
|
+
device ID. The library routes cloud event types and ignores unknown types for
|
|
36
|
+
these specialized callbacks. All callbacks may be synchronous or asynchronous.
|
|
37
|
+
The optional positional `event_callback` remains supported and receives all events.
|
|
32
38
|
|
|
33
39
|
## Development
|
|
34
40
|
|
|
@@ -29,6 +29,7 @@ _RECONNECT_DELAYS = (1.0, 2.0, 4.0, 8.0, 30.0, 60.0)
|
|
|
29
29
|
_RECONNECT_JITTER = 0.2
|
|
30
30
|
|
|
31
31
|
EventCallback = Callable[[BeatbotEvent], None | Awaitable[None]]
|
|
32
|
+
DeviceCallback = Callable[[str], None | Awaitable[None]]
|
|
32
33
|
ReconnectCallback = Callable[[], None | Awaitable[None]]
|
|
33
34
|
TokenRefreshCallback = Callable[[str], str | Awaitable[str]]
|
|
34
35
|
|
|
@@ -41,8 +42,11 @@ class BeatbotEventClient:
|
|
|
41
42
|
session: ClientSession,
|
|
42
43
|
url: str,
|
|
43
44
|
access_token: AccessTokenProvider,
|
|
44
|
-
event_callback: EventCallback,
|
|
45
|
+
event_callback: EventCallback | None = None,
|
|
45
46
|
*,
|
|
47
|
+
state_callback: EventCallback | None = None,
|
|
48
|
+
device_added_callback: DeviceCallback | None = None,
|
|
49
|
+
device_removed_callback: DeviceCallback | None = None,
|
|
46
50
|
reconnect_callback: ReconnectCallback | None = None,
|
|
47
51
|
token_refresh_callback: TokenRefreshCallback | None = None,
|
|
48
52
|
) -> None:
|
|
@@ -51,6 +55,9 @@ class BeatbotEventClient:
|
|
|
51
55
|
self._url = url
|
|
52
56
|
self._access_token = access_token
|
|
53
57
|
self._event_callback = event_callback
|
|
58
|
+
self._state_callback = state_callback
|
|
59
|
+
self._device_added_callback = device_added_callback
|
|
60
|
+
self._device_removed_callback = device_removed_callback
|
|
54
61
|
self._reconnect_callback = reconnect_callback
|
|
55
62
|
self._token_refresh_callback = token_refresh_callback
|
|
56
63
|
self._stream: BeatbotEventStream | None = None
|
|
@@ -160,14 +167,36 @@ class BeatbotEventClient:
|
|
|
160
167
|
if event.event_id in self._seen_event_ids:
|
|
161
168
|
continue
|
|
162
169
|
self._remember_event(event.event_id)
|
|
163
|
-
|
|
164
|
-
if isawaitable(result):
|
|
165
|
-
await result
|
|
170
|
+
await self._async_dispatch_event(event)
|
|
166
171
|
finally:
|
|
167
172
|
await stream.close()
|
|
168
173
|
if self._stream is stream:
|
|
169
174
|
self._stream = None
|
|
170
175
|
|
|
176
|
+
async def _async_dispatch_event(self, event: BeatbotEvent) -> None:
|
|
177
|
+
"""Route cloud event types to consumer callbacks."""
|
|
178
|
+
if self._event_callback is not None:
|
|
179
|
+
result = self._event_callback(event)
|
|
180
|
+
if isawaitable(result):
|
|
181
|
+
await result
|
|
182
|
+
if event.event_type in ("properties_changed", "status"):
|
|
183
|
+
if self._state_callback is not None:
|
|
184
|
+
result = self._state_callback(event)
|
|
185
|
+
if isawaitable(result):
|
|
186
|
+
await result
|
|
187
|
+
elif event.event_type == "device_added":
|
|
188
|
+
if self._device_added_callback is not None:
|
|
189
|
+
result = self._device_added_callback(event.device_id)
|
|
190
|
+
if isawaitable(result):
|
|
191
|
+
await result
|
|
192
|
+
elif (
|
|
193
|
+
event.event_type == "device_removed"
|
|
194
|
+
and self._device_removed_callback is not None
|
|
195
|
+
):
|
|
196
|
+
result = self._device_removed_callback(event.device_id)
|
|
197
|
+
if isawaitable(result):
|
|
198
|
+
await result
|
|
199
|
+
|
|
171
200
|
async def async_close(self) -> None:
|
|
172
201
|
"""Stop receiving and close the current connection."""
|
|
173
202
|
self._stopping = True
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: beatbot-cloud
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Asynchronous Python client for the Beatbot cloud API
|
|
5
5
|
Author: Beatbot Robotics
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -46,7 +46,9 @@ events = BeatbotEventClient(
|
|
|
46
46
|
session,
|
|
47
47
|
client.event_stream_url,
|
|
48
48
|
client.async_get_access_token,
|
|
49
|
-
|
|
49
|
+
state_callback=async_handle_state,
|
|
50
|
+
device_added_callback=async_handle_device_added,
|
|
51
|
+
device_removed_callback=async_handle_device_removed,
|
|
50
52
|
reconnect_callback=async_handle_reconnect,
|
|
51
53
|
token_refresh_callback=async_refresh_access_token,
|
|
52
54
|
)
|
|
@@ -56,7 +58,11 @@ await events.async_run()
|
|
|
56
58
|
The caller provides an access token or a synchronous/asynchronous token provider.
|
|
57
59
|
The library owns REST request construction and WebSocket reconnection; applications
|
|
58
60
|
can register callbacks for events, successful reconnections, and rejected-token
|
|
59
|
-
refreshes.
|
|
61
|
+
refreshes. State callbacks receive a `BeatbotEvent`, which can be applied with
|
|
62
|
+
`event.apply_to(device)`. Device-added and device-removed callbacks receive the
|
|
63
|
+
device ID. The library routes cloud event types and ignores unknown types for
|
|
64
|
+
these specialized callbacks. All callbacks may be synchronous or asynchronous.
|
|
65
|
+
The optional positional `event_callback` remains supported and receives all events.
|
|
60
66
|
|
|
61
67
|
## Development
|
|
62
68
|
|
|
@@ -116,6 +116,12 @@ async def test_client_error_is_connection_error():
|
|
|
116
116
|
await api._request("GET", "/test")
|
|
117
117
|
|
|
118
118
|
|
|
119
|
+
async def test_timeout_is_connection_error():
|
|
120
|
+
api, _ = client(TimeoutError("timed out"))
|
|
121
|
+
with pytest.raises(BeatbotConnectionError, match="timed out"):
|
|
122
|
+
await api._request("GET", "/test")
|
|
123
|
+
|
|
124
|
+
|
|
119
125
|
async def test_async_access_token_provider():
|
|
120
126
|
async def access_token():
|
|
121
127
|
return "rotated-token"
|
|
@@ -49,6 +49,76 @@ async def test_connect_delivers_unique_events_and_ignores_malformed():
|
|
|
49
49
|
stream.close.assert_awaited_once()
|
|
50
50
|
|
|
51
51
|
|
|
52
|
+
@pytest.mark.parametrize("callback_type", [Mock, AsyncMock])
|
|
53
|
+
@pytest.mark.parametrize(
|
|
54
|
+
("event_type", "callback_name", "payload"),
|
|
55
|
+
[
|
|
56
|
+
(
|
|
57
|
+
"properties_changed",
|
|
58
|
+
"state_callback",
|
|
59
|
+
{"interfaceInfo": "vacuum.battery", "value": 42},
|
|
60
|
+
),
|
|
61
|
+
("status", "state_callback", {"online": False}),
|
|
62
|
+
("device_added", "device_added_callback", {"deviceId": "device-1"}),
|
|
63
|
+
("device_removed", "device_removed_callback", None),
|
|
64
|
+
],
|
|
65
|
+
)
|
|
66
|
+
async def test_dispatches_specialized_callbacks(
|
|
67
|
+
callback_type, event_type, callback_name, payload
|
|
68
|
+
):
|
|
69
|
+
callback = callback_type()
|
|
70
|
+
event = BeatbotEvent("event-1", event_type, "device-1", payload)
|
|
71
|
+
event_client = client(event_callback=None, **{callback_name: callback})
|
|
72
|
+
stream = SimpleNamespace(
|
|
73
|
+
connect=AsyncMock(),
|
|
74
|
+
receive=AsyncMock(side_effect=[event, event, asyncio.CancelledError]),
|
|
75
|
+
close=AsyncMock(),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
with (
|
|
79
|
+
patch("beatbot_cloud.event_client.BeatbotEventStream", return_value=stream),
|
|
80
|
+
pytest.raises(asyncio.CancelledError),
|
|
81
|
+
):
|
|
82
|
+
await event_client._async_connect_and_receive()
|
|
83
|
+
|
|
84
|
+
argument = event if callback_name == "state_callback" else "device-1"
|
|
85
|
+
callback.assert_called_once_with(argument)
|
|
86
|
+
if callback_type is AsyncMock:
|
|
87
|
+
callback.assert_awaited_once_with(argument)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@pytest.mark.parametrize(
|
|
91
|
+
"event_type",
|
|
92
|
+
["status", "properties_changed", "device_added", "device_removed", "future_type"],
|
|
93
|
+
)
|
|
94
|
+
async def test_dispatch_without_callbacks(event_type):
|
|
95
|
+
event_client = client(event_callback=None)
|
|
96
|
+
await event_client._async_dispatch_event(
|
|
97
|
+
BeatbotEvent("event-1", event_type, "device-1", {})
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async def test_unknown_event_only_reaches_legacy_callback():
|
|
102
|
+
legacy = Mock()
|
|
103
|
+
state = Mock()
|
|
104
|
+
added = Mock()
|
|
105
|
+
removed = Mock()
|
|
106
|
+
event_client = client(
|
|
107
|
+
event_callback=legacy,
|
|
108
|
+
state_callback=state,
|
|
109
|
+
device_added_callback=added,
|
|
110
|
+
device_removed_callback=removed,
|
|
111
|
+
)
|
|
112
|
+
event = BeatbotEvent("event-1", "future_type", "device-1", {})
|
|
113
|
+
|
|
114
|
+
await event_client._async_dispatch_event(event)
|
|
115
|
+
|
|
116
|
+
legacy.assert_called_once_with(event)
|
|
117
|
+
state.assert_not_called()
|
|
118
|
+
added.assert_not_called()
|
|
119
|
+
removed.assert_not_called()
|
|
120
|
+
|
|
121
|
+
|
|
52
122
|
async def test_reconnect_callback_runs_after_first_connection():
|
|
53
123
|
reconnect_callback = AsyncMock()
|
|
54
124
|
event_client = client(reconnect_callback=reconnect_callback)
|
|
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
|