Skip to content

Commit 74847dc

Browse files
Mike ProsserMike Prosser
authored andcommitted
refactor to use PanelClient
1 parent 426aa54 commit 74847dc

File tree

6 files changed

+103
-54
lines changed

6 files changed

+103
-54
lines changed

src/nipanel/_panel.py

Lines changed: 12 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
from __future__ import annotations
22

33
import sys
4-
import time
54
from abc import ABC, abstractmethod
65
from types import TracebackType
76
from typing import TYPE_CHECKING, Optional, Type
87

9-
from grpc import RpcError
10-
from ni.pythonpanel.v1.python_panel_service_pb2 import ConnectRequest, DisconnectRequest
11-
from ni.pythonpanel.v1.python_panel_service_pb2_grpc import PythonPanelServiceStub
12-
from ni_measurement_plugin_sdk_service.grpc.channelpool import GrpcChannelPool
8+
from ni_measurement_plugin_sdk_service.discovery import DiscoveryClient
9+
10+
from nipanel._panel_client import PanelClient
1311

1412
if TYPE_CHECKING:
1513
if sys.version_info >= (3, 11):
@@ -21,19 +19,15 @@
2119
class Panel(ABC):
2220
"""This class allows you to connect to a panel and specify values for its controls."""
2321

24-
RETRY_WAIT_TIME = 1 # time in seconds to wait before retrying connection
25-
26-
_channel_pool: GrpcChannelPool
27-
_stub: PythonPanelServiceStub | None
22+
_panel_client: PanelClient
2823
_panel_id: str
2924
_panel_uri: str
3025

31-
__slots__ = ["_channel_pool", "_stub", "_panel_id", "_panel_uri", "__weakref__"]
26+
__slots__ = ["_panel_client", "_panel_id", "_panel_uri", "__weakref__"]
3227

3328
def __init__(self, panel_id: str, panel_uri: str) -> None:
3429
"""Initialize the panel."""
35-
self._channel_pool = GrpcChannelPool()
36-
self._stub = None
30+
self._panel_client = PanelClient(self._resolve_service_address)
3731
self._panel_id = panel_id
3832
self._panel_uri = panel_uri
3933

