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.
Files changed (66) hide show
  1. {meshagent_api-0.42.2/meshagent_api.egg-info → meshagent_api-0.43.0}/PKG-INFO +1 -1
  2. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/protocol.py +5 -1
  3. meshagent_api-0.43.0/meshagent/api/version.py +1 -0
  4. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/websocket_protocol.py +57 -5
  5. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/websocket_protocol_test.py +70 -4
  6. {meshagent_api-0.42.2 → meshagent_api-0.43.0/meshagent_api.egg-info}/PKG-INFO +1 -1
  7. meshagent_api-0.42.2/meshagent/api/version.py +0 -1
  8. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/LICENSE +0 -0
  9. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/MANIFEST.in +0 -0
  10. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/README.md +0 -0
  11. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/__init__.py +0 -0
  12. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/agent_content.py +0 -0
  13. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/aiohttp_redaction.py +0 -0
  14. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/chan.py +0 -0
  15. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/client.py +0 -0
  16. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/client_test.py +0 -0
  17. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/crdt.py +0 -0
  18. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/dataset_schema.py +0 -0
  19. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/entrypoint.js +0 -0
  20. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/error_codes.py +0 -0
  21. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/helpers.py +0 -0
  22. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/http.py +0 -0
  23. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/image_runtime.py +0 -0
  24. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/keys.py +0 -0
  25. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/managed_agents.py +0 -0
  26. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/managed_agents_test.py +0 -0
  27. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/messaging.py +0 -0
  28. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/messaging_test.py +0 -0
  29. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/oauth.py +0 -0
  30. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/oauth_scopes.py +0 -0
  31. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/oauth_scopes_test.py +0 -0
  32. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/participant.py +0 -0
  33. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/participant_token.py +0 -0
  34. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/participant_token_test.py +0 -0
  35. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/port_forward.py +0 -0
  36. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/port_forward_test.py +0 -0
  37. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/protocol_test.py +0 -0
  38. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/py.typed +0 -0
  39. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/reasoning_schema.py +0 -0
  40. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/registry_auth.py +0 -0
  41. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/room_ports.py +0 -0
  42. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/room_server_client.py +0 -0
  43. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/room_server_client_test.py +0 -0
  44. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/runtime.py +0 -0
  45. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/runtime_test.py +0 -0
  46. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/schema.py +0 -0
  47. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/schema_document.py +0 -0
  48. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/schema_document_test.py +0 -0
  49. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/schema_registry.py +0 -0
  50. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/schema_test.py +0 -0
  51. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/schema_util.py +0 -0
  52. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/service_spec_test.py +0 -0
  53. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/service_template_test.py +0 -0
  54. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/services.py +0 -0
  55. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/specs/service.py +0 -0
  56. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/sql.py +0 -0
  57. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/sql_test.py +0 -0
  58. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/token_test.py +0 -0
  59. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/urls.py +0 -0
  60. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent/api/webhooks.py +0 -0
  61. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent_api.egg-info/SOURCES.txt +0 -0
  62. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent_api.egg-info/dependency_links.txt +0 -0
  63. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent_api.egg-info/requires.txt +0 -0
  64. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/meshagent_api.egg-info/top_level.txt +0 -0
  65. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/pyproject.toml +0 -0
  66. {meshagent_api-0.42.2 → meshagent_api-0.43.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshagent-api
3
- Version: 0.42.2
3
+ Version: 0.43.0
4
4
  Summary: Python Server API for Meshagent
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Documentation, https://docs.meshagent.com
@@ -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, heartbeat=self._heartbeat, compress=15
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
- await self._ws.send_bytes(data)
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
- await self.socket.send_bytes(data)
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[tuple[str, float | None, int | None]] = []
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, url: str, *, heartbeat: float | None = None, compress: int | None = None
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][2] == 15
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshagent-api
3
- Version: 0.42.2
3
+ Version: 0.43.0
4
4
  Summary: Python Server API for Meshagent
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Documentation, https://docs.meshagent.com
@@ -1 +0,0 @@
1
- __version__ = "0.42.2"
File without changes
File without changes
File without changes