meshagent-api 0.42.2__tar.gz → 0.43.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.
- {meshagent_api-0.42.2/meshagent_api.egg-info → meshagent_api-0.43.0}/PKG-INFO +1 -1
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/protocol.py +5 -1
- meshagent_api-0.43.0/meshagent/api/version.py +1 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/websocket_protocol.py +57 -5
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/websocket_protocol_test.py +70 -4
- {meshagent_api-0.42.2 → meshagent_api-0.43.0/meshagent_api.egg-info}/PKG-INFO +1 -1
- meshagent_api-0.42.2/meshagent/api/version.py +0 -1
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/LICENSE +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/MANIFEST.in +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/README.md +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/__init__.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/agent_content.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/aiohttp_redaction.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/chan.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/client.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/client_test.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/crdt.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/dataset_schema.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/entrypoint.js +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/error_codes.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/helpers.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/http.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/image_runtime.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/keys.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/managed_agents.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/managed_agents_test.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/messaging.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/messaging_test.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/oauth.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/oauth_scopes.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/oauth_scopes_test.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/participant.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/participant_token.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/participant_token_test.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/port_forward.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/port_forward_test.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/protocol_test.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/py.typed +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/reasoning_schema.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/registry_auth.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/room_ports.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/room_server_client.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/room_server_client_test.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/runtime.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/runtime_test.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/schema.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/schema_document.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/schema_document_test.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/schema_registry.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/schema_test.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/schema_util.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/service_spec_test.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/service_template_test.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/services.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/specs/service.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/sql.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/sql_test.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/token_test.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/urls.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/webhooks.py +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent_api.egg-info/SOURCES.txt +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent_api.egg-info/dependency_links.txt +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent_api.egg-info/requires.txt +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent_api.egg-info/top_level.txt +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/pyproject.toml +0 -0
- {meshagent_api-0.42.2 → meshagent_api-0.43.0}/setup.cfg +0 -0
|
@@ -145,6 +145,8 @@ class Protocol:
|
|
|
145
145
|
async def _send_payload(self, message_id: int, data: bytes) -> None:
|
|
146
146
|
packets = compute_packets(data)
|
|
147
147
|
for i in range(packets):
|
|
148
|
+
if not self.is_open:
|
|
149
|
+
return
|
|
148
150
|
packet = bytearray()
|
|
149
151
|
packet.extend(message_id.to_bytes(8))
|
|
150
152
|
packet.extend(int(i + 1).to_bytes(4))
|
|
@@ -188,6 +190,8 @@ class Protocol:
|
|
|
188
190
|
header_data.extend(bytes(type, "utf-8"))
|
|
189
191
|
|
|
190
192
|
await self.send_packet(header_data)
|
|
193
|
+
if not self.is_open:
|
|
194
|
+
return
|
|
191
195
|
|
|
192
196
|
logger.debug("publishing message payload %s", message_id)
|
|
193
197
|
await self._send_payload(message_id=message_id, data=data)
|
|
@@ -341,7 +345,7 @@ class Protocol:
|
|
|
341
345
|
|
|
342
346
|
|
|
343
347
|
class ClientProtocol(Protocol):
|
|
344
|
-
def __init__(self, *, token: str):
|
|
348
|
+
def __init__(self, *, token: str | None):
|
|
345
349
|
super().__init__()
|
|
346
350
|
self._token = token
|
|
347
351
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.43.0"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import contextlib
|
|
2
2
|
import urllib.parse
|
|
3
3
|
from aiohttp import ClientSession, WSMsgType, web, ClientWebSocketResponse
|
|
4
|
+
from aiohttp.client_exceptions import ClientConnectionResetError
|
|
4
5
|
import asyncio
|
|
5
6
|
import logging
|
|
6
7
|
import os
|
|
@@ -80,12 +81,18 @@ def _format_websocket_close_reason(
|
|
|
80
81
|
return None
|
|
81
82
|
|
|
82
83
|
|
|
84
|
+
def _format_send_close_reason(exc: BaseException | None = None) -> str:
|
|
85
|
+
if exc is None:
|
|
86
|
+
return "websocket closed before send completed"
|
|
87
|
+
return str(exc).strip() or repr(exc)
|
|
88
|
+
|
|
89
|
+
|
|
83
90
|
class WebSocketClientProtocol(ClientProtocol):
|
|
84
91
|
def __init__(
|
|
85
92
|
self,
|
|
86
93
|
*,
|
|
87
94
|
url: str,
|
|
88
|
-
token: str,
|
|
95
|
+
token: str | None,
|
|
89
96
|
heartbeat: float | None = None,
|
|
90
97
|
session: ClientSession | None = None,
|
|
91
98
|
):
|
|
@@ -104,6 +111,16 @@ class WebSocketClientProtocol(ClientProtocol):
|
|
|
104
111
|
def url(self):
|
|
105
112
|
return self._url
|
|
106
113
|
|
|
114
|
+
@classmethod
|
|
115
|
+
def withIAP(
|
|
116
|
+
cls,
|
|
117
|
+
*,
|
|
118
|
+
url: str = "./.well-known/meshagent/room/connect",
|
|
119
|
+
heartbeat: float | None = None,
|
|
120
|
+
session: ClientSession | None = None,
|
|
121
|
+
):
|
|
122
|
+
return cls(url=url, token=None, heartbeat=heartbeat, session=session)
|
|
123
|
+
|
|
107
124
|
def create_factory(self):
|
|
108
125
|
session = self._session if self._session_external else None
|
|
109
126
|
|
|
@@ -129,7 +146,6 @@ class WebSocketClientProtocol(ClientProtocol):
|
|
|
129
146
|
|
|
130
147
|
url_parts = urllib.parse.urlparse(self._url)
|
|
131
148
|
query_dict = urllib.parse.parse_qs(url_parts.query)
|
|
132
|
-
query_dict.update({"token": self.token})
|
|
133
149
|
query_dict.update({"v": __version__})
|
|
134
150
|
new_query_string = urllib.parse.urlencode(query_dict, doseq=True)
|
|
135
151
|
url_with_params = urllib.parse.urlunparse(
|
|
@@ -143,8 +159,16 @@ class WebSocketClientProtocol(ClientProtocol):
|
|
|
143
159
|
)
|
|
144
160
|
)
|
|
145
161
|
|
|
162
|
+
headers = (
|
|
163
|
+
None
|
|
164
|
+
if self.token is None
|
|
165
|
+
else {"Authorization": f"Bearer {self.token}"}
|
|
166
|
+
)
|
|
146
167
|
self._ws_ctx = self._session.ws_connect(
|
|
147
|
-
url_with_params,
|
|
168
|
+
url_with_params,
|
|
169
|
+
headers=headers,
|
|
170
|
+
heartbeat=self._heartbeat,
|
|
171
|
+
compress=15,
|
|
148
172
|
)
|
|
149
173
|
self._ws = await self._ws_ctx.__aenter__()
|
|
150
174
|
self._ws_entered = True
|
|
@@ -220,7 +244,21 @@ class WebSocketClientProtocol(ClientProtocol):
|
|
|
220
244
|
await super().__aexit__(exc_type, exc, tb)
|
|
221
245
|
|
|
222
246
|
async def send_packet(self, data: bytes) -> None:
|
|
223
|
-
|
|
247
|
+
if not self.is_open or self._ws is None or self._ws.closed:
|
|
248
|
+
self._set_close_state(
|
|
249
|
+
kind=ProtocolCloseKind.SERVER,
|
|
250
|
+
reason="websocket closed before send completed",
|
|
251
|
+
)
|
|
252
|
+
self._shutdown()
|
|
253
|
+
return
|
|
254
|
+
try:
|
|
255
|
+
await self._ws.send_bytes(data)
|
|
256
|
+
except (ClientConnectionResetError, ConnectionResetError) as exc:
|
|
257
|
+
self._set_close_state(
|
|
258
|
+
kind=ProtocolCloseKind.SERVER,
|
|
259
|
+
reason=_format_send_close_reason(exc),
|
|
260
|
+
)
|
|
261
|
+
self._shutdown()
|
|
224
262
|
|
|
225
263
|
|
|
226
264
|
class WebSocketServerProtocol(Protocol):
|
|
@@ -301,4 +339,18 @@ class WebSocketServerProtocol(Protocol):
|
|
|
301
339
|
await super().__aexit__(exc_type=exc_type, exc=exc, tb=tb)
|
|
302
340
|
|
|
303
341
|
async def send_packet(self, data: bytes) -> None:
|
|
304
|
-
|
|
342
|
+
if not self.is_open or self.socket.closed:
|
|
343
|
+
self._set_close_state(
|
|
344
|
+
kind=ProtocolCloseKind.CLIENT,
|
|
345
|
+
reason="websocket closed before send completed",
|
|
346
|
+
)
|
|
347
|
+
self._shutdown()
|
|
348
|
+
return
|
|
349
|
+
try:
|
|
350
|
+
await self.socket.send_bytes(data)
|
|
351
|
+
except (ClientConnectionResetError, ConnectionResetError) as exc:
|
|
352
|
+
self._set_close_state(
|
|
353
|
+
kind=ProtocolCloseKind.CLIENT,
|
|
354
|
+
reason=_format_send_close_reason(exc),
|
|
355
|
+
)
|
|
356
|
+
self._shutdown()
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import importlib
|
|
2
2
|
|
|
3
|
+
import aiohttp
|
|
3
4
|
import pytest
|
|
4
5
|
|
|
6
|
+
from meshagent.api.protocol import ProtocolCloseKind
|
|
5
7
|
from meshagent.api.websocket_protocol import (
|
|
6
8
|
DEFAULT_WEBSOCKET_HEARTBEAT,
|
|
7
9
|
WEBSOCKET_HEARTBEAT_ENV,
|
|
8
10
|
WebSocketClientProtocol,
|
|
11
|
+
WebSocketServerProtocol,
|
|
9
12
|
resolve_websocket_heartbeat,
|
|
10
13
|
)
|
|
11
14
|
|
|
@@ -24,7 +27,9 @@ class _FakeSession:
|
|
|
24
27
|
def __init__(self) -> None:
|
|
25
28
|
self.entered = False
|
|
26
29
|
self.exited = False
|
|
27
|
-
self.ws_connect_calls: list[
|
|
30
|
+
self.ws_connect_calls: list[
|
|
31
|
+
tuple[str, dict[str, str] | None, float | None, int | None]
|
|
32
|
+
] = []
|
|
28
33
|
|
|
29
34
|
async def __aenter__(self):
|
|
30
35
|
self.entered = True
|
|
@@ -34,12 +39,26 @@ class _FakeSession:
|
|
|
34
39
|
self.exited = True
|
|
35
40
|
|
|
36
41
|
def ws_connect(
|
|
37
|
-
self,
|
|
42
|
+
self,
|
|
43
|
+
url: str,
|
|
44
|
+
*,
|
|
45
|
+
headers: dict[str, str] | None = None,
|
|
46
|
+
heartbeat: float | None = None,
|
|
47
|
+
compress: int | None = None,
|
|
38
48
|
):
|
|
39
|
-
self.ws_connect_calls.append((url, heartbeat, compress))
|
|
49
|
+
self.ws_connect_calls.append((url, headers, heartbeat, compress))
|
|
40
50
|
return _FailingWebsocketContext()
|
|
41
51
|
|
|
42
52
|
|
|
53
|
+
class _ClosingWebSocket:
|
|
54
|
+
closed = False
|
|
55
|
+
|
|
56
|
+
async def send_bytes(self, data: bytes) -> None:
|
|
57
|
+
raise aiohttp.client_exceptions.ClientConnectionResetError(
|
|
58
|
+
"Cannot write to closing transport"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
43
62
|
def test_resolve_websocket_heartbeat_uses_default(
|
|
44
63
|
monkeypatch: pytest.MonkeyPatch,
|
|
45
64
|
) -> None:
|
|
@@ -107,4 +126,51 @@ async def test_websocket_client_protocol_keeps_external_session_open_when_enter_
|
|
|
107
126
|
assert session.entered is False
|
|
108
127
|
assert session.exited is False
|
|
109
128
|
assert len(session.ws_connect_calls) == 1
|
|
110
|
-
assert session.ws_connect_calls[0][
|
|
129
|
+
assert session.ws_connect_calls[0][1] == {"Authorization": "Bearer token"}
|
|
130
|
+
assert session.ws_connect_calls[0][3] == 15
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@pytest.mark.asyncio
|
|
134
|
+
async def test_websocket_client_protocol_with_iap_omits_authorization_header():
|
|
135
|
+
session = _FakeSession()
|
|
136
|
+
protocol = WebSocketClientProtocol.withIAP(session=session)
|
|
137
|
+
|
|
138
|
+
with pytest.raises(RuntimeError, match="websocket failed"):
|
|
139
|
+
await protocol.__aenter__()
|
|
140
|
+
|
|
141
|
+
assert protocol.url == "./.well-known/meshagent/room/connect"
|
|
142
|
+
assert protocol.token is None
|
|
143
|
+
assert len(session.ws_connect_calls) == 1
|
|
144
|
+
assert session.ws_connect_calls[0][0].startswith(
|
|
145
|
+
"./.well-known/meshagent/room/connect?v="
|
|
146
|
+
)
|
|
147
|
+
assert session.ws_connect_calls[0][1] is None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@pytest.mark.asyncio
|
|
151
|
+
async def test_websocket_client_protocol_treats_closing_transport_as_server_close():
|
|
152
|
+
protocol = WebSocketClientProtocol(
|
|
153
|
+
url="ws://example.test/room",
|
|
154
|
+
token="token",
|
|
155
|
+
session=_FakeSession(),
|
|
156
|
+
)
|
|
157
|
+
protocol._open = True
|
|
158
|
+
protocol._ws = _ClosingWebSocket()
|
|
159
|
+
|
|
160
|
+
await protocol.send_packet(b"hello")
|
|
161
|
+
|
|
162
|
+
assert protocol.close_kind() == ProtocolCloseKind.SERVER
|
|
163
|
+
assert protocol.close_reason() == "Cannot write to closing transport"
|
|
164
|
+
assert protocol.is_open is False
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@pytest.mark.asyncio
|
|
168
|
+
async def test_websocket_server_protocol_treats_closing_transport_as_client_close():
|
|
169
|
+
protocol = WebSocketServerProtocol(socket=_ClosingWebSocket())
|
|
170
|
+
protocol._open = True
|
|
171
|
+
|
|
172
|
+
await protocol.send_packet(b"hello")
|
|
173
|
+
|
|
174
|
+
assert protocol.close_kind() == ProtocolCloseKind.CLIENT
|
|
175
|
+
assert protocol.close_reason() == "Cannot write to closing transport"
|
|
176
|
+
assert protocol.is_open is False
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.42.2"
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|