@@ -64,27 +58,11 @@ def __exit__(
6458

6559
def connect(self) -> None:
6660
"""Connect to the panel and open it."""
67-
address = self._resolve_service_address()
68-
channel = self._channel_pool.get_channel(address)
69-
self._stub = PythonPanelServiceStub(channel)
70-
71-
connect_request = ConnectRequest(panel_id=self._panel_id, panel_uri=self._panel_uri)
72-
try:
73-
self._stub.Connect(connect_request)
74-
except RpcError:
75-
# retry the connection if it fails, but only once
76-
time.sleep(self.RETRY_WAIT_TIME)
77-
self._stub.Connect(connect_request)
61+
self._panel_client.connect(self._panel_id, self._panel_uri)
7862

7963
def disconnect(self) -> None:
8064
"""Disconnect from the panel (does not close the panel)."""
81-
if self._stub is None:
82-
raise RuntimeError("connect() must be called before disconnect()")
83-
84-
disconnect_request = DisconnectRequest(panel_id=self._panel_id)
85-
self._stub.Disconnect(disconnect_request)
86-
self._stub = None
87-
self._channel_pool.close()
65+
self._panel_client.disconnect(self._panel_id)
8866

8967
def get_value(self, value_id: str) -> object:
9068
"""Get the value for a control on the panel.
@@ -95,7 +73,7 @@ def get_value(self, value_id: str) -> object:
9573
Returns:
9674
The value
9775
"""
98-
# TODO: AB#3095681 - get the Any from _stub.GetValue and convert it to the correct type
76+
# TODO: AB#3095681 - get the Any from _client.get_value and convert it to the correct type
9977
return "placeholder value"
10078

10179
def set_value(self, value_id: str, value: object) -> None:
@@ -105,10 +83,10 @@ def set_value(self, value_id: str, value: object) -> None:
10583
value_id: The id of the value
10684
value: The value
10785
"""
108-
# TODO: AB#3095681 - Convert the value to an Any and pass it to _stub.SetValue
86+
# TODO: AB#3095681 - Convert the value to an Any and pass it to _client.set_value
10987
pass
11088

11189
@abstractmethod
112-
def _resolve_service_address(self) -> str:
113-
"""Resolve the service location for the panel."""
90+
def _resolve_service_address(self, discovery_client: DiscoveryClient) -> str:
91+
"""Resolve the service address for the panel."""
11492
raise NotImplementedError

src/nipanel/_panel_client.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""Client for accessing the NI Python Panel Service."""
2+
3+
from __future__ import annotations
4+
5+
import logging
6+
import threading
7+
from typing import Any, Callable
8+
9+
import grpc
10+
from ni.pythonpanel.v1.python_panel_service_pb2 import ConnectRequest, DisconnectRequest
11+
from ni.pythonpanel.v1.python_panel_service_pb2_grpc import PythonPanelServiceStub
12+
from ni_measurement_plugin_sdk_service.discovery import DiscoveryClient
13+
from ni_measurement_plugin_sdk_service.grpc.channelpool import GrpcChannelPool
14+
15+
_logger = logging.getLogger(__name__)
16+
17+
18+
class PanelClient:
19+
"""Client for accessing the NI Python Panel Service."""
20+
21+
def __init__(
22+
self,
23+
resolve_service_address_fn: Callable[[DiscoveryClient], str],
24+
*,
25+
discovery_client: DiscoveryClient | None = None,
26+
grpc_channel: grpc.Channel | None = None,
27+
grpc_channel_pool: GrpcChannelPool | None = None,
28+
) -> None:
29+
"""Initialize the panel client.
30+
31+
Args:
32+
resolve_service_address_fn: A function to resolve the service location.
33+
discovery_client: An optional discovery client.
34+
grpc_channel: An optional panel gRPC channel.
35+
grpc_channel_pool: An optional gRPC channel pool.
36+
"""
37+
self._initialization_lock = threading.Lock()
38+
self._resolve_service_address_fn = resolve_service_address_fn
39+
self._discovery_client = discovery_client
40+
self._grpc_channel_pool = grpc_channel_pool
41+
self._stub: PythonPanelServiceStub | None = None
42+
43+
if grpc_channel is not None:
44+
self._stub = PythonPanelServiceStub(grpc_channel)
45+
46+
def _get_stub(self) -> PythonPanelServiceStub:
47+
if self._stub is None:
48+
with self._initialization_lock:
49+
if self._grpc_channel_pool is None:
50+
_logger.debug("Creating unshared GrpcChannelPool.")
51+
self._grpc_channel_pool = GrpcChannelPool()
52+
if self._discovery_client is None:
53+
_logger.debug("Creating unshared DiscoveryClient.")
54+
self._discovery_client = DiscoveryClient(
55+
grpc_channel_pool=self._grpc_channel_pool
56+
)
57+
if self._stub is None:
58+
service_address = self._resolve_service_address_fn(self._discovery_client)
59+
channel = self._grpc_channel_pool.get_channel(service_address)
60+
self._stub = PythonPanelServiceStub(channel)
61+
return self._stub
62+
63+
def _invoke_with_retry(self, method: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
64+
"""Invoke a gRPC method with retry logic."""
65+
try:
66+
return method(*args, **kwargs)
67+
except grpc.RpcError as e:
68+
if e.code() == grpc.StatusCode.UNAVAILABLE or e.code() == grpc.StatusCode.UNKNOWN:
69+
# if the service is unavailable, we can retry the connection
70+
self._stub = None
71+
return method(*args, **kwargs)
72+
73+
def connect(self, panel_id: str, panel_uri: str) -> None:
74+
"""Connect to the panel and open it."""
75+
connect_request = ConnectRequest(panel_id=panel_id, panel_uri=panel_uri)
76+
self._invoke_with_retry(self._get_stub().Connect, connect_request)
77+
78+
def disconnect(self, panel_id: str) -> None:
79+
"""Disconnect from the panel (does not close the panel)."""
80+
disconnect_request = DisconnectRequest(panel_id=panel_id)
81+
self._invoke_with_retry(self._get_stub().Disconnect, disconnect_request)

src/nipanel/_streamlit_panel.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from ni_measurement_plugin_sdk_service.discovery import DiscoveryClient
2-
from ni_measurement_plugin_sdk_service.grpc.channelpool import GrpcChannelPool
32

43
from nipanel._panel import Panel
54

@@ -23,8 +22,8 @@ def __init__(self, panel_id: str, streamlit_script_uri: str) -> None:
2322
"""
2423
super().__init__(panel_id, streamlit_script_uri)
2524

26-
def _resolve_service_address(self) -> str:
27-
with GrpcChannelPool() as grpc_channel_pool:
28-
discovery_client = DiscoveryClient(grpc_channel_pool=grpc_channel_pool)
29-
service_location = discovery_client.resolve_service(self.PYTHON_PANEL_SERVICE)
30-
return service_location.insecure_address
25+
def _resolve_service_address(self, discovery_client: DiscoveryClient) -> str:
26+
service_location = discovery_client.resolve_service(
27+
provided_interface=self.PYTHON_PANEL_SERVICE, service_class=self.PYTHON_PANEL_SERVICE
28+
)
29+
return service_location.insecure_address

tests/unit/test_panel.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import pytest
2-
31
from tests.utils._fake_python_panel_servicer import FakePythonPanelServicer
42
from tests.utils._port_panel import PortPanel
53

@@ -35,16 +33,6 @@ def test___with_panel___set_value___gets_same_value(
3533
assert panel.get_value("test_id") == "placeholder value"
3634

3735

38-
def test___new_panel___disconnect___raises_runtime_error(
39-
fake_python_panel_service: tuple[FakePythonPanelServicer, int],
40-
) -> None:
41-
_, port = fake_python_panel_service
42-
panel = PortPanel(port, "my_panel", "path/to/script")
43-
44-
with pytest.raises(RuntimeError):
45-
panel.disconnect()
46-
47-
4836
def test___first_connect_fails___connect___gets_value(
4937
fake_python_panel_service: tuple[FakePythonPanelServicer, int],
5038
) -> None:

tests/utils/_fake_python_panel_servicer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Any
22

33
import google.protobuf.any_pb2 as any_pb2
4+
import grpc
45
from ni.pythonpanel.v1.python_panel_service_pb2 import (
56
ConnectRequest,
67
ConnectResponse,
@@ -24,7 +25,7 @@ def Connect(self, request: ConnectRequest, context: Any) -> ConnectResponse: #
2425
"""Trivial implementation for testing."""
2526
if self._fail_next_connect:
2627
self._fail_next_connect = False
27-
raise ValueError("Simulate a failure to Connect.")
28+
context.abort(grpc.StatusCode.UNAVAILABLE, "Simulated connection failure")
2829
return ConnectResponse()
2930

3031
def Disconnect( # noqa: N802

tests/utils/_port_panel.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from ni_measurement_plugin_sdk_service.discovery import DiscoveryClient
2+
13
from nipanel._panel import Panel
24

35

@@ -18,5 +20,5 @@ def __init__(self, port: int, panel_id: str, panel_uri: str) -> None:
1820
super().__init__(panel_id, panel_uri)
1921
self.port = port
2022

21-
def _resolve_service_address(self) -> str:
23+
def _resolve_service_address(self, discovery_client: DiscoveryClient) -> str:
2224
return f"localhost:{self.port}"

0 commit comments

Comments
 (0)