From b8e09eb0bf485696c1658b32228b0a3930fe87f5 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Tue, 24 Jun 2025 10:37:39 -0500 Subject: [PATCH 1/4] add set_values() for batch updates --- examples/simple_graph/simple_graph.py | 17 +++- .../pythonpanel/v1/python_panel_service.proto | 27 ++++++ .../v1/python_panel_service_pb2.py | 12 ++- .../v1/python_panel_service_pb2.pyi | 59 +++++++++++++ .../v1/python_panel_service_pb2_grpc.py | 36 ++++++++ .../v1/python_panel_service_pb2_grpc.pyi | 29 +++++++ src/nipanel/_panel_client.py | 20 +++++ src/nipanel/_panel_value_accessor.py | 8 ++ tests/unit/test_panel_client.py | 46 ++++++++++ tests/unit/test_streamlit_panel.py | 85 +++++++++++++++++++ tests/utils/_fake_python_panel_servicer.py | 11 +++ 11 files changed, 343 insertions(+), 7 deletions(-) diff --git a/examples/simple_graph/simple_graph.py b/examples/simple_graph/simple_graph.py index ab79929..3ef18ac 100644 --- a/examples/simple_graph/simple_graph.py +++ b/examples/simple_graph/simple_graph.py @@ -25,10 +25,19 @@ time_points = np.linspace(0, num_points, num_points) sine_values = amplitude * np.sin(frequency * time_points) - panel.set_value("time_points", time_points.tolist()) - panel.set_value("sine_values", sine_values.tolist()) - panel.set_value("amplitude", amplitude) - panel.set_value("frequency", frequency) + panel.set_values( + { + "time_points": time_points.tolist(), + "sine_values": sine_values.tolist(), + "amplitude": amplitude, + "frequency": frequency, + } + ) + + # panel.set_value("time_points", time_points.tolist()) + # panel.set_value("sine_values", sine_values.tolist()) + # panel.set_value("amplitude", amplitude) + # panel.set_value("frequency", frequency) # Slowly vary the frequency for a more dynamic visualization frequency = 1.0 + 0.5 * math.sin(time.time() / 5.0) diff --git a/protos/ni/pythonpanel/v1/python_panel_service.proto b/protos/ni/pythonpanel/v1/python_panel_service.proto index ea2508b..e2463e8 100644 --- a/protos/ni/pythonpanel/v1/python_panel_service.proto +++ b/protos/ni/pythonpanel/v1/python_panel_service.proto @@ -41,6 +41,11 @@ service PythonPanelService { // Status Codes for errors: // - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. rpc SetValue(SetValueRequest) returns (SetValueResponse); + + // Set values for multiple controls on the panel + // Status Codes for errors: + // - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. + rpc SetValues(SetValuesRequest) returns (SetValuesResponse); } message StartPanelRequest { @@ -114,4 +119,26 @@ message SetValueRequest { } message SetValueResponse { +} + +message SetValuesRequest { + // Unique ID of the panel + string panel_id = 1; + + // Value IDs and Values + repeated ValueInformation values = 2; + + // Notify other clients of these new values + bool notify = 3; +} + +message ValueInformation { + // Unique ID of the value + string value_id = 1; + + // The value + google.protobuf.Any value = 2; +} + +message SetValuesResponse { } \ No newline at end of file diff --git a/src/ni/pythonpanel/v1/python_panel_service_pb2.py b/src/ni/pythonpanel/v1/python_panel_service_pb2.py index d75bf54..299ea38 100644 --- a/src/ni/pythonpanel/v1/python_panel_service_pb2.py +++ b/src/ni/pythonpanel/v1/python_panel_service_pb2.py @@ -14,7 +14,7 @@ from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n,ni/pythonpanel/v1/python_panel_service.proto\x12\x11ni.pythonpanel.v1\x1a\x19google/protobuf/any.proto\"@\n\x11StartPanelRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x19\n\x11panel_script_path\x18\x02 \x01(\t\"\'\n\x12StartPanelResponse\x12\x11\n\tpanel_url\x18\x01 \x01(\t\"3\n\x10StopPanelRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\r\n\x05reset\x18\x02 \x01(\x08\"\x13\n\x11StopPanelResponse\"\x18\n\x16\x45numeratePanelsRequest\"J\n\x10PanelInformation\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x11\n\tpanel_url\x18\x02 \x01(\t\x12\x11\n\tvalue_ids\x18\x03 \x03(\t\"N\n\x17\x45numeratePanelsResponse\x12\x33\n\x06panels\x18\x01 \x03(\x0b\x32#.ni.pythonpanel.v1.PanelInformation\"5\n\x0fGetValueRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x10\n\x08value_id\x18\x02 \x01(\t\"7\n\x10GetValueResponse\x12#\n\x05value\x18\x01 \x01(\x0b\x32\x14.google.protobuf.Any\"j\n\x0fSetValueRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x10\n\x08value_id\x18\x02 \x01(\t\x12#\n\x05value\x18\x03 \x01(\x0b\x32\x14.google.protobuf.Any\x12\x0e\n\x06notify\x18\x04 \x01(\x08\"\x12\n\x10SetValueResponse2\xdb\x03\n\x12PythonPanelService\x12Y\n\nStartPanel\x12$.ni.pythonpanel.v1.StartPanelRequest\x1a%.ni.pythonpanel.v1.StartPanelResponse\x12V\n\tStopPanel\x12#.ni.pythonpanel.v1.StopPanelRequest\x1a$.ni.pythonpanel.v1.StopPanelResponse\x12h\n\x0f\x45numeratePanels\x12).ni.pythonpanel.v1.EnumeratePanelsRequest\x1a*.ni.pythonpanel.v1.EnumeratePanelsResponse\x12S\n\x08GetValue\x12\".ni.pythonpanel.v1.GetValueRequest\x1a#.ni.pythonpanel.v1.GetValueResponse\x12S\n\x08SetValue\x12\".ni.pythonpanel.v1.SetValueRequest\x1a#.ni.pythonpanel.v1.SetValueResponseB\x9a\x01\n\x15\x63om.ni.pythonpanel.v1B\x17PythonPanelServiceProtoP\x01Z\rpythonpanelv1\xf8\x01\x01\xa2\x02\x04NIPP\xaa\x02\"NationalInstruments.PythonPanel.V1\xca\x02\x11NI\\PythonPanel\\V1\xea\x02\x13NI::PythonPanel::V1b\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n,ni/pythonpanel/v1/python_panel_service.proto\x12\x11ni.pythonpanel.v1\x1a\x19google/protobuf/any.proto\"@\n\x11StartPanelRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x19\n\x11panel_script_path\x18\x02 \x01(\t\"\'\n\x12StartPanelResponse\x12\x11\n\tpanel_url\x18\x01 \x01(\t\"3\n\x10StopPanelRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\r\n\x05reset\x18\x02 \x01(\x08\"\x13\n\x11StopPanelResponse\"\x18\n\x16\x45numeratePanelsRequest\"J\n\x10PanelInformation\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x11\n\tpanel_url\x18\x02 \x01(\t\x12\x11\n\tvalue_ids\x18\x03 \x03(\t\"N\n\x17\x45numeratePanelsResponse\x12\x33\n\x06panels\x18\x01 \x03(\x0b\x32#.ni.pythonpanel.v1.PanelInformation\"5\n\x0fGetValueRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x10\n\x08value_id\x18\x02 \x01(\t\"7\n\x10GetValueResponse\x12#\n\x05value\x18\x01 \x01(\x0b\x32\x14.google.protobuf.Any\"j\n\x0fSetValueRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x10\n\x08value_id\x18\x02 \x01(\t\x12#\n\x05value\x18\x03 \x01(\x0b\x32\x14.google.protobuf.Any\x12\x0e\n\x06notify\x18\x04 \x01(\x08\"\x12\n\x10SetValueResponse\"i\n\x10SetValuesRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x33\n\x06values\x18\x02 \x03(\x0b\x32#.ni.pythonpanel.v1.ValueInformation\x12\x0e\n\x06notify\x18\x03 \x01(\x08\"I\n\x10ValueInformation\x12\x10\n\x08value_id\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.google.protobuf.Any\"\x13\n\x11SetValuesResponse2\xb3\x04\n\x12PythonPanelService\x12Y\n\nStartPanel\x12$.ni.pythonpanel.v1.StartPanelRequest\x1a%.ni.pythonpanel.v1.StartPanelResponse\x12V\n\tStopPanel\x12#.ni.pythonpanel.v1.StopPanelRequest\x1a$.ni.pythonpanel.v1.StopPanelResponse\x12h\n\x0f\x45numeratePanels\x12).ni.pythonpanel.v1.EnumeratePanelsRequest\x1a*.ni.pythonpanel.v1.EnumeratePanelsResponse\x12S\n\x08GetValue\x12\".ni.pythonpanel.v1.GetValueRequest\x1a#.ni.pythonpanel.v1.GetValueResponse\x12S\n\x08SetValue\x12\".ni.pythonpanel.v1.SetValueRequest\x1a#.ni.pythonpanel.v1.SetValueResponse\x12V\n\tSetValues\x12#.ni.pythonpanel.v1.SetValuesRequest\x1a$.ni.pythonpanel.v1.SetValuesResponseB\x9a\x01\n\x15\x63om.ni.pythonpanel.v1B\x17PythonPanelServiceProtoP\x01Z\rpythonpanelv1\xf8\x01\x01\xa2\x02\x04NIPP\xaa\x02\"NationalInstruments.PythonPanel.V1\xca\x02\x11NI\\PythonPanel\\V1\xea\x02\x13NI::PythonPanel::V1b\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'ni.pythonpanel.v1.python_panel_service_pb2', globals()) @@ -44,6 +44,12 @@ _SETVALUEREQUEST._serialized_end=675 _SETVALUERESPONSE._serialized_start=677 _SETVALUERESPONSE._serialized_end=695 - _PYTHONPANELSERVICE._serialized_start=698 - _PYTHONPANELSERVICE._serialized_end=1173 + _SETVALUESREQUEST._serialized_start=697 + _SETVALUESREQUEST._serialized_end=802 + _VALUEINFORMATION._serialized_start=804 + _VALUEINFORMATION._serialized_end=877 + _SETVALUESRESPONSE._serialized_start=879 + _SETVALUESRESPONSE._serialized_end=898 + _PYTHONPANELSERVICE._serialized_start=901 + _PYTHONPANELSERVICE._serialized_end=1464 # @@protoc_insertion_point(module_scope) diff --git a/src/ni/pythonpanel/v1/python_panel_service_pb2.pyi b/src/ni/pythonpanel/v1/python_panel_service_pb2.pyi index 1fe9b49..9170738 100644 --- a/src/ni/pythonpanel/v1/python_panel_service_pb2.pyi +++ b/src/ni/pythonpanel/v1/python_panel_service_pb2.pyi @@ -212,3 +212,62 @@ class SetValueResponse(google.protobuf.message.Message): ) -> None: ... global___SetValueResponse = SetValueResponse + +@typing.final +class SetValuesRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + PANEL_ID_FIELD_NUMBER: builtins.int + VALUES_FIELD_NUMBER: builtins.int + NOTIFY_FIELD_NUMBER: builtins.int + panel_id: builtins.str + """Unique ID of the panel""" + notify: builtins.bool + """Notify other clients of these new values""" + @property + def values(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ValueInformation]: + """Value IDs and Values""" + + def __init__( + self, + *, + panel_id: builtins.str = ..., + values: collections.abc.Iterable[global___ValueInformation] | None = ..., + notify: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["notify", b"notify", "panel_id", b"panel_id", "values", b"values"]) -> None: ... + +global___SetValuesRequest = SetValuesRequest + +@typing.final +class ValueInformation(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VALUE_ID_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + value_id: builtins.str + """Unique ID of the value""" + @property + def value(self) -> google.protobuf.any_pb2.Any: + """The value""" + + def __init__( + self, + *, + value_id: builtins.str = ..., + value: google.protobuf.any_pb2.Any | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["value", b"value", "value_id", b"value_id"]) -> None: ... + +global___ValueInformation = ValueInformation + +@typing.final +class SetValuesResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___SetValuesResponse = SetValuesResponse diff --git a/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.py b/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.py index 0f5c31b..b6f19cb 100644 --- a/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.py +++ b/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.py @@ -40,6 +40,11 @@ def __init__(self, channel): request_serializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValueRequest.SerializeToString, response_deserializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValueResponse.FromString, ) + self.SetValues = channel.unary_unary( + '/ni.pythonpanel.v1.PythonPanelService/SetValues', + request_serializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValuesRequest.SerializeToString, + response_deserializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValuesResponse.FromString, + ) class PythonPanelServiceServicer(object): @@ -92,6 +97,15 @@ def SetValue(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def SetValues(self, request, context): + """Set values for multiple controls on the panel + Status Codes for errors: + - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def add_PythonPanelServiceServicer_to_server(servicer, server): rpc_method_handlers = { @@ -120,6 +134,11 @@ def add_PythonPanelServiceServicer_to_server(servicer, server): request_deserializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValueRequest.FromString, response_serializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValueResponse.SerializeToString, ), + 'SetValues': grpc.unary_unary_rpc_method_handler( + servicer.SetValues, + request_deserializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValuesRequest.FromString, + response_serializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValuesResponse.SerializeToString, + ), } generic_handler = grpc.method_handlers_generic_handler( 'ni.pythonpanel.v1.PythonPanelService', rpc_method_handlers) @@ -215,3 +234,20 @@ def SetValue(request, ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValueResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def SetValues(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/ni.pythonpanel.v1.PythonPanelService/SetValues', + ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValuesRequest.SerializeToString, + ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValuesResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.pyi b/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.pyi index b2d5fa6..2ca7667 100644 --- a/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.pyi +++ b/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.pyi @@ -67,6 +67,15 @@ class PythonPanelServiceStub: - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. """ + SetValues: grpc.UnaryUnaryMultiCallable[ + ni.pythonpanel.v1.python_panel_service_pb2.SetValuesRequest, + ni.pythonpanel.v1.python_panel_service_pb2.SetValuesResponse, + ] + """Set values for multiple controls on the panel + Status Codes for errors: + - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. + """ + class PythonPanelServiceAsyncStub: """Service interface for interacting with python panels""" @@ -116,6 +125,15 @@ class PythonPanelServiceAsyncStub: - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. """ + SetValues: grpc.aio.UnaryUnaryMultiCallable[ + ni.pythonpanel.v1.python_panel_service_pb2.SetValuesRequest, + ni.pythonpanel.v1.python_panel_service_pb2.SetValuesResponse, + ] + """Set values for multiple controls on the panel + Status Codes for errors: + - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. + """ + class PythonPanelServiceServicer(metaclass=abc.ABCMeta): """Service interface for interacting with python panels""" @@ -175,4 +193,15 @@ class PythonPanelServiceServicer(metaclass=abc.ABCMeta): - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. """ + @abc.abstractmethod + def SetValues( + self, + request: ni.pythonpanel.v1.python_panel_service_pb2.SetValuesRequest, + context: _ServicerContext, + ) -> typing.Union[ni.pythonpanel.v1.python_panel_service_pb2.SetValuesResponse, collections.abc.Awaitable[ni.pythonpanel.v1.python_panel_service_pb2.SetValuesResponse]]: + """Set values for multiple controls on the panel + Status Codes for errors: + - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. + """ + def add_PythonPanelServiceServicer_to_server(servicer: PythonPanelServiceServicer, server: typing.Union[grpc.Server, grpc.aio.Server]) -> None: ... diff --git a/src/nipanel/_panel_client.py b/src/nipanel/_panel_client.py index 911d03f..2be12e7 100644 --- a/src/nipanel/_panel_client.py +++ b/src/nipanel/_panel_client.py @@ -13,6 +13,8 @@ EnumeratePanelsRequest, GetValueRequest, SetValueRequest, + SetValuesRequest, + ValueInformation, ) from ni.pythonpanel.v1.python_panel_service_pb2_grpc import PythonPanelServiceStub from ni_measurement_plugin_sdk_service.discovery import DiscoveryClient @@ -115,6 +117,24 @@ def set_value(self, panel_id: str, value_id: str, value: object, notify: bool) - ) self._invoke_with_retry(self._get_stub().SetValue, set_value_request) + def set_values(self, panel_id: str, values: dict[str, object], notify: bool) -> None: + """Set multiple values for controls in a panel at once. + + Args: + panel_id: The ID of the panel. + values: A dictionary mapping value IDs to their corresponding values. + notify: Whether to notify other clients of the new values. + """ + value_informations = [] + for value_id, value in values.items(): + new_any = to_any(value) + value_informations.append(ValueInformation(value_id=value_id, value=new_any)) + + set_values_request = SetValuesRequest( + panel_id=panel_id, values=value_informations, notify=notify + ) + self._invoke_with_retry(self._get_stub().SetValues, set_values_request) + def get_value(self, panel_id: str, value_id: str) -> object: """Get the value for the control with value_id. diff --git a/src/nipanel/_panel_value_accessor.py b/src/nipanel/_panel_value_accessor.py index c174843..d709ff6 100644 --- a/src/nipanel/_panel_value_accessor.py +++ b/src/nipanel/_panel_value_accessor.py @@ -82,3 +82,11 @@ def set_value(self, value_id: str, value: object) -> None: self._panel_client.set_value( self._panel_id, value_id, value, notify=self._notify_on_set_value ) + + def set_values(self, values: dict[str, object]) -> None: + """Set multiple values for controls on the panel. + + Args: + values: A dictionary mapping value IDs to their corresponding values. + """ + self._panel_client.set_values(self._panel_id, values, notify=self._notify_on_set_value) diff --git a/tests/unit/test_panel_client.py b/tests/unit/test_panel_client.py index 560f6d9..4d6fb56 100644 --- a/tests/unit/test_panel_client.py +++ b/tests/unit/test_panel_client.py @@ -76,6 +76,52 @@ def test___set_value___gets_value(fake_panel_channel: grpc.Channel) -> None: assert client.get_value("panel1", "val1") == "value1" +def test___set_values___enumerate_panels_shows_values( + fake_panel_channel: grpc.Channel, +) -> None: + client = create_panel_client(fake_panel_channel) + + values = {"val1": "value1", "val2": 42, "val3": True} + + client.set_values("panel1", values, notify=False) + + assert client.enumerate_panels() == {"panel1": ("", ["val1", "val2", "val3"])} + + +def test___set_values___gets_values(fake_panel_channel: grpc.Channel) -> None: + client = create_panel_client(fake_panel_channel) + + values = {"val1": "value1", "val2": 42, "val3": True} + + client.set_values("panel1", values, notify=False) + + assert client.get_value("panel1", "val1") == "value1" + assert client.get_value("panel1", "val2") == 42 + assert client.get_value("panel1", "val3") is True + + +def test___set_values_empty_dict___has_no_effect(fake_panel_channel: grpc.Channel) -> None: + client = create_panel_client(fake_panel_channel) + + client.set_values("panel1", {}, notify=False) + + assert client.enumerate_panels() == {"panel1": ("", [])} + + +def test___set_values_then_set_value___both_values_accessible( + fake_panel_channel: grpc.Channel, +) -> None: + client = create_panel_client(fake_panel_channel) + + client.set_values("panel1", {"val1": "batch value", "val2": 42}, notify=False) + client.set_value("panel1", "val3", "individual value", notify=False) + + assert client.get_value("panel1", "val1") == "batch value" + assert client.get_value("panel1", "val2") == 42 + assert client.get_value("panel1", "val3") == "individual value" + assert client.enumerate_panels() == {"panel1": ("", ["val1", "val2", "val3"])} + + def create_panel_client(fake_panel_channel: grpc.Channel) -> PanelClient: return PanelClient( provided_interface="iface", diff --git a/tests/unit/test_streamlit_panel.py b/tests/unit/test_streamlit_panel.py index 0d65778..b32a87e 100644 --- a/tests/unit/test_streamlit_panel.py +++ b/tests/unit/test_streamlit_panel.py @@ -356,3 +356,88 @@ def is_panel_in_memory(panel: StreamlitPanel) -> bool: def is_panel_running(panel: StreamlitPanel) -> bool: return panel._panel_client.enumerate_panels()[panel.panel_id][0] != "" + + +def test___panel___set_values___gets_same_values( + fake_panel_channel: grpc.Channel, +) -> None: + panel = StreamlitPanel("my_panel", "path/to/script", grpc_channel=fake_panel_channel) + + values = { + "string_id": "test_string", + "int_id": 42, + "bool_id": True, + "float_id": 3.14, + } + panel.set_values(values) + + assert panel.get_value("string_id") == "test_string" + assert panel.get_value("int_id") == 42 + assert panel.get_value("bool_id") is True + assert panel.get_value("float_id") == 3.14 + + +def test___panel___panel_set_values___accessor_gets_same_values( + fake_panel_channel: grpc.Channel, +) -> None: + panel = StreamlitPanel("my_panel", "path/to/script", grpc_channel=fake_panel_channel) + accessor = StreamlitPanelValueAccessor("my_panel", grpc_channel=fake_panel_channel) + + values = { + "string_id": "test_string", + "int_id": 42, + "bool_id": True, + } + panel.set_values(values) + + assert accessor.get_value("string_id") == "test_string" + assert accessor.get_value("int_id") == 42 + assert accessor.get_value("bool_id") is True + + +def test___panel___accessor_set_values___panel_gets_same_values( + fake_panel_channel: grpc.Channel, +) -> None: + panel = StreamlitPanel("my_panel", "path/to/script", grpc_channel=fake_panel_channel) + accessor = StreamlitPanelValueAccessor("my_panel", grpc_channel=fake_panel_channel) + + values = { + "string_id": "test_string", + "int_id": 42, + "bool_id": True, + } + accessor.set_values(values) + + assert panel.get_value("string_id") == "test_string" + assert panel.get_value("int_id") == 42 + assert panel.get_value("bool_id") is True + + +def test___panel___set_values_empty_dict___no_changes( + fake_panel_channel: grpc.Channel, +) -> None: + panel = StreamlitPanel("my_panel", "path/to/script", grpc_channel=fake_panel_channel) + + panel.set_value("existing", "value") + + panel.set_values({}) + + assert panel.get_value("existing") == "value" + + +def test___panel___set_values_then_set_value___both_methods_work_together( + fake_panel_channel: grpc.Channel, +) -> None: + panel = StreamlitPanel("my_panel", "path/to/script", grpc_channel=fake_panel_channel) + + panel.set_values( + { + "batch1": "value1", + "batch2": "value2", + } + ) + panel.set_value("individual", "value3") + + assert panel.get_value("batch1") == "value1" + assert panel.get_value("batch2") == "value2" + assert panel.get_value("individual") == "value3" diff --git a/tests/utils/_fake_python_panel_servicer.py b/tests/utils/_fake_python_panel_servicer.py index 3490ec6..7738ccd 100644 --- a/tests/utils/_fake_python_panel_servicer.py +++ b/tests/utils/_fake_python_panel_servicer.py @@ -13,6 +13,8 @@ GetValueResponse, SetValueRequest, SetValueResponse, + SetValuesRequest, + SetValuesResponse, ) from ni.pythonpanel.v1.python_panel_service_pb2_grpc import PythonPanelServiceServicer @@ -72,6 +74,15 @@ def SetValue(self, request: SetValueRequest, context: Any) -> SetValueResponse: self._notification_count += 1 return SetValueResponse() + def SetValues(self, request: SetValuesRequest, context: Any) -> SetValuesResponse: # noqa: N802 + """Set multiple values for controls in a panel at once.""" + self._init_panel(request.panel_id) + for value_info in request.values: + self._panel_value_ids[request.panel_id][value_info.value_id] = value_info.value + if request.notify: + self._notification_count += 1 + return SetValuesResponse() + def fail_next_start_panel(self) -> None: """Set whether the StartPanel method should fail the next time it is called.""" self._fail_next_start_panel = True From 72c30c43c6d9acae8753eac1b683bc89cdf3f90c Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Tue, 24 Jun 2025 11:53:49 -0500 Subject: [PATCH 2/4] limit gRPC call rate to avoid overwhelming the server --- src/nipanel/_panel_client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/nipanel/_panel_client.py b/src/nipanel/_panel_client.py index 2be12e7..97122dd 100644 --- a/src/nipanel/_panel_client.py +++ b/src/nipanel/_panel_client.py @@ -4,6 +4,7 @@ import logging import threading +import time from typing import Callable, TypeVar import grpc @@ -178,7 +179,9 @@ def _invoke_with_retry( ) -> _T: """Invoke a gRPC method with retry logic.""" try: - return method(*args, **kwargs) + retval = method(*args, **kwargs) + time.sleep(0.001) # limit gRPC call rate to avoid overwhelming the server + return retval except grpc.RpcError as e: if e.code() == grpc.StatusCode.UNAVAILABLE or e.code() == grpc.StatusCode.UNKNOWN: # if the service is unavailable, we can retry the connection From 478c9979a3a6ca02b010aa441740307bdcfebdc6 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Tue, 24 Jun 2025 14:34:25 -0500 Subject: [PATCH 3/4] we don't need set_values() after all --- CONTRIBUTING.md | 2 +- examples/hello/hello.py | 6 +- examples/simple_graph/simple_graph.py | 17 +--- .../pythonpanel/v1/python_panel_service.proto | 27 ------ .../v1/python_panel_service_pb2.py | 12 +-- .../v1/python_panel_service_pb2.pyi | 59 ------------- .../v1/python_panel_service_pb2_grpc.py | 36 -------- .../v1/python_panel_service_pb2_grpc.pyi | 29 ------- src/nipanel/_panel_client.py | 20 ----- src/nipanel/_panel_value_accessor.py | 8 -- tests/unit/test_panel_client.py | 46 ---------- tests/unit/test_streamlit_panel.py | 85 ------------------- tests/utils/_fake_python_panel_servicer.py | 11 --- 13 files changed, 13 insertions(+), 345 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5b45770..9a27a4d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -61,7 +61,7 @@ start docs\_build\index.html 3. Open http://localhost:42001/panel-service/panels/hello_panel/ in your browser 4. If there is an error about missing imports (especially nipanel), execute this command (from the nipanel-python directory) to install the dependencies into the venv: - `%localappdata%\Temp\python_panel_service_venv\Scripts\python.exe -m pip install .\[examples,dev]`, + `%localappdata%\Temp\python_panel_service_venv\Scripts\python.exe -m pip install . streamlit-echarts>=0.4.0`, then restart the PythonPanelService and re-run hello.py. You can see all running panels (and stop them) at: http://localhost:42001/panel-service/ diff --git a/examples/hello/hello.py b/examples/hello/hello.py index ad2d97a..a8b8cd6 100644 --- a/examples/hello/hello.py +++ b/examples/hello/hello.py @@ -7,6 +7,10 @@ panel_script_path = Path(__file__).with_name("hello_panel.py") panel = nipanel.create_panel(panel_script_path) -panel.set_value("hello_string", "Hello, World!") +index = 0 +while True: + panel.set_value("hello_string", f"Hello, World! {index}") + index += 1 + print(f"...{index}") print(f"Panel URL: {panel.panel_url}") diff --git a/examples/simple_graph/simple_graph.py b/examples/simple_graph/simple_graph.py index 3ef18ac..ab79929 100644 --- a/examples/simple_graph/simple_graph.py +++ b/examples/simple_graph/simple_graph.py @@ -25,19 +25,10 @@ time_points = np.linspace(0, num_points, num_points) sine_values = amplitude * np.sin(frequency * time_points) - panel.set_values( - { - "time_points": time_points.tolist(), - "sine_values": sine_values.tolist(), - "amplitude": amplitude, - "frequency": frequency, - } - ) - - # panel.set_value("time_points", time_points.tolist()) - # panel.set_value("sine_values", sine_values.tolist()) - # panel.set_value("amplitude", amplitude) - # panel.set_value("frequency", frequency) + panel.set_value("time_points", time_points.tolist()) + panel.set_value("sine_values", sine_values.tolist()) + panel.set_value("amplitude", amplitude) + panel.set_value("frequency", frequency) # Slowly vary the frequency for a more dynamic visualization frequency = 1.0 + 0.5 * math.sin(time.time() / 5.0) diff --git a/protos/ni/pythonpanel/v1/python_panel_service.proto b/protos/ni/pythonpanel/v1/python_panel_service.proto index e2463e8..ea2508b 100644 --- a/protos/ni/pythonpanel/v1/python_panel_service.proto +++ b/protos/ni/pythonpanel/v1/python_panel_service.proto @@ -41,11 +41,6 @@ service PythonPanelService { // Status Codes for errors: // - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. rpc SetValue(SetValueRequest) returns (SetValueResponse); - - // Set values for multiple controls on the panel - // Status Codes for errors: - // - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. - rpc SetValues(SetValuesRequest) returns (SetValuesResponse); } message StartPanelRequest { @@ -119,26 +114,4 @@ message SetValueRequest { } message SetValueResponse { -} - -message SetValuesRequest { - // Unique ID of the panel - string panel_id = 1; - - // Value IDs and Values - repeated ValueInformation values = 2; - - // Notify other clients of these new values - bool notify = 3; -} - -message ValueInformation { - // Unique ID of the value - string value_id = 1; - - // The value - google.protobuf.Any value = 2; -} - -message SetValuesResponse { } \ No newline at end of file diff --git a/src/ni/pythonpanel/v1/python_panel_service_pb2.py b/src/ni/pythonpanel/v1/python_panel_service_pb2.py index 299ea38..d75bf54 100644 --- a/src/ni/pythonpanel/v1/python_panel_service_pb2.py +++ b/src/ni/pythonpanel/v1/python_panel_service_pb2.py @@ -14,7 +14,7 @@ from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n,ni/pythonpanel/v1/python_panel_service.proto\x12\x11ni.pythonpanel.v1\x1a\x19google/protobuf/any.proto\"@\n\x11StartPanelRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x19\n\x11panel_script_path\x18\x02 \x01(\t\"\'\n\x12StartPanelResponse\x12\x11\n\tpanel_url\x18\x01 \x01(\t\"3\n\x10StopPanelRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\r\n\x05reset\x18\x02 \x01(\x08\"\x13\n\x11StopPanelResponse\"\x18\n\x16\x45numeratePanelsRequest\"J\n\x10PanelInformation\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x11\n\tpanel_url\x18\x02 \x01(\t\x12\x11\n\tvalue_ids\x18\x03 \x03(\t\"N\n\x17\x45numeratePanelsResponse\x12\x33\n\x06panels\x18\x01 \x03(\x0b\x32#.ni.pythonpanel.v1.PanelInformation\"5\n\x0fGetValueRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x10\n\x08value_id\x18\x02 \x01(\t\"7\n\x10GetValueResponse\x12#\n\x05value\x18\x01 \x01(\x0b\x32\x14.google.protobuf.Any\"j\n\x0fSetValueRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x10\n\x08value_id\x18\x02 \x01(\t\x12#\n\x05value\x18\x03 \x01(\x0b\x32\x14.google.protobuf.Any\x12\x0e\n\x06notify\x18\x04 \x01(\x08\"\x12\n\x10SetValueResponse\"i\n\x10SetValuesRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x33\n\x06values\x18\x02 \x03(\x0b\x32#.ni.pythonpanel.v1.ValueInformation\x12\x0e\n\x06notify\x18\x03 \x01(\x08\"I\n\x10ValueInformation\x12\x10\n\x08value_id\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.google.protobuf.Any\"\x13\n\x11SetValuesResponse2\xb3\x04\n\x12PythonPanelService\x12Y\n\nStartPanel\x12$.ni.pythonpanel.v1.StartPanelRequest\x1a%.ni.pythonpanel.v1.StartPanelResponse\x12V\n\tStopPanel\x12#.ni.pythonpanel.v1.StopPanelRequest\x1a$.ni.pythonpanel.v1.StopPanelResponse\x12h\n\x0f\x45numeratePanels\x12).ni.pythonpanel.v1.EnumeratePanelsRequest\x1a*.ni.pythonpanel.v1.EnumeratePanelsResponse\x12S\n\x08GetValue\x12\".ni.pythonpanel.v1.GetValueRequest\x1a#.ni.pythonpanel.v1.GetValueResponse\x12S\n\x08SetValue\x12\".ni.pythonpanel.v1.SetValueRequest\x1a#.ni.pythonpanel.v1.SetValueResponse\x12V\n\tSetValues\x12#.ni.pythonpanel.v1.SetValuesRequest\x1a$.ni.pythonpanel.v1.SetValuesResponseB\x9a\x01\n\x15\x63om.ni.pythonpanel.v1B\x17PythonPanelServiceProtoP\x01Z\rpythonpanelv1\xf8\x01\x01\xa2\x02\x04NIPP\xaa\x02\"NationalInstruments.PythonPanel.V1\xca\x02\x11NI\\PythonPanel\\V1\xea\x02\x13NI::PythonPanel::V1b\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n,ni/pythonpanel/v1/python_panel_service.proto\x12\x11ni.pythonpanel.v1\x1a\x19google/protobuf/any.proto\"@\n\x11StartPanelRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x19\n\x11panel_script_path\x18\x02 \x01(\t\"\'\n\x12StartPanelResponse\x12\x11\n\tpanel_url\x18\x01 \x01(\t\"3\n\x10StopPanelRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\r\n\x05reset\x18\x02 \x01(\x08\"\x13\n\x11StopPanelResponse\"\x18\n\x16\x45numeratePanelsRequest\"J\n\x10PanelInformation\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x11\n\tpanel_url\x18\x02 \x01(\t\x12\x11\n\tvalue_ids\x18\x03 \x03(\t\"N\n\x17\x45numeratePanelsResponse\x12\x33\n\x06panels\x18\x01 \x03(\x0b\x32#.ni.pythonpanel.v1.PanelInformation\"5\n\x0fGetValueRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x10\n\x08value_id\x18\x02 \x01(\t\"7\n\x10GetValueResponse\x12#\n\x05value\x18\x01 \x01(\x0b\x32\x14.google.protobuf.Any\"j\n\x0fSetValueRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x10\n\x08value_id\x18\x02 \x01(\t\x12#\n\x05value\x18\x03 \x01(\x0b\x32\x14.google.protobuf.Any\x12\x0e\n\x06notify\x18\x04 \x01(\x08\"\x12\n\x10SetValueResponse2\xdb\x03\n\x12PythonPanelService\x12Y\n\nStartPanel\x12$.ni.pythonpanel.v1.StartPanelRequest\x1a%.ni.pythonpanel.v1.StartPanelResponse\x12V\n\tStopPanel\x12#.ni.pythonpanel.v1.StopPanelRequest\x1a$.ni.pythonpanel.v1.StopPanelResponse\x12h\n\x0f\x45numeratePanels\x12).ni.pythonpanel.v1.EnumeratePanelsRequest\x1a*.ni.pythonpanel.v1.EnumeratePanelsResponse\x12S\n\x08GetValue\x12\".ni.pythonpanel.v1.GetValueRequest\x1a#.ni.pythonpanel.v1.GetValueResponse\x12S\n\x08SetValue\x12\".ni.pythonpanel.v1.SetValueRequest\x1a#.ni.pythonpanel.v1.SetValueResponseB\x9a\x01\n\x15\x63om.ni.pythonpanel.v1B\x17PythonPanelServiceProtoP\x01Z\rpythonpanelv1\xf8\x01\x01\xa2\x02\x04NIPP\xaa\x02\"NationalInstruments.PythonPanel.V1\xca\x02\x11NI\\PythonPanel\\V1\xea\x02\x13NI::PythonPanel::V1b\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'ni.pythonpanel.v1.python_panel_service_pb2', globals()) @@ -44,12 +44,6 @@ _SETVALUEREQUEST._serialized_end=675 _SETVALUERESPONSE._serialized_start=677 _SETVALUERESPONSE._serialized_end=695 - _SETVALUESREQUEST._serialized_start=697 - _SETVALUESREQUEST._serialized_end=802 - _VALUEINFORMATION._serialized_start=804 - _VALUEINFORMATION._serialized_end=877 - _SETVALUESRESPONSE._serialized_start=879 - _SETVALUESRESPONSE._serialized_end=898 - _PYTHONPANELSERVICE._serialized_start=901 - _PYTHONPANELSERVICE._serialized_end=1464 + _PYTHONPANELSERVICE._serialized_start=698 + _PYTHONPANELSERVICE._serialized_end=1173 # @@protoc_insertion_point(module_scope) diff --git a/src/ni/pythonpanel/v1/python_panel_service_pb2.pyi b/src/ni/pythonpanel/v1/python_panel_service_pb2.pyi index 9170738..1fe9b49 100644 --- a/src/ni/pythonpanel/v1/python_panel_service_pb2.pyi +++ b/src/ni/pythonpanel/v1/python_panel_service_pb2.pyi @@ -212,62 +212,3 @@ class SetValueResponse(google.protobuf.message.Message): ) -> None: ... global___SetValueResponse = SetValueResponse - -@typing.final -class SetValuesRequest(google.protobuf.message.Message): - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - PANEL_ID_FIELD_NUMBER: builtins.int - VALUES_FIELD_NUMBER: builtins.int - NOTIFY_FIELD_NUMBER: builtins.int - panel_id: builtins.str - """Unique ID of the panel""" - notify: builtins.bool - """Notify other clients of these new values""" - @property - def values(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ValueInformation]: - """Value IDs and Values""" - - def __init__( - self, - *, - panel_id: builtins.str = ..., - values: collections.abc.Iterable[global___ValueInformation] | None = ..., - notify: builtins.bool = ..., - ) -> None: ... - def ClearField(self, field_name: typing.Literal["notify", b"notify", "panel_id", b"panel_id", "values", b"values"]) -> None: ... - -global___SetValuesRequest = SetValuesRequest - -@typing.final -class ValueInformation(google.protobuf.message.Message): - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - VALUE_ID_FIELD_NUMBER: builtins.int - VALUE_FIELD_NUMBER: builtins.int - value_id: builtins.str - """Unique ID of the value""" - @property - def value(self) -> google.protobuf.any_pb2.Any: - """The value""" - - def __init__( - self, - *, - value_id: builtins.str = ..., - value: google.protobuf.any_pb2.Any | None = ..., - ) -> None: ... - def HasField(self, field_name: typing.Literal["value", b"value"]) -> builtins.bool: ... - def ClearField(self, field_name: typing.Literal["value", b"value", "value_id", b"value_id"]) -> None: ... - -global___ValueInformation = ValueInformation - -@typing.final -class SetValuesResponse(google.protobuf.message.Message): - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - def __init__( - self, - ) -> None: ... - -global___SetValuesResponse = SetValuesResponse diff --git a/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.py b/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.py index b6f19cb..0f5c31b 100644 --- a/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.py +++ b/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.py @@ -40,11 +40,6 @@ def __init__(self, channel): request_serializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValueRequest.SerializeToString, response_deserializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValueResponse.FromString, ) - self.SetValues = channel.unary_unary( - '/ni.pythonpanel.v1.PythonPanelService/SetValues', - request_serializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValuesRequest.SerializeToString, - response_deserializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValuesResponse.FromString, - ) class PythonPanelServiceServicer(object): @@ -97,15 +92,6 @@ def SetValue(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') - def SetValues(self, request, context): - """Set values for multiple controls on the panel - Status Codes for errors: - - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - def add_PythonPanelServiceServicer_to_server(servicer, server): rpc_method_handlers = { @@ -134,11 +120,6 @@ def add_PythonPanelServiceServicer_to_server(servicer, server): request_deserializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValueRequest.FromString, response_serializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValueResponse.SerializeToString, ), - 'SetValues': grpc.unary_unary_rpc_method_handler( - servicer.SetValues, - request_deserializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValuesRequest.FromString, - response_serializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValuesResponse.SerializeToString, - ), } generic_handler = grpc.method_handlers_generic_handler( 'ni.pythonpanel.v1.PythonPanelService', rpc_method_handlers) @@ -234,20 +215,3 @@ def SetValue(request, ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValueResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) - - @staticmethod - def SetValues(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary(request, target, '/ni.pythonpanel.v1.PythonPanelService/SetValues', - ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValuesRequest.SerializeToString, - ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.SetValuesResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.pyi b/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.pyi index 2ca7667..b2d5fa6 100644 --- a/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.pyi +++ b/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.pyi @@ -67,15 +67,6 @@ class PythonPanelServiceStub: - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. """ - SetValues: grpc.UnaryUnaryMultiCallable[ - ni.pythonpanel.v1.python_panel_service_pb2.SetValuesRequest, - ni.pythonpanel.v1.python_panel_service_pb2.SetValuesResponse, - ] - """Set values for multiple controls on the panel - Status Codes for errors: - - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. - """ - class PythonPanelServiceAsyncStub: """Service interface for interacting with python panels""" @@ -125,15 +116,6 @@ class PythonPanelServiceAsyncStub: - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. """ - SetValues: grpc.aio.UnaryUnaryMultiCallable[ - ni.pythonpanel.v1.python_panel_service_pb2.SetValuesRequest, - ni.pythonpanel.v1.python_panel_service_pb2.SetValuesResponse, - ] - """Set values for multiple controls on the panel - Status Codes for errors: - - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. - """ - class PythonPanelServiceServicer(metaclass=abc.ABCMeta): """Service interface for interacting with python panels""" @@ -193,15 +175,4 @@ class PythonPanelServiceServicer(metaclass=abc.ABCMeta): - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. """ - @abc.abstractmethod - def SetValues( - self, - request: ni.pythonpanel.v1.python_panel_service_pb2.SetValuesRequest, - context: _ServicerContext, - ) -> typing.Union[ni.pythonpanel.v1.python_panel_service_pb2.SetValuesResponse, collections.abc.Awaitable[ni.pythonpanel.v1.python_panel_service_pb2.SetValuesResponse]]: - """Set values for multiple controls on the panel - Status Codes for errors: - - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. - """ - def add_PythonPanelServiceServicer_to_server(servicer: PythonPanelServiceServicer, server: typing.Union[grpc.Server, grpc.aio.Server]) -> None: ... diff --git a/src/nipanel/_panel_client.py b/src/nipanel/_panel_client.py index 97122dd..b18dc8c 100644 --- a/src/nipanel/_panel_client.py +++ b/src/nipanel/_panel_client.py @@ -14,8 +14,6 @@ EnumeratePanelsRequest, GetValueRequest, SetValueRequest, - SetValuesRequest, - ValueInformation, ) from ni.pythonpanel.v1.python_panel_service_pb2_grpc import PythonPanelServiceStub from ni_measurement_plugin_sdk_service.discovery import DiscoveryClient @@ -118,24 +116,6 @@ def set_value(self, panel_id: str, value_id: str, value: object, notify: bool) - ) self._invoke_with_retry(self._get_stub().SetValue, set_value_request) - def set_values(self, panel_id: str, values: dict[str, object], notify: bool) -> None: - """Set multiple values for controls in a panel at once. - - Args: - panel_id: The ID of the panel. - values: A dictionary mapping value IDs to their corresponding values. - notify: Whether to notify other clients of the new values. - """ - value_informations = [] - for value_id, value in values.items(): - new_any = to_any(value) - value_informations.append(ValueInformation(value_id=value_id, value=new_any)) - - set_values_request = SetValuesRequest( - panel_id=panel_id, values=value_informations, notify=notify - ) - self._invoke_with_retry(self._get_stub().SetValues, set_values_request) - def get_value(self, panel_id: str, value_id: str) -> object: """Get the value for the control with value_id. diff --git a/src/nipanel/_panel_value_accessor.py b/src/nipanel/_panel_value_accessor.py index d709ff6..c174843 100644 --- a/src/nipanel/_panel_value_accessor.py +++ b/src/nipanel/_panel_value_accessor.py @@ -82,11 +82,3 @@ def set_value(self, value_id: str, value: object) -> None: self._panel_client.set_value( self._panel_id, value_id, value, notify=self._notify_on_set_value ) - - def set_values(self, values: dict[str, object]) -> None: - """Set multiple values for controls on the panel. - - Args: - values: A dictionary mapping value IDs to their corresponding values. - """ - self._panel_client.set_values(self._panel_id, values, notify=self._notify_on_set_value) diff --git a/tests/unit/test_panel_client.py b/tests/unit/test_panel_client.py index 4d6fb56..560f6d9 100644 --- a/tests/unit/test_panel_client.py +++ b/tests/unit/test_panel_client.py @@ -76,52 +76,6 @@ def test___set_value___gets_value(fake_panel_channel: grpc.Channel) -> None: assert client.get_value("panel1", "val1") == "value1" -def test___set_values___enumerate_panels_shows_values( - fake_panel_channel: grpc.Channel, -) -> None: - client = create_panel_client(fake_panel_channel) - - values = {"val1": "value1", "val2": 42, "val3": True} - - client.set_values("panel1", values, notify=False) - - assert client.enumerate_panels() == {"panel1": ("", ["val1", "val2", "val3"])} - - -def test___set_values___gets_values(fake_panel_channel: grpc.Channel) -> None: - client = create_panel_client(fake_panel_channel) - - values = {"val1": "value1", "val2": 42, "val3": True} - - client.set_values("panel1", values, notify=False) - - assert client.get_value("panel1", "val1") == "value1" - assert client.get_value("panel1", "val2") == 42 - assert client.get_value("panel1", "val3") is True - - -def test___set_values_empty_dict___has_no_effect(fake_panel_channel: grpc.Channel) -> None: - client = create_panel_client(fake_panel_channel) - - client.set_values("panel1", {}, notify=False) - - assert client.enumerate_panels() == {"panel1": ("", [])} - - -def test___set_values_then_set_value___both_values_accessible( - fake_panel_channel: grpc.Channel, -) -> None: - client = create_panel_client(fake_panel_channel) - - client.set_values("panel1", {"val1": "batch value", "val2": 42}, notify=False) - client.set_value("panel1", "val3", "individual value", notify=False) - - assert client.get_value("panel1", "val1") == "batch value" - assert client.get_value("panel1", "val2") == 42 - assert client.get_value("panel1", "val3") == "individual value" - assert client.enumerate_panels() == {"panel1": ("", ["val1", "val2", "val3"])} - - def create_panel_client(fake_panel_channel: grpc.Channel) -> PanelClient: return PanelClient( provided_interface="iface", diff --git a/tests/unit/test_streamlit_panel.py b/tests/unit/test_streamlit_panel.py index b32a87e..0d65778 100644 --- a/tests/unit/test_streamlit_panel.py +++ b/tests/unit/test_streamlit_panel.py @@ -356,88 +356,3 @@ def is_panel_in_memory(panel: StreamlitPanel) -> bool: def is_panel_running(panel: StreamlitPanel) -> bool: return panel._panel_client.enumerate_panels()[panel.panel_id][0] != "" - - -def test___panel___set_values___gets_same_values( - fake_panel_channel: grpc.Channel, -) -> None: - panel = StreamlitPanel("my_panel", "path/to/script", grpc_channel=fake_panel_channel) - - values = { - "string_id": "test_string", - "int_id": 42, - "bool_id": True, - "float_id": 3.14, - } - panel.set_values(values) - - assert panel.get_value("string_id") == "test_string" - assert panel.get_value("int_id") == 42 - assert panel.get_value("bool_id") is True - assert panel.get_value("float_id") == 3.14 - - -def test___panel___panel_set_values___accessor_gets_same_values( - fake_panel_channel: grpc.Channel, -) -> None: - panel = StreamlitPanel("my_panel", "path/to/script", grpc_channel=fake_panel_channel) - accessor = StreamlitPanelValueAccessor("my_panel", grpc_channel=fake_panel_channel) - - values = { - "string_id": "test_string", - "int_id": 42, - "bool_id": True, - } - panel.set_values(values) - - assert accessor.get_value("string_id") == "test_string" - assert accessor.get_value("int_id") == 42 - assert accessor.get_value("bool_id") is True - - -def test___panel___accessor_set_values___panel_gets_same_values( - fake_panel_channel: grpc.Channel, -) -> None: - panel = StreamlitPanel("my_panel", "path/to/script", grpc_channel=fake_panel_channel) - accessor = StreamlitPanelValueAccessor("my_panel", grpc_channel=fake_panel_channel) - - values = { - "string_id": "test_string", - "int_id": 42, - "bool_id": True, - } - accessor.set_values(values) - - assert panel.get_value("string_id") == "test_string" - assert panel.get_value("int_id") == 42 - assert panel.get_value("bool_id") is True - - -def test___panel___set_values_empty_dict___no_changes( - fake_panel_channel: grpc.Channel, -) -> None: - panel = StreamlitPanel("my_panel", "path/to/script", grpc_channel=fake_panel_channel) - - panel.set_value("existing", "value") - - panel.set_values({}) - - assert panel.get_value("existing") == "value" - - -def test___panel___set_values_then_set_value___both_methods_work_together( - fake_panel_channel: grpc.Channel, -) -> None: - panel = StreamlitPanel("my_panel", "path/to/script", grpc_channel=fake_panel_channel) - - panel.set_values( - { - "batch1": "value1", - "batch2": "value2", - } - ) - panel.set_value("individual", "value3") - - assert panel.get_value("batch1") == "value1" - assert panel.get_value("batch2") == "value2" - assert panel.get_value("individual") == "value3" diff --git a/tests/utils/_fake_python_panel_servicer.py b/tests/utils/_fake_python_panel_servicer.py index 7738ccd..3490ec6 100644 --- a/tests/utils/_fake_python_panel_servicer.py +++ b/tests/utils/_fake_python_panel_servicer.py @@ -13,8 +13,6 @@ GetValueResponse, SetValueRequest, SetValueResponse, - SetValuesRequest, - SetValuesResponse, ) from ni.pythonpanel.v1.python_panel_service_pb2_grpc import PythonPanelServiceServicer @@ -74,15 +72,6 @@ def SetValue(self, request: SetValueRequest, context: Any) -> SetValueResponse: self._notification_count += 1 return SetValueResponse() - def SetValues(self, request: SetValuesRequest, context: Any) -> SetValuesResponse: # noqa: N802 - """Set multiple values for controls in a panel at once.""" - self._init_panel(request.panel_id) - for value_info in request.values: - self._panel_value_ids[request.panel_id][value_info.value_id] = value_info.value - if request.notify: - self._notification_count += 1 - return SetValuesResponse() - def fail_next_start_panel(self) -> None: """Set whether the StartPanel method should fail the next time it is called.""" self._fail_next_start_panel = True From 4bf7b768bd407604ff00f2cfe9f2e671972392b5 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Wed, 25 Jun 2025 08:59:38 -0500 Subject: [PATCH 4/4] don't rate limit on the client side --- src/nipanel/_panel_client.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/nipanel/_panel_client.py b/src/nipanel/_panel_client.py index b18dc8c..911d03f 100644 --- a/src/nipanel/_panel_client.py +++ b/src/nipanel/_panel_client.py @@ -4,7 +4,6 @@ import logging import threading -import time from typing import Callable, TypeVar import grpc @@ -159,9 +158,7 @@ def _invoke_with_retry( ) -> _T: """Invoke a gRPC method with retry logic.""" try: - retval = method(*args, **kwargs) - time.sleep(0.001) # limit gRPC call rate to avoid overwhelming the server - return retval + return method(*args, **kwargs) except grpc.RpcError as e: if e.code() == grpc.StatusCode.UNAVAILABLE or e.code() == grpc.StatusCode.UNKNOWN: # if the service is unavailable, we can retry the connection