phystack-hub-client 6.16.0__tar.gz → 6.18.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.
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/CHANGELOG.md +18 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/PKG-INFO +1 -1
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/pyproject.toml +1 -1
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/src/phystack/hub_client/client.py +12 -2
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/src/phystack/hub_client/cloud_twins.py +11 -3
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/src/phystack/hub_client/services/webrtc/data_channel.py +8 -4
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/src/phystack/hub_client/services/webrtc/data_responder.py +4 -2
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/src/phystack/hub_client/services/webrtc/media_responder.py +4 -2
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/src/phystack/hub_client/services/webrtc/media_stream.py +21 -5
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/tests/test_client_integration.py +31 -1
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/tests/test_media_resilience.py +21 -8
- phystack_hub_client-6.18.2/tests/test_no_google_stun.py +77 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/.gitignore +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/README.md +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/examples/browser_test_backend.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/scripts/python-node-test.sh +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/scripts/webrtc-test.sh +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/src/phystack/hub_client/__init__.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/src/phystack/hub_client/advisory_validation.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/src/phystack/hub_client/instances/__init__.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/src/phystack/hub_client/instances/base_instance.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/src/phystack/hub_client/instances/edge_instance.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/src/phystack/hub_client/instances/peripheral_instance.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/src/phystack/hub_client/scoped_hardware_id.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/src/phystack/hub_client/services/__init__.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/src/phystack/hub_client/services/cloud_app_connection.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/src/phystack/hub_client/services/cloud_app_token.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/src/phystack/hub_client/services/phyhub_connection.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/src/phystack/hub_client/services/signals.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/src/phystack/hub_client/services/web_session_code.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/src/phystack/hub_client/services/webrtc/__init__.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/src/phystack/hub_client/services/webrtc/media_source.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/src/phystack/hub_client/twin_messaging.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/src/phystack/hub_client/twin_registry.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/src/phystack/hub_client/types/__init__.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/src/phystack/hub_client/types/twin_types.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/tests/test_advisory_validation.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/tests/test_cloud_app_token.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/tests/test_cloud_twins.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/tests/test_communication.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/tests/test_data_channel.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/tests/test_instance_listeners.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/tests/test_media_kinds.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/tests/test_media_responder.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/tests/test_media_source.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/tests/test_peripheral_lifecycle.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/tests/test_signals_rollover.py +0 -0
- {phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/tests/test_web_session_code.py +0 -0
|
@@ -6,6 +6,24 @@ publishes even minors from `main`, so a Python release is the next even minor at
|
|
|
6
6
|
or above the npm version it matches; the entries below name the npm version and
|
|
7
7
|
PRs each change was ported from.
|
|
8
8
|
|
|
9
|
+
## 6.18.0 — parity with npm 6.16.0 (TECH-1521)
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Removed the hardcoded Google STUN fallback (npm 6.16.0, ps-platform #973,
|
|
14
|
+
TECH-1516/TECH-1521). `MediaStreamOptions.stun_servers` and
|
|
15
|
+
`DataChannelOptions.stun_servers` now default to the `PHYSTACK_STUN_URL` env
|
|
16
|
+
var when set, otherwise an empty list — with no `ice_servers_provider`
|
|
17
|
+
resolving, connections degrade to host candidates only instead of contacting
|
|
18
|
+
`stun.l.google.com`. All STUN normally comes from the phyhub-minted ICE list
|
|
19
|
+
(`stun:turn-<region>.phystack.com:3478`).
|
|
20
|
+
- Every configuration builder now returns an explicit `RTCConfiguration`
|
|
21
|
+
(possibly with an empty server list) instead of `None`: aiortc substitutes
|
|
22
|
+
its own default — Google STUN — when a peer connection is created without a
|
|
23
|
+
configuration, which would have silently reintroduced the third-party STUN.
|
|
24
|
+
- Guard tests: defaults carry no third-party STUN, and a source scan fails the
|
|
25
|
+
suite if a Google STUN host/port literal ever reappears in the package.
|
|
26
|
+
|
|
9
27
|
## 6.16.0 — parity with npm 6.15.1 (TECH-1483)
|
|
10
28
|
|
|
11
29
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: phystack-hub-client
|
|
3
|
-
Version: 6.
|
|
3
|
+
Version: 6.18.2
|
|
4
4
|
Summary: Python client for PhyHub - events, actions, DataChannel, and MediaStream
|
|
5
5
|
Project-URL: Homepage, https://github.com/phystack/phystack-pypi
|
|
6
6
|
Project-URL: Documentation, https://docs.phystack.com
|
|
@@ -7,7 +7,7 @@ name = "phystack-hub-client"
|
|
|
7
7
|
# Fixed version group with the Node hub-client (@phystack/hub-client in
|
|
8
8
|
# phystack/ps-platform): both clients implement the same twin/WebRTC contract
|
|
9
9
|
# and are released together under the same version.
|
|
10
|
-
version = "6.
|
|
10
|
+
version = "6.18.2"
|
|
11
11
|
description = "Python client for PhyHub - events, actions, DataChannel, and MediaStream"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
license = "MIT"
|
|
@@ -365,8 +365,18 @@ class PhyHubClient:
|
|
|
365
365
|
|
|
366
366
|
self._connection.on(self.EVENTS.TWIN_MESSAGE, on_twin_message)
|
|
367
367
|
|
|
368
|
-
def on_twin_updated(payload: dict[str, Any]) -> None:
|
|
369
|
-
"""Handle twin property update events.
|
|
368
|
+
def on_twin_updated(payload: dict[str, Any], *_extra: Any) -> None:
|
|
369
|
+
"""Handle twin property update events.
|
|
370
|
+
|
|
371
|
+
``*_extra`` absorbs any trailing positional argument the server
|
|
372
|
+
appends to the ``twinUpdated`` emit. Some phyhub builds send twin
|
|
373
|
+
lifecycle events with a second positional arg (event metadata);
|
|
374
|
+
JS clients silently ignore it, but python-socketio passes every
|
|
375
|
+
positional arg through, so a strict ``(payload)`` signature raises
|
|
376
|
+
"takes 1 positional argument but 2 were given" and DROPS EVERY twin
|
|
377
|
+
update — the device stops seeing settings/desired-property changes
|
|
378
|
+
until restarted. Matches the tolerant ``on_twin_message`` sibling.
|
|
379
|
+
"""
|
|
370
380
|
twin_data = payload.get("data") or payload
|
|
371
381
|
twin_id = payload.get("twinId") or (twin_data.get("id") if isinstance(twin_data, dict) else None)
|
|
372
382
|
|
{phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/src/phystack/hub_client/cloud_twins.py
RENAMED
|
@@ -83,15 +83,23 @@ class CloudTwinStore:
|
|
|
83
83
|
|
|
84
84
|
# --- lifecycle events ---
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
# ``*_extra`` on these three absorbs any trailing positional arg the
|
|
87
|
+
# server appends to a twin lifecycle emit. They are registered directly
|
|
88
|
+
# as socket.io handlers (PhyHubClient cloud-app mode), so a strict
|
|
89
|
+
# ``(payload)`` signature would raise "takes 1 positional argument but 2
|
|
90
|
+
# were given" and silently drop every cloud-twin update — the same defect
|
|
91
|
+
# fixed in PhyHubClient.on_twin_updated. Direct internal callers still
|
|
92
|
+
# pass a single argument.
|
|
93
|
+
|
|
94
|
+
def apply_created(self, payload: CloudTwinLifecycleEvent, *_extra: Any) -> None:
|
|
87
95
|
"""Apply a ``twinCreated`` event and notify ``on_created`` listeners."""
|
|
88
96
|
self._apply(_KIND_CREATED, payload)
|
|
89
97
|
|
|
90
|
-
def apply_updated(self, payload: CloudTwinLifecycleEvent) -> None:
|
|
98
|
+
def apply_updated(self, payload: CloudTwinLifecycleEvent, *_extra: Any) -> None:
|
|
91
99
|
"""Apply a ``twinUpdated`` event and notify ``on_updated`` listeners."""
|
|
92
100
|
self._apply(_KIND_UPDATED, payload)
|
|
93
101
|
|
|
94
|
-
def apply_deleted(self, payload: CloudTwinLifecycleEvent) -> None:
|
|
102
|
+
def apply_deleted(self, payload: CloudTwinLifecycleEvent, *_extra: Any) -> None:
|
|
95
103
|
"""Apply a ``twinDeleted`` event and notify ``on_deleted`` listeners."""
|
|
96
104
|
self._apply(_KIND_DELETED, payload)
|
|
97
105
|
|
|
@@ -39,6 +39,7 @@ from aiortc import (
|
|
|
39
39
|
)
|
|
40
40
|
|
|
41
41
|
from phystack.hub_client.services.webrtc.media_stream import (
|
|
42
|
+
_get_default_stun_servers,
|
|
42
43
|
apply_remote_ice_candidate,
|
|
43
44
|
build_ice_servers,
|
|
44
45
|
)
|
|
@@ -110,9 +111,9 @@ class DataChannelOptions:
|
|
|
110
111
|
"""Options for DataChannel connection."""
|
|
111
112
|
|
|
112
113
|
use_stun: bool = True
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
)
|
|
114
|
+
# Static STUN fallback (PHYSTACK_STUN_URL or empty — never third-party;
|
|
115
|
+
# TECH-1521, mirroring npm @phystack/hub-client 6.16.0).
|
|
116
|
+
stun_servers: list[str] = field(default_factory=_get_default_stun_servers)
|
|
116
117
|
turn_servers: list[dict[str, str]] = field(default_factory=_get_default_turn_servers)
|
|
117
118
|
ice_transport_policy: str = field(default_factory=_get_default_ice_transport_policy)
|
|
118
119
|
connection_timeout: float = 15.0
|
|
@@ -384,7 +385,10 @@ class DataChannelHandler:
|
|
|
384
385
|
if self._is_closed:
|
|
385
386
|
raise RuntimeError("DataChannelHandler closed during connection attempt")
|
|
386
387
|
|
|
387
|
-
|
|
388
|
+
# Always pass an explicit configuration: aiortc substitutes ITS OWN
|
|
389
|
+
# default (Google STUN) when given None — an explicit empty list means
|
|
390
|
+
# host candidates only (TECH-1521).
|
|
391
|
+
configuration = RTCConfiguration(iceServers=ice_servers)
|
|
388
392
|
peer_connection = RTCPeerConnection(configuration=configuration)
|
|
389
393
|
self._pc = peer_connection
|
|
390
394
|
self._pending_remote_candidates = []
|
|
@@ -536,9 +536,11 @@ class DataChannelResponder:
|
|
|
536
536
|
self._clear_connect_timer(peer_key)
|
|
537
537
|
self._unhealthy_since.pop(peer_key, None)
|
|
538
538
|
|
|
539
|
-
async def _build_configuration(self) ->
|
|
539
|
+
async def _build_configuration(self) -> RTCConfiguration:
|
|
540
540
|
ice_servers = await resolve_data_channel_ice_servers(
|
|
541
541
|
self._options.ice_options,
|
|
542
542
|
provider=self._options.ice_servers_provider,
|
|
543
543
|
)
|
|
544
|
-
|
|
544
|
+
# Explicit configuration always: aiortc's None default is Google STUN
|
|
545
|
+
# (TECH-1521); an empty list means host candidates only.
|
|
546
|
+
return RTCConfiguration(iceServers=ice_servers)
|
|
@@ -476,7 +476,7 @@ class MediaStreamResponder:
|
|
|
476
476
|
fps=ice_options.video_fps,
|
|
477
477
|
)
|
|
478
478
|
|
|
479
|
-
async def _build_configuration(self) ->
|
|
479
|
+
async def _build_configuration(self) -> RTCConfiguration:
|
|
480
480
|
extra_servers: list[dict[str, Any]] = []
|
|
481
481
|
if self._options.ice_servers_provider:
|
|
482
482
|
try:
|
|
@@ -489,7 +489,9 @@ class MediaStreamResponder:
|
|
|
489
489
|
provider_error,
|
|
490
490
|
)
|
|
491
491
|
ice_servers = build_ice_servers(self._options.ice_options, extra_servers)
|
|
492
|
-
|
|
492
|
+
# Explicit configuration always: aiortc's None default is Google STUN
|
|
493
|
+
# (TECH-1521); an empty list means host candidates only.
|
|
494
|
+
return RTCConfiguration(iceServers=ice_servers)
|
|
493
495
|
|
|
494
496
|
# =========================================================================
|
|
495
497
|
# Connect-timeout eviction (npm responder-fanout connectTimers parity)
|
|
@@ -94,6 +94,20 @@ def _get_default_ice_transport_policy() -> str:
|
|
|
94
94
|
return "relay" if os.environ.get("PHYSTACK_TURN_URL") else "all"
|
|
95
95
|
|
|
96
96
|
|
|
97
|
+
def _get_default_stun_servers() -> list[str]:
|
|
98
|
+
"""Static STUN fallback, used only when no ice_servers_provider resolves.
|
|
99
|
+
|
|
100
|
+
Must be OUR coturn (stun:turn-<region>.phystack.com:3478 via
|
|
101
|
+
PHYSTACK_STUN_URL) — never a third-party service like Google STUN
|
|
102
|
+
(TECH-1521, mirroring npm @phystack/hub-client 6.16.0). With nothing
|
|
103
|
+
configured the fallback is host candidates only: same-network connections
|
|
104
|
+
still work, and cross-NAT needs TURN anyway (which bare STUN never
|
|
105
|
+
provided).
|
|
106
|
+
"""
|
|
107
|
+
url = os.environ.get("PHYSTACK_STUN_URL")
|
|
108
|
+
return [url] if url else []
|
|
109
|
+
|
|
110
|
+
|
|
97
111
|
def build_ice_servers(
|
|
98
112
|
options: "MediaStreamOptions",
|
|
99
113
|
extra_servers: Optional[list[dict[str, Any]]] = None,
|
|
@@ -181,9 +195,7 @@ class MediaStreamOptions:
|
|
|
181
195
|
"""Options for MediaStream connection."""
|
|
182
196
|
|
|
183
197
|
use_stun: bool = True
|
|
184
|
-
stun_servers: list[str] = field(
|
|
185
|
-
default_factory=lambda: ["stun:stun.l.google.com:19302", "stun:stun1.l.google.com:19302"]
|
|
186
|
-
)
|
|
198
|
+
stun_servers: list[str] = field(default_factory=_get_default_stun_servers)
|
|
187
199
|
turn_servers: list[dict[str, str]] = field(default_factory=_get_default_turn_servers)
|
|
188
200
|
ice_transport_policy: str = field(default_factory=_get_default_ice_transport_policy)
|
|
189
201
|
connection_timeout: float = 15.0
|
|
@@ -594,7 +606,7 @@ class MediaStreamHandler:
|
|
|
594
606
|
return []
|
|
595
607
|
return servers or []
|
|
596
608
|
|
|
597
|
-
async def _build_rtc_configuration(self, use_stun: bool) ->
|
|
609
|
+
async def _build_rtc_configuration(self, use_stun: bool) -> RTCConfiguration:
|
|
598
610
|
"""Build the RTCConfiguration for one connection attempt.
|
|
599
611
|
|
|
600
612
|
Provider-supplied servers take precedence (listed first). The per-attempt
|
|
@@ -613,7 +625,11 @@ class MediaStreamHandler:
|
|
|
613
625
|
ice_servers = build_ice_servers(source_options, extra_servers)
|
|
614
626
|
if not use_stun or self._options.ice_transport_policy == "relay":
|
|
615
627
|
ice_servers = keep_turn_servers(ice_servers)
|
|
616
|
-
|
|
628
|
+
# Always pass an explicit configuration: aiortc substitutes ITS OWN
|
|
629
|
+
# default (Google STUN) when given None, which would silently
|
|
630
|
+
# reintroduce the third-party STUN this client no longer uses. An
|
|
631
|
+
# explicit empty list means host candidates only (TECH-1521).
|
|
632
|
+
return RTCConfiguration(iceServers=ice_servers)
|
|
617
633
|
|
|
618
634
|
async def _create_peer_connection(self, use_stun: bool) -> RTCPeerConnection:
|
|
619
635
|
"""Create a fresh RTCPeerConnection and wire its track/state handlers."""
|
|
@@ -12,6 +12,7 @@ class FakeConnection:
|
|
|
12
12
|
self.emitted: list[tuple[str, dict[str, Any]]] = []
|
|
13
13
|
self.device_id = "device-1"
|
|
14
14
|
self.ack_response: Optional[dict[str, Any]] = {"status": "success"}
|
|
15
|
+
self.handlers: dict[str, Any] = {}
|
|
15
16
|
|
|
16
17
|
@property
|
|
17
18
|
def is_connected(self) -> bool:
|
|
@@ -22,7 +23,7 @@ class FakeConnection:
|
|
|
22
23
|
return True
|
|
23
24
|
|
|
24
25
|
def on(self, event: str, handler: Any) -> None:
|
|
25
|
-
|
|
26
|
+
self.handlers[event] = handler
|
|
26
27
|
|
|
27
28
|
async def emit(
|
|
28
29
|
self, method: str, payload: Any = None, callback: bool = False
|
|
@@ -95,6 +96,35 @@ async def test_device_status_cached_for_ten_seconds() -> None:
|
|
|
95
96
|
assert status_calls.count("getDeviceStatus") == 1
|
|
96
97
|
|
|
97
98
|
|
|
99
|
+
def test_twin_updated_handler_tolerates_trailing_positional_arg() -> None:
|
|
100
|
+
"""Regression: some phyhub builds emit `twinUpdated` with a second
|
|
101
|
+
positional arg. python-socketio forwards every positional arg, so a
|
|
102
|
+
strict `(payload)` handler raised "takes 1 positional argument but 2 were
|
|
103
|
+
given" and dropped every twin update fleet-wide (edge apps stopped seeing
|
|
104
|
+
settings changes until restarted). The handler must accept and ignore the
|
|
105
|
+
extra arg the way JS clients do."""
|
|
106
|
+
client, connection = make_client()
|
|
107
|
+
client._setup_socket_listeners()
|
|
108
|
+
|
|
109
|
+
on_twin_updated = connection.handlers["twinUpdated"]
|
|
110
|
+
received: list[dict[str, Any]] = []
|
|
111
|
+
# Register the listener directly to keep the test on the handler's arity,
|
|
112
|
+
# not the subscribe machinery (`on_twin_update` spawns an async task).
|
|
113
|
+
client._twin_update_listeners["twin-x"] = {lambda data: received.append(data)}
|
|
114
|
+
|
|
115
|
+
payload = {
|
|
116
|
+
"twinId": "twin-x",
|
|
117
|
+
"data": {"id": "twin-x", "properties": {"desired": {"volume": 5}}},
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# One positional arg (phyhub master) — the baseline that already worked.
|
|
121
|
+
on_twin_updated(payload)
|
|
122
|
+
# Two positional args (the QA build that triggered the crash) — must not raise.
|
|
123
|
+
on_twin_updated(payload, {"eventId": "evt-1"})
|
|
124
|
+
|
|
125
|
+
assert received == [payload["data"], payload["data"]]
|
|
126
|
+
|
|
127
|
+
|
|
98
128
|
def test_public_exports_importable() -> None:
|
|
99
129
|
import phystack.hub_client as hub_client
|
|
100
130
|
|
|
@@ -63,7 +63,10 @@ def make_handler(bus: FakeTwinMessageBus, options: MediaStreamOptions) -> MediaS
|
|
|
63
63
|
# =============================================================================
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
async def test_ice_servers_provider_servers_land_in_configuration_first(
|
|
66
|
+
async def test_ice_servers_provider_servers_land_in_configuration_first(
|
|
67
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
68
|
+
) -> None:
|
|
69
|
+
monkeypatch.delenv("PHYSTACK_STUN_URL", raising=False)
|
|
67
70
|
provider_calls: list[int] = []
|
|
68
71
|
|
|
69
72
|
async def provider() -> list[dict[str, Any]]:
|
|
@@ -84,11 +87,17 @@ async def test_ice_servers_provider_servers_land_in_configuration_first() -> Non
|
|
|
84
87
|
first_url = first_server_urls[0] if isinstance(first_server_urls, list) else first_server_urls
|
|
85
88
|
assert first_url == TURN_URL
|
|
86
89
|
assert configuration.iceServers[0].username == "user"
|
|
87
|
-
#
|
|
88
|
-
|
|
90
|
+
# No static third-party STUN follows the provider list: with
|
|
91
|
+
# PHYSTACK_STUN_URL unset the static fallback is empty (TECH-1521), so
|
|
92
|
+
# the provider-supplied servers stand alone.
|
|
93
|
+
assert len(configuration.iceServers) == 1
|
|
94
|
+
|
|
89
95
|
|
|
96
|
+
async def test_ice_servers_provider_failure_falls_back_to_static(
|
|
97
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
98
|
+
) -> None:
|
|
99
|
+
monkeypatch.delenv("PHYSTACK_STUN_URL", raising=False)
|
|
90
100
|
|
|
91
|
-
async def test_ice_servers_provider_failure_falls_back_to_static() -> None:
|
|
92
101
|
async def failing_provider() -> list[dict[str, Any]]:
|
|
93
102
|
raise RuntimeError("phyhub unreachable")
|
|
94
103
|
|
|
@@ -99,10 +108,12 @@ async def test_ice_servers_provider_failure_falls_back_to_static() -> None:
|
|
|
99
108
|
|
|
100
109
|
configuration = await handler._build_rtc_configuration(use_stun=True)
|
|
101
110
|
|
|
111
|
+
# TECH-1521: the static fallback carries no third-party STUN — with
|
|
112
|
+
# PHYSTACK_STUN_URL unset it is EMPTY (host candidates only), and the
|
|
113
|
+
# configuration must still be explicit (None would invoke aiortc's own
|
|
114
|
+
# Google STUN default).
|
|
102
115
|
assert configuration is not None
|
|
103
|
-
|
|
104
|
-
assert all(url.startswith("stun:") for url in urls)
|
|
105
|
-
assert urls # static STUN fallback present
|
|
116
|
+
assert configuration.iceServers == []
|
|
106
117
|
|
|
107
118
|
|
|
108
119
|
async def test_ice_servers_provider_timeout_falls_back_to_static(
|
|
@@ -121,10 +132,12 @@ async def test_ice_servers_provider_timeout_falls_back_to_static(
|
|
|
121
132
|
|
|
122
133
|
configuration = await handler._build_rtc_configuration(use_stun=True)
|
|
123
134
|
|
|
135
|
+
# TECH-1521: the hung provider's TURN never lands, and the static
|
|
136
|
+
# fallback is empty (no third-party STUN) — explicit, not None.
|
|
124
137
|
assert configuration is not None
|
|
125
138
|
urls = flatten_urls(configuration)
|
|
126
139
|
assert TURN_URL not in urls
|
|
127
|
-
assert
|
|
140
|
+
assert urls == []
|
|
128
141
|
|
|
129
142
|
|
|
130
143
|
# =============================================================================
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Guard tests for TECH-1521: no third-party STUN (Google or otherwise) may
|
|
2
|
+
ever be baked into the client. STUN comes from the phyhub ice_servers_provider
|
|
3
|
+
(our regional coturn) or from PHYSTACK_STUN_URL; with neither configured the
|
|
4
|
+
static fallback is empty (host candidates only).
|
|
5
|
+
|
|
6
|
+
Also pins the aiortc gotcha: RTCPeerConnection given NO configuration (None)
|
|
7
|
+
substitutes aiortc's own default — which is Google STUN — so every
|
|
8
|
+
configuration builder must return an explicit RTCConfiguration, never None.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import importlib
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from aiortc import RTCConfiguration
|
|
16
|
+
|
|
17
|
+
import phystack.hub_client.services.webrtc.media_stream as media_stream_module
|
|
18
|
+
from phystack.hub_client.services.webrtc.data_channel import DataChannelOptions
|
|
19
|
+
from phystack.hub_client.services.webrtc.media_responder import MediaStreamResponder
|
|
20
|
+
from phystack.hub_client.services.webrtc.media_stream import (
|
|
21
|
+
MediaStreamOptions,
|
|
22
|
+
build_ice_servers,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Needles split so this file does not match its own scan.
|
|
26
|
+
GOOGLE_STUN_HOST = "l.google" + ".com"
|
|
27
|
+
GOOGLE_STUN_PORT = ":1930" + "2"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_default_stun_servers_empty_without_env(monkeypatch) -> None:
|
|
31
|
+
monkeypatch.delenv("PHYSTACK_STUN_URL", raising=False)
|
|
32
|
+
assert MediaStreamOptions().stun_servers == []
|
|
33
|
+
assert DataChannelOptions().stun_servers == []
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_default_stun_servers_from_env(monkeypatch) -> None:
|
|
37
|
+
monkeypatch.setenv("PHYSTACK_STUN_URL", "stun:turn-qa.phystack.com:3478")
|
|
38
|
+
assert MediaStreamOptions().stun_servers == ["stun:turn-qa.phystack.com:3478"]
|
|
39
|
+
assert DataChannelOptions().stun_servers == ["stun:turn-qa.phystack.com:3478"]
|
|
40
|
+
for url in MediaStreamOptions().stun_servers:
|
|
41
|
+
assert "google" not in url
|
|
42
|
+
assert GOOGLE_STUN_PORT not in url
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_build_ice_servers_defaults_carry_no_third_party_stun(monkeypatch) -> None:
|
|
46
|
+
monkeypatch.delenv("PHYSTACK_STUN_URL", raising=False)
|
|
47
|
+
monkeypatch.delenv("PHYSTACK_TURN_URL", raising=False)
|
|
48
|
+
servers = build_ice_servers(MediaStreamOptions())
|
|
49
|
+
assert servers == []
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_responder_configuration_is_explicit_even_when_empty(monkeypatch) -> None:
|
|
53
|
+
# aiortc's None-configuration default is Google STUN; the builder must
|
|
54
|
+
# return an explicit (possibly empty) RTCConfiguration instead.
|
|
55
|
+
monkeypatch.delenv("PHYSTACK_STUN_URL", raising=False)
|
|
56
|
+
monkeypatch.delenv("PHYSTACK_TURN_URL", raising=False)
|
|
57
|
+
responder = MediaStreamResponder(
|
|
58
|
+
twin_id="twin-guard-test",
|
|
59
|
+
twin_messaging=None, # type: ignore[arg-type] # start() is never called
|
|
60
|
+
callback=lambda peer: None,
|
|
61
|
+
)
|
|
62
|
+
configuration = asyncio.run(responder._build_configuration())
|
|
63
|
+
assert isinstance(configuration, RTCConfiguration)
|
|
64
|
+
assert configuration.iceServers == []
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_no_google_stun_literal_anywhere_in_source() -> None:
|
|
68
|
+
package_root = Path(importlib.import_module("phystack.hub_client").__file__).parent
|
|
69
|
+
scanned = 0
|
|
70
|
+
for source_path in package_root.rglob("*.py"):
|
|
71
|
+
content = source_path.read_text(encoding="utf-8", errors="ignore")
|
|
72
|
+
assert GOOGLE_STUN_HOST not in content, f"Google STUN host in {source_path}"
|
|
73
|
+
assert GOOGLE_STUN_PORT not in content, f"Google STUN port in {source_path}"
|
|
74
|
+
scanned += 1
|
|
75
|
+
assert scanned > 10 # sanity: the walk really covered the package
|
|
76
|
+
# media_stream must exist and stay the single source of the default.
|
|
77
|
+
assert hasattr(media_stream_module, "_get_default_stun_servers")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/src/phystack/hub_client/__init__.py
RENAMED
|
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
|
{phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/src/phystack/hub_client/twin_messaging.py
RENAMED
|
File without changes
|
{phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/src/phystack/hub_client/twin_registry.py
RENAMED
|
File without changes
|
{phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/src/phystack/hub_client/types/__init__.py
RENAMED
|
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
|
{phystack_hub_client-6.16.0 → phystack_hub_client-6.18.2}/tests/test_peripheral_lifecycle.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|