hypha-rpc 0.20.92__tar.gz → 0.20.94__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.
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/PKG-INFO +1 -1
- hypha_rpc-0.20.94/hypha_rpc/VERSION +3 -0
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/hypha_rpc/__init__.py +18 -7
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/hypha_rpc/http_client.py +45 -16
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/hypha_rpc/rpc.py +231 -136
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/hypha_rpc/utils/__init__.py +15 -9
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/hypha_rpc/websocket_client.py +42 -8
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/hypha_rpc.egg-info/PKG-INFO +1 -1
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/hypha_rpc.egg-info/SOURCES.txt +1 -0
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/pyproject.toml +1 -1
- hypha_rpc-0.20.94/tests/test_http_rpc.py +728 -0
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/tests/test_reconnection_runner.py +57 -46
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/tests/test_reconnection_stability.py +296 -194
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/tests/test_websocket_rpc.py +859 -627
- hypha_rpc-0.20.92/hypha_rpc/VERSION +0 -3
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/MANIFEST.in +0 -0
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/README.md +0 -0
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/hypha_rpc/pyodide_sse.py +0 -0
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/hypha_rpc/pyodide_websocket.py +0 -0
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/hypha_rpc/sync.py +0 -0
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/hypha_rpc/utils/launch.py +0 -0
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/hypha_rpc/utils/mcp.py +0 -0
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/hypha_rpc/utils/pydantic.py +0 -0
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/hypha_rpc/utils/schema.py +0 -0
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/hypha_rpc/utils/serve.py +0 -0
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/hypha_rpc/webrtc_client.py +0 -0
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/hypha_rpc.egg-info/dependency_links.txt +0 -0
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/hypha_rpc.egg-info/requires.txt +0 -0
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/hypha_rpc.egg-info/top_level.txt +0 -0
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/setup.cfg +0 -0
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/tests/test_mcp.py +0 -0
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/tests/test_schema.py +0 -0
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/tests/test_server_compatibility.py +0 -0
- {hypha_rpc-0.20.92 → hypha_rpc-0.20.94}/tests/test_utils.py +0 -0
|
@@ -28,11 +28,12 @@ from .http_client import HTTPStreamingRPCConnection
|
|
|
28
28
|
with open(os.path.join(os.path.dirname(__file__), "VERSION"), "r") as f:
|
|
29
29
|
__version__ = json.load(f)["version"]
|
|
30
30
|
|
|
31
|
+
|
|
31
32
|
def is_user_defined_class_instance(obj):
|
|
32
33
|
return (
|
|
33
|
-
not isinstance(obj, type)
|
|
34
|
-
hasattr(obj, "__class__")
|
|
35
|
-
obj.__class__.__module__ != "builtins"
|
|
34
|
+
not isinstance(obj, type) # not a class itself
|
|
35
|
+
and hasattr(obj, "__class__")
|
|
36
|
+
and obj.__class__.__module__ != "builtins" # not a built-in type
|
|
36
37
|
)
|
|
37
38
|
|
|
38
39
|
|
|
@@ -41,23 +42,34 @@ class API(ObjectProxy):
|
|
|
41
42
|
super().__init__(*args, **kwargs)
|
|
42
43
|
self._registry = {}
|
|
43
44
|
self._export_handler = self._default_export_handler
|
|
44
|
-
|
|
45
|
+
|
|
45
46
|
async def _register_services(self, obj, config=None, **kwargs):
|
|
46
47
|
if not os.environ.get("HYPHA_SERVER_URL"):
|
|
47
48
|
try:
|
|
48
49
|
from dotenv import load_dotenv, find_dotenv
|
|
50
|
+
|
|
49
51
|
load_dotenv(dotenv_path=find_dotenv(usecwd=True))
|
|
50
52
|
# use info from .env file
|
|
51
53
|
print("✅ Loaded connection configuration from .env file.")
|
|
52
54
|
except ImportError:
|
|
53
|
-
print(
|
|
55
|
+
print(
|
|
56
|
+
"❌ Missing environment variables. Set HYPHA_SERVER_URL, HYPHA_TOKEN, HYPHA_WORKSPACE",
|
|
57
|
+
file=sys.stderr,
|
|
58
|
+
)
|
|
54
59
|
sys.exit(1)
|
|
55
60
|
SERVER_URL = os.environ.get("HYPHA_SERVER_URL")
|
|
56
61
|
TOKEN = os.environ.get("HYPHA_TOKEN")
|
|
57
62
|
CLIENT_ID = os.environ.get("HYPHA_CLIENT_ID")
|
|
58
63
|
WORKSPACE = os.environ.get("HYPHA_WORKSPACE")
|
|
59
64
|
|
|
60
|
-
server = await connect_to_server(
|
|
65
|
+
server = await connect_to_server(
|
|
66
|
+
{
|
|
67
|
+
"client_id": CLIENT_ID,
|
|
68
|
+
"server_url": SERVER_URL,
|
|
69
|
+
"token": TOKEN,
|
|
70
|
+
"workspace": WORKSPACE,
|
|
71
|
+
}
|
|
72
|
+
)
|
|
61
73
|
# If obj is a class, instantiate it
|
|
62
74
|
if isinstance(obj, type):
|
|
63
75
|
obj = obj()
|
|
@@ -96,7 +108,6 @@ class API(ObjectProxy):
|
|
|
96
108
|
asyncio.create_task(self._register_services(obj, config, **kwargs))
|
|
97
109
|
else:
|
|
98
110
|
asyncio.run(self._register_services(obj, config, **kwargs))
|
|
99
|
-
|
|
100
111
|
|
|
101
112
|
def set_export_handler(self, handler):
|
|
102
113
|
self._export_handler = handler
|
|
@@ -134,7 +134,9 @@ class HTTPStreamingRPCConnection:
|
|
|
134
134
|
if response.status_code == 200:
|
|
135
135
|
logger.debug("Token refresh requested successfully")
|
|
136
136
|
else:
|
|
137
|
-
logger.warning(
|
|
137
|
+
logger.warning(
|
|
138
|
+
f"Token refresh request failed: {response.status_code}"
|
|
139
|
+
)
|
|
138
140
|
except Exception as e:
|
|
139
141
|
logger.warning(f"Failed to send refresh token request: {e}")
|
|
140
142
|
|
|
@@ -174,27 +176,43 @@ class HTTPStreamingRPCConnection:
|
|
|
174
176
|
elif self._ssl is not None:
|
|
175
177
|
verify = self._ssl
|
|
176
178
|
|
|
179
|
+
# Try to enable HTTP/2 if h2 is available
|
|
180
|
+
try:
|
|
181
|
+
import h2 # noqa
|
|
182
|
+
|
|
183
|
+
http2_enabled = True
|
|
184
|
+
logger.info("HTTP/2 enabled for improved performance")
|
|
185
|
+
except ImportError:
|
|
186
|
+
http2_enabled = False
|
|
187
|
+
logger.debug(
|
|
188
|
+
"HTTP/2 not available (install httpx[http2] for better performance)"
|
|
189
|
+
)
|
|
190
|
+
|
|
177
191
|
return httpx.AsyncClient(
|
|
178
192
|
timeout=httpx.Timeout(self._timeout, connect=30.0),
|
|
179
193
|
verify=verify,
|
|
180
|
-
#
|
|
194
|
+
# Optimized connection pooling for high-performance RPC
|
|
181
195
|
limits=httpx.Limits(
|
|
182
|
-
max_connections=
|
|
183
|
-
max_keepalive_connections=
|
|
184
|
-
keepalive_expiry=
|
|
196
|
+
max_connections=200, # Max total connections (increased for parallel requests)
|
|
197
|
+
max_keepalive_connections=50, # More reusable connections (up from 20)
|
|
198
|
+
keepalive_expiry=300.0, # Keep connections alive longer (5 minutes)
|
|
185
199
|
),
|
|
200
|
+
# Enable HTTP/2 for better multiplexing if available
|
|
201
|
+
http2=http2_enabled,
|
|
186
202
|
)
|
|
187
203
|
|
|
188
204
|
async def open(self):
|
|
189
205
|
"""Open the streaming connection."""
|
|
190
|
-
logger.info(
|
|
206
|
+
logger.info(
|
|
207
|
+
f"Opening HTTP streaming connection to {self._server_url} (format={self._format})"
|
|
208
|
+
)
|
|
191
209
|
|
|
192
210
|
if self._http_client is None:
|
|
193
211
|
self._http_client = await self._create_http_client()
|
|
194
212
|
|
|
195
|
-
# Build stream URL
|
|
196
|
-
|
|
197
|
-
stream_url = f"{self._server_url}/{
|
|
213
|
+
# Build stream URL - workspace is part of path, default to "public" for anonymous
|
|
214
|
+
ws = self._workspace or "public"
|
|
215
|
+
stream_url = f"{self._server_url}/{ws}/rpc"
|
|
198
216
|
params = {"client_id": self._client_id}
|
|
199
217
|
if self._format == "msgpack":
|
|
200
218
|
params["format"] = "msgpack"
|
|
@@ -346,15 +364,15 @@ class HTTPStreamingRPCConnection:
|
|
|
346
364
|
# Process complete frames from buffer
|
|
347
365
|
while len(buffer) >= 4:
|
|
348
366
|
# Read 4-byte length prefix (big-endian)
|
|
349
|
-
length = int.from_bytes(buffer[:4],
|
|
367
|
+
length = int.from_bytes(buffer[:4], "big")
|
|
350
368
|
|
|
351
369
|
if len(buffer) < 4 + length:
|
|
352
370
|
# Incomplete frame, wait for more data
|
|
353
371
|
break
|
|
354
372
|
|
|
355
373
|
# Extract the frame
|
|
356
|
-
frame_data = buffer[4:4 + length]
|
|
357
|
-
buffer = buffer[4 + length:]
|
|
374
|
+
frame_data = buffer[4 : 4 + length]
|
|
375
|
+
buffer = buffer[4 + length :]
|
|
358
376
|
|
|
359
377
|
try:
|
|
360
378
|
# For msgpack, first check if it's a control message
|
|
@@ -418,18 +436,24 @@ class HTTPStreamingRPCConnection:
|
|
|
418
436
|
self._handle_message(data)
|
|
419
437
|
|
|
420
438
|
async def emit_message(self, data: bytes):
|
|
421
|
-
"""Send a message to the server via HTTP POST.
|
|
439
|
+
"""Send a message to the server via HTTP POST.
|
|
440
|
+
|
|
441
|
+
Uses optimized connection pooling with keep-alive for better performance.
|
|
442
|
+
HTTP client automatically handles efficient transfer for all payload sizes.
|
|
443
|
+
"""
|
|
422
444
|
if self._closed:
|
|
423
445
|
raise ConnectionError("Connection is closed")
|
|
424
446
|
|
|
425
447
|
if self._http_client is None:
|
|
426
448
|
self._http_client = await self._create_http_client()
|
|
427
449
|
|
|
428
|
-
workspace
|
|
429
|
-
|
|
450
|
+
# Build POST URL - workspace is part of path (must be set after connection)
|
|
451
|
+
ws = self._workspace or "public"
|
|
452
|
+
url = f"{self._server_url}/{ws}/rpc"
|
|
430
453
|
params = {"client_id": self._client_id}
|
|
431
454
|
|
|
432
455
|
try:
|
|
456
|
+
# httpx handles large payloads efficiently with connection pooling
|
|
433
457
|
response = await self._http_client.post(
|
|
434
458
|
url,
|
|
435
459
|
content=data,
|
|
@@ -438,7 +462,9 @@ class HTTPStreamingRPCConnection:
|
|
|
438
462
|
)
|
|
439
463
|
|
|
440
464
|
if response.status_code != 200:
|
|
441
|
-
error =
|
|
465
|
+
error = (
|
|
466
|
+
response.json() if response.content else {"detail": "Unknown error"}
|
|
467
|
+
)
|
|
442
468
|
raise ConnectionError(f"POST failed: {error.get('detail', error)}")
|
|
443
469
|
|
|
444
470
|
except httpx.TimeoutException:
|
|
@@ -512,6 +538,7 @@ def connect_to_server_http(config=None, **kwargs):
|
|
|
512
538
|
ServerContextManager that can be used as async context manager
|
|
513
539
|
"""
|
|
514
540
|
from .websocket_client import connect_to_server
|
|
541
|
+
|
|
515
542
|
config = config or {}
|
|
516
543
|
config.update(kwargs)
|
|
517
544
|
config["transport"] = "http"
|
|
@@ -609,6 +636,7 @@ async def _connect_to_server_http(config: dict):
|
|
|
609
636
|
|
|
610
637
|
# Handle force-exit from manager
|
|
611
638
|
if connection.manager_id:
|
|
639
|
+
|
|
612
640
|
async def handle_disconnect(message):
|
|
613
641
|
if message.get("from") == "*/" + connection.manager_id:
|
|
614
642
|
logger.info(f"Disconnecting from server: {message.get('reason')}")
|
|
@@ -626,6 +654,7 @@ def get_remote_service_http(service_uri: str, config=None, **kwargs):
|
|
|
626
654
|
For a unified interface, use get_remote_service with transport="http" instead.
|
|
627
655
|
"""
|
|
628
656
|
from .websocket_client import get_remote_service
|
|
657
|
+
|
|
629
658
|
config = config or {}
|
|
630
659
|
config.update(kwargs)
|
|
631
660
|
config["transport"] = "http"
|