diff --git a/examples/performance_checker/README.md b/examples/performance_checker/README.md new file mode 100644 index 0000000..d59a804 --- /dev/null +++ b/examples/performance_checker/README.md @@ -0,0 +1,18 @@ +# Performance checker Example + +This example measures the performance of a stremlit panel with a graph. + +## Features + +- Generates sine wave data with varying frequency +- Displays the data in a graph +- Updates rapidly +- Shows timing information + +### Required Software + +- Python 3.9 or later + +### Usage + +Run `poetry run python examples/performance_checker/performance_checker.py` diff --git a/examples/performance_checker/performance_checker.py b/examples/performance_checker/performance_checker.py new file mode 100644 index 0000000..3bbc885 --- /dev/null +++ b/examples/performance_checker/performance_checker.py @@ -0,0 +1,78 @@ +"""Check the performance of get_value and set_value methods in nipanel.""" + +import math +import time +import timeit +from pathlib import Path + +import numpy as np + +import nipanel + + +panel_script_path = Path(__file__).with_name("performance_checker_panel.py") +panel = nipanel.create_panel(panel_script_path) + +amplitude = 1.0 +frequency = 1.0 +num_points = 1000 +time_points = np.linspace(0, num_points, num_points) +sine_values = amplitude * np.sin(frequency * time_points) + + +def _set_time_points() -> None: + panel.set_value("time_points", time_points.tolist()) + + +def _set_amplitude() -> None: + panel.set_value("amplitude", amplitude) + + +def _get_time_points() -> None: + panel.get_value("time_points", [0.0]) + + +def _get_amplitude() -> None: + panel.get_value("amplitude", 1.0) + + +def _get_unset_value() -> None: + panel.get_value("unset_value", 1.0) + + +iterations = 100 + +set_time_points_time = timeit.timeit(_set_time_points, number=iterations) * 1000 / iterations +print(f"Average time to set 'time_points': {set_time_points_time:.2f} ms") + +set_amplitude_time = timeit.timeit(_set_amplitude, number=iterations) * 1000 / iterations +print(f"Average time to set 'amplitude': {set_amplitude_time:.2f} ms") + +get_time_points_time = timeit.timeit(_get_time_points, number=iterations) * 1000 / iterations +print(f"Average time to get 'time_points': {get_time_points_time:.2f} ms") + +get_amplitude_time = timeit.timeit(_get_amplitude, number=iterations) * 1000 / iterations +print(f"Average time to get 'amplitude': {get_amplitude_time:.2f} ms") + +get_unset_value_time = timeit.timeit(_get_unset_value, number=iterations) * 1000 / iterations +print(f"Average time to get 'unset_value': {get_unset_value_time:.2f} ms") + +try: + print(f"Panel URL: {panel.panel_url}") + print("Press Ctrl+C to exit") + + # Generate and update the sine wave data as fast as possible + while True: + 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) + + # Slowly vary the frequency for a more dynamic visualization + frequency = 1.0 + 0.5 * math.sin(time.time() / 5.0) + +except KeyboardInterrupt: + print("Exiting...") diff --git a/examples/performance_checker/performance_checker_panel.py b/examples/performance_checker/performance_checker_panel.py new file mode 100644 index 0000000..e29f58c --- /dev/null +++ b/examples/performance_checker/performance_checker_panel.py @@ -0,0 +1,125 @@ +"""A Streamlit visualization panel for the performance_checker.py example script.""" + +import statistics +import time +import timeit +from functools import partial +from typing import Any, Tuple + +import streamlit as st +from streamlit_echarts import st_echarts + +import nipanel + + +def profile_get_value( + panel: "nipanel.StreamlitPanelValueAccessor", + value_id: str, + default_value: Any = None, + num_runs: int = 5, +) -> Tuple[Any, float]: + """Measure the time it takes to get a value from the panel. + + Args: + panel: The panel accessor object + value_id: The ID of the value to get + default_value: Default value if the value is not found + num_runs: Number of runs for timing + + Returns: + A tuple of (value, time_ms) where time_ms is the time in milliseconds + """ + value = panel.get_value(value_id, default_value) + get_value_func = partial(panel.get_value, value_id, default_value) + time_ms = timeit.timeit(get_value_func, number=num_runs) * 1000 / num_runs + return value, time_ms + + +st.set_page_config(page_title="Performance Checker Example", page_icon="📈", layout="wide") +st.title("Performance Checker Example") + +if "refresh_history" not in st.session_state: + st.session_state.refresh_history = [] # List of tuples (timestamp, refresh_time_ms) + +# Store current timestamp and calculate time since last refresh +current_time = time.time() +if "last_refresh_time" not in st.session_state: + st.session_state.last_refresh_time = current_time + time_since_last_refresh = 0.0 +else: + time_since_last_refresh = (current_time - st.session_state.last_refresh_time) * 1000 + st.session_state.last_refresh_time = current_time + + # Store refresh times with timestamps, keeping only the last 1 second of data + st.session_state.refresh_history.append((current_time, time_since_last_refresh)) + + # Remove entries older than 1 second + cutoff_time = current_time - 1.0 # 1 second ago + st.session_state.refresh_history = [ + item for item in st.session_state.refresh_history if item[0] >= cutoff_time + ] + +# Extract just the refresh times for calculations +if st.session_state.refresh_history: + refresh_history = [item[1] for item in st.session_state.refresh_history] +else: + refresh_history = [] + +min_refresh_time = min(refresh_history) if refresh_history else 0 +max_refresh_time = max(refresh_history) if refresh_history else 0 +avg_refresh_time = statistics.mean(refresh_history) if refresh_history else 0 + +panel = nipanel.get_panel_accessor() + +num_timing_runs = 5 +time_points, time_points_ms = profile_get_value(panel, "time_points", [0.0], num_timing_runs) +sine_values, sine_values_ms = profile_get_value(panel, "sine_values", [0.0], num_timing_runs) +amplitude, amplitude_ms = profile_get_value(panel, "amplitude", 1.0, num_timing_runs) +frequency, frequency_ms = profile_get_value(panel, "frequency", 1.0, num_timing_runs) +unset_value, unset_value_ms = profile_get_value(panel, "unset_value", "default", num_timing_runs) + +data = [{"value": [x, y]} for x, y in zip(time_points, sine_values)] + +options = { + "animation": False, # Disable animation for smoother updates + "title": {"text": "Sine Wave"}, + "tooltip": {"trigger": "axis"}, + "xAxis": {"type": "value", "name": "Time (s)", "nameLocation": "middle", "nameGap": 30}, + "yAxis": { + "type": "value", + "name": "Amplitude", + "nameLocation": "middle", + "nameGap": 30, + }, + "series": [ + { + "data": data, + "type": "line", + "showSymbol": True, + "smooth": True, + "lineStyle": {"width": 2, "color": "#1f77b4"}, + "areaStyle": {"color": "#1f77b4", "opacity": 0.3}, + "name": "Sine Wave", + } + ], +} + +st_echarts(options=options, height="400px", key="graph") + +col1, col2, col3 = st.columns(3) +with col1: + st.metric("Amplitude", f"{amplitude:.2f}") + st.metric("Frequency", f"{frequency:.2f} Hz") +with col2: + st.metric("Refresh Time", f"{time_since_last_refresh:.1f} ms") + st.metric("Min Refresh Time", f"{min_refresh_time:.1f} ms") + st.metric("Max Refresh Time", f"{max_refresh_time:.1f} ms") + st.metric("Avg Refresh Time", f"{avg_refresh_time:.1f} ms") + st.metric("FPS", f"{len(refresh_history)}") + +with col3: + st.metric("get time_points", f"{time_points_ms:.1f} ms") + st.metric("get sine_values", f"{sine_values_ms:.1f} ms") + st.metric("get amplitude", f"{amplitude_ms:.1f} ms") + st.metric("get frequency", f"{frequency_ms:.1f} ms") + st.metric("get unset_value", f"{unset_value_ms:.1f} ms") diff --git a/protos/ni/pythonpanel/v1/python_panel_service.proto b/protos/ni/pythonpanel/v1/python_panel_service.proto index 5b8dd2a..0476d06 100644 --- a/protos/ni/pythonpanel/v1/python_panel_service.proto +++ b/protos/ni/pythonpanel/v1/python_panel_service.proto @@ -34,7 +34,6 @@ service PythonPanelService { // Get a value for a control on the panel // Status Codes for errors: // - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. - // - NOT_FOUND: The value with the specified identifier was not found rpc GetValue(GetValueRequest) returns (GetValueResponse); // Set a value for a control on the panel @@ -99,7 +98,7 @@ message GetValueRequest { message GetValueResponse { // The value - google.protobuf.Any value = 1; + optional google.protobuf.Any value = 1; } message SetValueRequest { diff --git a/src/ni/pythonpanel/v1/python_panel_service_pb2.py b/src/ni/pythonpanel/v1/python_panel_service_pb2.py index 3435f6b..3d03f69 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\"U\n\x11StartPanelRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x19\n\x11panel_script_path\x18\x02 \x01(\t\x12\x13\n\x0bpython_path\x18\x03 \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\"U\n\x11StartPanelRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x19\n\x11panel_script_path\x18\x02 \x01(\t\x12\x13\n\x0bpython_path\x18\x03 \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\"F\n\x10GetValueResponse\x12(\n\x05value\x18\x01 \x01(\x0b\x32\x14.google.protobuf.AnyH\x00\x88\x01\x01\x42\x08\n\x06_value\"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()) @@ -39,11 +39,11 @@ _GETVALUEREQUEST._serialized_start=478 _GETVALUEREQUEST._serialized_end=531 _GETVALUERESPONSE._serialized_start=533 - _GETVALUERESPONSE._serialized_end=588 - _SETVALUEREQUEST._serialized_start=590 - _SETVALUEREQUEST._serialized_end=696 - _SETVALUERESPONSE._serialized_start=698 - _SETVALUERESPONSE._serialized_end=716 - _PYTHONPANELSERVICE._serialized_start=719 - _PYTHONPANELSERVICE._serialized_end=1194 + _GETVALUERESPONSE._serialized_end=603 + _SETVALUEREQUEST._serialized_start=605 + _SETVALUEREQUEST._serialized_end=711 + _SETVALUERESPONSE._serialized_start=713 + _SETVALUERESPONSE._serialized_end=731 + _PYTHONPANELSERVICE._serialized_start=734 + _PYTHONPANELSERVICE._serialized_end=1209 # @@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 71483cf..aba0543 100644 --- a/src/ni/pythonpanel/v1/python_panel_service_pb2.pyi +++ b/src/ni/pythonpanel/v1/python_panel_service_pb2.pyi @@ -171,8 +171,9 @@ class GetValueResponse(google.protobuf.message.Message): *, 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"]) -> None: ... + def HasField(self, field_name: typing.Literal["_value", b"_value", "value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_value", b"_value", "value", b"value"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_value", b"_value"]) -> typing.Literal["value"] | None: ... global___GetValueResponse = GetValueResponse 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..01ba9c9 100644 --- a/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.py +++ b/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.py @@ -77,7 +77,6 @@ def GetValue(self, request, context): """Get a value for a control on the panel Status Codes for errors: - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. - - NOT_FOUND: The value with the specified identifier was not found """ context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details('Method not implemented!') 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..622c2e0 100644 --- a/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.pyi +++ b/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.pyi @@ -55,7 +55,6 @@ class PythonPanelServiceStub: """Get a value for a control on the panel Status Codes for errors: - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. - - NOT_FOUND: The value with the specified identifier was not found """ SetValue: grpc.UnaryUnaryMultiCallable[ @@ -104,7 +103,6 @@ class PythonPanelServiceAsyncStub: """Get a value for a control on the panel Status Codes for errors: - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. - - NOT_FOUND: The value with the specified identifier was not found """ SetValue: grpc.aio.UnaryUnaryMultiCallable[ @@ -161,7 +159,6 @@ class PythonPanelServiceServicer(metaclass=abc.ABCMeta): """Get a value for a control on the panel Status Codes for errors: - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. - - NOT_FOUND: The value with the specified identifier was not found """ @abc.abstractmethod diff --git a/src/nipanel/_panel_client.py b/src/nipanel/_panel_client.py index d06e2c0..278378e 100644 --- a/src/nipanel/_panel_client.py +++ b/src/nipanel/_panel_client.py @@ -116,7 +116,7 @@ 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 get_value(self, panel_id: str, value_id: str) -> object: + def get_value(self, panel_id: str, value_id: str) -> tuple[bool, object]: """Get the value for the control with value_id. Args: @@ -124,12 +124,15 @@ def get_value(self, panel_id: str, value_id: str) -> object: value_id: The ID of the control. Returns: - The value. + A tuple containing a boolean indicating whether the value was successfully retrieved and + the value itself (or None if not present). """ get_value_request = GetValueRequest(panel_id=panel_id, value_id=value_id) response = self._invoke_with_retry(self._get_stub().GetValue, get_value_request) - the_value = from_any(response.value) - return the_value + if response.HasField("value"): + return True, from_any(response.value) + else: + return False, None def _get_stub(self) -> PythonPanelServiceStub: if self._stub is None: diff --git a/src/nipanel/_panel_value_accessor.py b/src/nipanel/_panel_value_accessor.py index c174843..d60f7d7 100644 --- a/src/nipanel/_panel_value_accessor.py +++ b/src/nipanel/_panel_value_accessor.py @@ -60,17 +60,15 @@ def get_value(self, value_id: str, default_value: _T | None = None) -> _T | obje Returns: The value, or the default value if not set """ - try: - value = self._panel_client.get_value(self._panel_id, value_id) - if default_value is not None and not isinstance(value, type(default_value)): - raise TypeError("Value type does not match default value type.") - return value - - except grpc.RpcError as e: - if e.code() == grpc.StatusCode.NOT_FOUND and default_value is not None: + found, value = self._panel_client.get_value(self._panel_id, value_id) + if not found: + if default_value is not None: return default_value - else: - raise e + raise KeyError(f"Value with id '{value_id}' not found on panel '{self._panel_id}'.") + + if default_value is not None and not isinstance(value, type(default_value)): + raise TypeError("Value type does not match default value type.") + return value def set_value(self, value_id: str, value: object) -> None: """Set the value for a control on the panel. diff --git a/tests/unit/test_panel_client.py b/tests/unit/test_panel_client.py index 2a9723d..151d912 100644 --- a/tests/unit/test_panel_client.py +++ b/tests/unit/test_panel_client.py @@ -1,5 +1,4 @@ import grpc -import pytest from nipanel._panel_client import PanelClient @@ -51,11 +50,12 @@ def test___start_panels___stop_panel_1_without_reset___enumerate_has_both_panels } -def test___get_unset_value_raises_exception(fake_panel_channel: grpc.Channel) -> None: +def test___get_unset_value___returns_not_found(fake_panel_channel: grpc.Channel) -> None: client = create_panel_client(fake_panel_channel) - with pytest.raises(Exception): - client.get_value("panel1", "unset_id") + response = client.get_value("panel1", "unset_id") + + assert response == (False, None) def test___set_value___enumerate_panels_shows_value( @@ -73,7 +73,7 @@ def test___set_value___gets_value(fake_panel_channel: grpc.Channel) -> None: client.set_value("panel1", "val1", "value1", notify=False) - assert client.get_value("panel1", "val1") == "value1" + assert client.get_value("panel1", "val1") == (True, "value1") def create_panel_client(fake_panel_channel: grpc.Channel) -> PanelClient: diff --git a/tests/unit/test_streamlit_panel.py b/tests/unit/test_streamlit_panel.py index a271c79..c1c3382 100644 --- a/tests/unit/test_streamlit_panel.py +++ b/tests/unit/test_streamlit_panel.py @@ -131,7 +131,7 @@ def test___panel___get_unset_value_with_no_default___raises_exception( panel = StreamlitPanel("my_panel", "path/to/script", grpc_channel=fake_panel_channel) value_id = "test_id" - with pytest.raises(grpc.RpcError): + with pytest.raises(KeyError): panel.get_value(value_id) diff --git a/tests/utils/_fake_python_panel_servicer.py b/tests/utils/_fake_python_panel_servicer.py index 5d450a0..e6d59ee 100644 --- a/tests/utils/_fake_python_panel_servicer.py +++ b/tests/utils/_fake_python_panel_servicer.py @@ -62,7 +62,7 @@ def EnumeratePanels( # noqa: N802 def GetValue(self, request: GetValueRequest, context: Any) -> GetValueResponse: # noqa: N802 """Trivial implementation for testing.""" if request.value_id not in self._panel_value_ids.get(request.panel_id, {}): - context.abort(grpc.StatusCode.NOT_FOUND, "Value ID not found in panel") + return GetValueResponse() value = self._panel_value_ids[request.panel_id][request.value_id] return GetValueResponse(value=value)