From 789b7503da9e7f12e9ae8a6abe6634a61219dcf0 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Tue, 27 May 2025 10:34:31 -0500 Subject: [PATCH 1/4] first draft --- .../pythonpanel/v1/python_panel_service.proto | 44 +++++++++-- .../v1/python_panel_service_pb2.py | 38 +++++---- .../v1/python_panel_service_pb2.pyi | 60 +++++++++++++++ .../v1/python_panel_service_pb2_grpc.py | 77 ++++++++++++++++++- .../v1/python_panel_service_pb2_grpc.pyi | 73 ++++++++++++++++-- src/nipanel/_panel.py | 8 ++ src/nipanel/_panel_client.py | 33 +++++++- tests/unit/test_python_panel_service_stub.py | 19 +++++ tests/utils/_fake_python_panel_servicer.py | 18 +++++ 9 files changed, 338 insertions(+), 32 deletions(-) diff --git a/protos/ni/pythonpanel/v1/python_panel_service.proto b/protos/ni/pythonpanel/v1/python_panel_service.proto index 0dd6073..d131296 100644 --- a/protos/ni/pythonpanel/v1/python_panel_service.proto +++ b/protos/ni/pythonpanel/v1/python_panel_service.proto @@ -16,20 +16,39 @@ option ruby_package = "NI::PythonPanel::V1"; // Service interface for interacting with python panels service PythonPanelService { + // Enumerate the panels available in the system + // Status Codes for errors: + rpc EnumeratePanels(EnumeratePanelsRequest) returns (EnumeratePanelsResponse); + // Open a panel // Status Codes for errors: + // - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. // - NOT_FOUND: the file for the panel was not found rpc OpenPanel(OpenPanelRequest) returns (OpenPanelResponse); // Get a value for a control on the panel // Status Codes for errors: - // - NOT_FOUND: the panel with the specified id was not found + // - 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 // Status Codes for errors: - // - NOT_FOUND: the panel with the specified id was not found + // - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. rpc SetValue(SetValueRequest) returns (SetValueResponse); + + // Close a panel + // Status Codes for errors: + // - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. + rpc ClosePanel(ClosePanelRequest) returns (ClosePanelResponse); +} + +message EnumeratePanelsRequest { +} + +message EnumeratePanelsResponse { + // The list of panels available in the system + repeated string panel_ids = 1; } message OpenPanelRequest { @@ -40,12 +59,12 @@ message OpenPanelRequest { string panel_uri = 2; } -message OpenPanelResponse { +message OpenPanelResponse { } message GetValueRequest { // Unique ID of the panel - string panel_id = 1; + string panel_id = 1; // Unique ID of value string value_id = 2; @@ -58,8 +77,8 @@ message GetValueResponse { message SetValueRequest { // Unique ID of the panel - string panel_id = 1; - + string panel_id = 1; + // Unique ID of the value string value_id = 2; @@ -67,5 +86,16 @@ message SetValueRequest { google.protobuf.Any value = 3; } -message SetValueResponse { +message SetValueResponse { +} + +message ClosePanelRequest { + // Unique ID of the panel + string panel_id = 1; + + // Reset all storage associated with panel + bool reset = 2; +} + +message ClosePanelResponse { } \ 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 f2c1e9c..79fce9c 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\"7\n\x10OpenPanelRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x11\n\tpanel_uri\x18\x02 \x01(\t\"\x13\n\x11OpenPanelResponse\"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\"Z\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\n\x10SetValueResponse2\x96\x02\n\x12PythonPanelService\x12V\n\tOpenPanel\x12#.ni.pythonpanel.v1.OpenPanelRequest\x1a$.ni.pythonpanel.v1.OpenPanelResponse\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\"\x18\n\x16\x45numeratePanelsRequest\",\n\x17\x45numeratePanelsResponse\x12\x11\n\tpanel_ids\x18\x01 \x03(\t\"7\n\x10OpenPanelRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x11\n\tpanel_uri\x18\x02 \x01(\t\"\x13\n\x11OpenPanelResponse\"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\"Z\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\n\x10SetValueResponse\"4\n\x11\x43losePanelRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\r\n\x05reset\x18\x02 \x01(\x08\"\x14\n\x12\x43losePanelResponse2\xdb\x03\n\x12PythonPanelService\x12h\n\x0f\x45numeratePanels\x12).ni.pythonpanel.v1.EnumeratePanelsRequest\x1a*.ni.pythonpanel.v1.EnumeratePanelsResponse\x12V\n\tOpenPanel\x12#.ni.pythonpanel.v1.OpenPanelRequest\x1a$.ni.pythonpanel.v1.OpenPanelResponse\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\x12Y\n\nClosePanel\x12$.ni.pythonpanel.v1.ClosePanelRequest\x1a%.ni.pythonpanel.v1.ClosePanelResponseB\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()) @@ -22,18 +22,26 @@ DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'\n\025com.ni.pythonpanel.v1B\027PythonPanelServiceProtoP\001Z\rpythonpanelv1\370\001\001\242\002\004NIPP\252\002\"NationalInstruments.PythonPanel.V1\312\002\021NI\\PythonPanel\\V1\352\002\023NI::PythonPanel::V1' - _OPENPANELREQUEST._serialized_start=94 - _OPENPANELREQUEST._serialized_end=149 - _OPENPANELRESPONSE._serialized_start=151 - _OPENPANELRESPONSE._serialized_end=170 - _GETVALUEREQUEST._serialized_start=172 - _GETVALUEREQUEST._serialized_end=225 - _GETVALUERESPONSE._serialized_start=227 - _GETVALUERESPONSE._serialized_end=282 - _SETVALUEREQUEST._serialized_start=284 - _SETVALUEREQUEST._serialized_end=374 - _SETVALUERESPONSE._serialized_start=376 - _SETVALUERESPONSE._serialized_end=394 - _PYTHONPANELSERVICE._serialized_start=397 - _PYTHONPANELSERVICE._serialized_end=675 + _ENUMERATEPANELSREQUEST._serialized_start=94 + _ENUMERATEPANELSREQUEST._serialized_end=118 + _ENUMERATEPANELSRESPONSE._serialized_start=120 + _ENUMERATEPANELSRESPONSE._serialized_end=164 + _OPENPANELREQUEST._serialized_start=166 + _OPENPANELREQUEST._serialized_end=221 + _OPENPANELRESPONSE._serialized_start=223 + _OPENPANELRESPONSE._serialized_end=242 + _GETVALUEREQUEST._serialized_start=244 + _GETVALUEREQUEST._serialized_end=297 + _GETVALUERESPONSE._serialized_start=299 + _GETVALUERESPONSE._serialized_end=354 + _SETVALUEREQUEST._serialized_start=356 + _SETVALUEREQUEST._serialized_end=446 + _SETVALUERESPONSE._serialized_start=448 + _SETVALUERESPONSE._serialized_end=466 + _CLOSEPANELREQUEST._serialized_start=468 + _CLOSEPANELREQUEST._serialized_end=520 + _CLOSEPANELRESPONSE._serialized_start=522 + _CLOSEPANELRESPONSE._serialized_end=542 + _PYTHONPANELSERVICE._serialized_start=545 + _PYTHONPANELSERVICE._serialized_end=1020 # @@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 989f29f..161d039 100644 --- a/src/ni/pythonpanel/v1/python_panel_service_pb2.pyi +++ b/src/ni/pythonpanel/v1/python_panel_service_pb2.pyi @@ -4,13 +4,43 @@ isort:skip_file """ import builtins +import collections.abc import google.protobuf.any_pb2 import google.protobuf.descriptor +import google.protobuf.internal.containers import google.protobuf.message import typing DESCRIPTOR: google.protobuf.descriptor.FileDescriptor +@typing.final +class EnumeratePanelsRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___EnumeratePanelsRequest = EnumeratePanelsRequest + +@typing.final +class EnumeratePanelsResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + PANEL_IDS_FIELD_NUMBER: builtins.int + @property + def panel_ids(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """The list of panels available in the system""" + + def __init__( + self, + *, + panel_ids: collections.abc.Iterable[builtins.str] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["panel_ids", b"panel_ids"]) -> None: ... + +global___EnumeratePanelsResponse = EnumeratePanelsResponse + @typing.final class OpenPanelRequest(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -116,3 +146,33 @@ class SetValueResponse(google.protobuf.message.Message): ) -> None: ... global___SetValueResponse = SetValueResponse + +@typing.final +class ClosePanelRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + PANEL_ID_FIELD_NUMBER: builtins.int + RESET_FIELD_NUMBER: builtins.int + panel_id: builtins.str + """Unique ID of the panel""" + reset: builtins.bool + """Reset all storage associated with panel""" + def __init__( + self, + *, + panel_id: builtins.str = ..., + reset: builtins.bool = ..., + ) -> None: ... + def ClearField(self, field_name: typing.Literal["panel_id", b"panel_id", "reset", b"reset"]) -> None: ... + +global___ClosePanelRequest = ClosePanelRequest + +@typing.final +class ClosePanelResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___ClosePanelResponse = ClosePanelResponse 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 4b04c3e..f3bfc2e 100644 --- a/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.py +++ b/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.py @@ -15,6 +15,11 @@ def __init__(self, channel): Args: channel: A grpc.Channel. """ + self.EnumeratePanels = channel.unary_unary( + '/ni.pythonpanel.v1.PythonPanelService/EnumeratePanels', + request_serializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.EnumeratePanelsRequest.SerializeToString, + response_deserializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.EnumeratePanelsResponse.FromString, + ) self.OpenPanel = channel.unary_unary( '/ni.pythonpanel.v1.PythonPanelService/OpenPanel', request_serializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.OpenPanelRequest.SerializeToString, @@ -30,15 +35,29 @@ 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.ClosePanel = channel.unary_unary( + '/ni.pythonpanel.v1.PythonPanelService/ClosePanel', + request_serializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.ClosePanelRequest.SerializeToString, + response_deserializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.ClosePanelResponse.FromString, + ) class PythonPanelServiceServicer(object): """Service interface for interacting with python panels """ + def EnumeratePanels(self, request, context): + """Enumerate the panels available in the system + Status Codes for errors: + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def OpenPanel(self, request, context): """Open a panel Status Codes for errors: + - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. - NOT_FOUND: the file for the panel was not found """ context.set_code(grpc.StatusCode.UNIMPLEMENTED) @@ -48,7 +67,8 @@ def OpenPanel(self, request, context): def GetValue(self, request, context): """Get a value for a control on the panel Status Codes for errors: - - NOT_FOUND: the panel with the specified id was not found + - 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!') @@ -57,7 +77,16 @@ def GetValue(self, request, context): def SetValue(self, request, context): """Set a value for a control on the panel Status Codes for errors: - - NOT_FOUND: the panel with the specified id was not found + - 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 ClosePanel(self, request, context): + """Close a 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!') @@ -66,6 +95,11 @@ def SetValue(self, request, context): def add_PythonPanelServiceServicer_to_server(servicer, server): rpc_method_handlers = { + 'EnumeratePanels': grpc.unary_unary_rpc_method_handler( + servicer.EnumeratePanels, + request_deserializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.EnumeratePanelsRequest.FromString, + response_serializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.EnumeratePanelsResponse.SerializeToString, + ), 'OpenPanel': grpc.unary_unary_rpc_method_handler( servicer.OpenPanel, request_deserializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.OpenPanelRequest.FromString, @@ -81,6 +115,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, ), + 'ClosePanel': grpc.unary_unary_rpc_method_handler( + servicer.ClosePanel, + request_deserializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.ClosePanelRequest.FromString, + response_serializer=ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.ClosePanelResponse.SerializeToString, + ), } generic_handler = grpc.method_handlers_generic_handler( 'ni.pythonpanel.v1.PythonPanelService', rpc_method_handlers) @@ -92,6 +131,23 @@ class PythonPanelService(object): """Service interface for interacting with python panels """ + @staticmethod + def EnumeratePanels(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/EnumeratePanels', + ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.EnumeratePanelsRequest.SerializeToString, + ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.EnumeratePanelsResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + @staticmethod def OpenPanel(request, target, @@ -142,3 +198,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 ClosePanel(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/ClosePanel', + ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.ClosePanelRequest.SerializeToString, + ni_dot_pythonpanel_dot_v1_dot_python__panel__service__pb2.ClosePanelResponse.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 4b9a62a..d6a16b5 100644 --- a/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.pyi +++ b/src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.pyi @@ -21,12 +21,21 @@ class PythonPanelServiceStub: """Service interface for interacting with python panels""" def __init__(self, channel: typing.Union[grpc.Channel, grpc.aio.Channel]) -> None: ... + EnumeratePanels: grpc.UnaryUnaryMultiCallable[ + ni.pythonpanel.v1.python_panel_service_pb2.EnumeratePanelsRequest, + ni.pythonpanel.v1.python_panel_service_pb2.EnumeratePanelsResponse, + ] + """Enumerate the panels available in the system + Status Codes for errors: + """ + OpenPanel: grpc.UnaryUnaryMultiCallable[ ni.pythonpanel.v1.python_panel_service_pb2.OpenPanelRequest, ni.pythonpanel.v1.python_panel_service_pb2.OpenPanelResponse, ] """Open a panel Status Codes for errors: + - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. - NOT_FOUND: the file for the panel was not found """ @@ -36,7 +45,8 @@ class PythonPanelServiceStub: ] """Get a value for a control on the panel Status Codes for errors: - - NOT_FOUND: the panel with the specified id was not found + - 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[ @@ -45,18 +55,36 @@ class PythonPanelServiceStub: ] """Set a value for a control on the panel Status Codes for errors: - - NOT_FOUND: the panel with the specified id was not found + - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. + """ + + ClosePanel: grpc.UnaryUnaryMultiCallable[ + ni.pythonpanel.v1.python_panel_service_pb2.ClosePanelRequest, + ni.pythonpanel.v1.python_panel_service_pb2.ClosePanelResponse, + ] + """Close a 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""" + EnumeratePanels: grpc.aio.UnaryUnaryMultiCallable[ + ni.pythonpanel.v1.python_panel_service_pb2.EnumeratePanelsRequest, + ni.pythonpanel.v1.python_panel_service_pb2.EnumeratePanelsResponse, + ] + """Enumerate the panels available in the system + Status Codes for errors: + """ + OpenPanel: grpc.aio.UnaryUnaryMultiCallable[ ni.pythonpanel.v1.python_panel_service_pb2.OpenPanelRequest, ni.pythonpanel.v1.python_panel_service_pb2.OpenPanelResponse, ] """Open a panel Status Codes for errors: + - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. - NOT_FOUND: the file for the panel was not found """ @@ -66,7 +94,8 @@ class PythonPanelServiceAsyncStub: ] """Get a value for a control on the panel Status Codes for errors: - - NOT_FOUND: the panel with the specified id was not found + - 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[ @@ -75,12 +104,31 @@ class PythonPanelServiceAsyncStub: ] """Set a value for a control on the panel Status Codes for errors: - - NOT_FOUND: the panel with the specified id was not found + - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. + """ + + ClosePanel: grpc.aio.UnaryUnaryMultiCallable[ + ni.pythonpanel.v1.python_panel_service_pb2.ClosePanelRequest, + ni.pythonpanel.v1.python_panel_service_pb2.ClosePanelResponse, + ] + """Close a 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""" + @abc.abstractmethod + def EnumeratePanels( + self, + request: ni.pythonpanel.v1.python_panel_service_pb2.EnumeratePanelsRequest, + context: _ServicerContext, + ) -> typing.Union[ni.pythonpanel.v1.python_panel_service_pb2.EnumeratePanelsResponse, collections.abc.Awaitable[ni.pythonpanel.v1.python_panel_service_pb2.EnumeratePanelsResponse]]: + """Enumerate the panels available in the system + Status Codes for errors: + """ + @abc.abstractmethod def OpenPanel( self, @@ -89,6 +137,7 @@ class PythonPanelServiceServicer(metaclass=abc.ABCMeta): ) -> typing.Union[ni.pythonpanel.v1.python_panel_service_pb2.OpenPanelResponse, collections.abc.Awaitable[ni.pythonpanel.v1.python_panel_service_pb2.OpenPanelResponse]]: """Open a panel Status Codes for errors: + - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. - NOT_FOUND: the file for the panel was not found """ @@ -100,7 +149,8 @@ class PythonPanelServiceServicer(metaclass=abc.ABCMeta): ) -> typing.Union[ni.pythonpanel.v1.python_panel_service_pb2.GetValueResponse, collections.abc.Awaitable[ni.pythonpanel.v1.python_panel_service_pb2.GetValueResponse]]: """Get a value for a control on the panel Status Codes for errors: - - NOT_FOUND: the panel with the specified id was not found + - 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 @@ -111,7 +161,18 @@ class PythonPanelServiceServicer(metaclass=abc.ABCMeta): ) -> typing.Union[ni.pythonpanel.v1.python_panel_service_pb2.SetValueResponse, collections.abc.Awaitable[ni.pythonpanel.v1.python_panel_service_pb2.SetValueResponse]]: """Set a value for a control on the panel Status Codes for errors: - - NOT_FOUND: the panel with the specified id was not found + - INVALID_ARGUMENT: The specified identifier contains invalid characters. Only alphanumeric characters and underscores are allowed. + """ + + @abc.abstractmethod + def ClosePanel( + self, + request: ni.pythonpanel.v1.python_panel_service_pb2.ClosePanelRequest, + context: _ServicerContext, + ) -> typing.Union[ni.pythonpanel.v1.python_panel_service_pb2.ClosePanelResponse, collections.abc.Awaitable[ni.pythonpanel.v1.python_panel_service_pb2.ClosePanelResponse]]: + """Close a 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.py b/src/nipanel/_panel.py index 41d0d0b..d1f0b61 100644 --- a/src/nipanel/_panel.py +++ b/src/nipanel/_panel.py @@ -54,6 +54,14 @@ def open_panel(self) -> None: """Open the panel.""" self._panel_client.open_panel(self._panel_id, self._panel_uri) + def close_panel(self, reset: bool) -> None: + """Close the panel. + + Args: + reset: Whether to reset all storage associated with the panel. + """ + self._panel_client.close_panel(self._panel_id, reset=reset) + def get_value(self, value_id: str) -> object: """Get the value for a control on the panel. diff --git a/src/nipanel/_panel_client.py b/src/nipanel/_panel_client.py index 82b21c4..48d8fa6 100644 --- a/src/nipanel/_panel_client.py +++ b/src/nipanel/_panel_client.py @@ -8,8 +8,10 @@ import grpc from ni.pythonpanel.v1.python_panel_service_pb2 import ( - GetValueRequest, OpenPanelRequest, + ClosePanelRequest, + EnumeratePanelsRequest, + GetValueRequest, SetValueRequest, ) from ni.pythonpanel.v1.python_panel_service_pb2_grpc import PythonPanelServiceStub @@ -58,10 +60,37 @@ def __init__( self._stub: PythonPanelServiceStub | None = None def open_panel(self, panel_id: str, panel_uri: str) -> None: - """Open the panel.""" + """Open the panel. + + Args: + panel_id: The ID of the panel to open. + panel_uri: The URI of the panel script. + """ open_panel_request = OpenPanelRequest(panel_id=panel_id, panel_uri=panel_uri) self._invoke_with_retry(self._get_stub().OpenPanel, open_panel_request) + def close_panel(self, panel_id: str, reset: bool) -> None: + """Close the panel. + + Args: + panel_id: The ID of the panel to close. + reset: Whether to reset all storage associated with panel. + """ + close_panel_request = ClosePanelRequest(panel_id=panel_id, reset=reset) + self._invoke_with_retry(self._get_stub().ClosePanel, close_panel_request) + + def enumerate_panels(self) -> list[str]: + """Enumerate all available panels. + + Returns: + A list of panel IDs. + """ + enumerate_panels_request = EnumeratePanelsRequest() + response = self._invoke_with_retry( + self._get_stub().EnumeratePanels, enumerate_panels_request + ) + return list(response.panel_ids) + def set_value(self, panel_id: str, value_id: str, value: object) -> None: """Set the value for the control with value_id. diff --git a/tests/unit/test_python_panel_service_stub.py b/tests/unit/test_python_panel_service_stub.py index 3aaf911..113647b 100644 --- a/tests/unit/test_python_panel_service_stub.py +++ b/tests/unit/test_python_panel_service_stub.py @@ -2,6 +2,8 @@ from google.protobuf.wrappers_pb2 import StringValue from ni.pythonpanel.v1.python_panel_service_pb2 import ( OpenPanelRequest, + ClosePanelRequest, + EnumeratePanelsRequest, GetValueRequest, SetValueRequest, ) @@ -15,6 +17,23 @@ def test___open_panel___gets_response(python_panel_service_stub: PythonPanelServ assert response is not None # Ensure response is returned +def test___close_panel___gets_response(python_panel_service_stub: PythonPanelServiceStub) -> None: + request = ClosePanelRequest(panel_id="test_panel", reset=False) + response = python_panel_service_stub.ClosePanel(request) + + assert response is not None # Ensure response is returned + + +def test___enumerate_panels___gets_response( + python_panel_service_stub: PythonPanelServiceStub, +) -> None: + request = EnumeratePanelsRequest() + response = python_panel_service_stub.EnumeratePanels(request) + + assert response is not None # Ensure response is returned + assert len(response.panel_ids) > 0 # Ensure there is at least one panel ID + + def test___get_value___gets_response( python_panel_service_stub: PythonPanelServiceStub, ) -> None: diff --git a/tests/utils/_fake_python_panel_servicer.py b/tests/utils/_fake_python_panel_servicer.py index 920d3dd..4c51b5e 100644 --- a/tests/utils/_fake_python_panel_servicer.py +++ b/tests/utils/_fake_python_panel_servicer.py @@ -5,6 +5,10 @@ from ni.pythonpanel.v1.python_panel_service_pb2 import ( OpenPanelRequest, OpenPanelResponse, + ClosePanelRequest, + ClosePanelResponse, + EnumeratePanelsRequest, + EnumeratePanelsResponse, GetValueRequest, GetValueResponse, SetValueRequest, @@ -28,6 +32,20 @@ def OpenPanel(self, request: OpenPanelRequest, context: Any) -> OpenPanelRespons context.abort(grpc.StatusCode.UNAVAILABLE, "Simulated failure") return OpenPanelResponse() + def ClosePanel( # noqa: N802 + self, request: ClosePanelRequest, context: Any + ) -> ClosePanelResponse: + """Trivial implementation for testing.""" + # No action needed for close panel in this fake implementation. + return ClosePanelResponse() + + def EnumeratePanels( # noqa: N802 + self, request: EnumeratePanelsRequest, context: Any + ) -> EnumeratePanelsResponse: + """Trivial implementation for testing.""" + # Return a panel id. + return EnumeratePanelsResponse(panel_ids=["test_panel"]) + def GetValue(self, request: GetValueRequest, context: Any) -> GetValueResponse: # noqa: N802 """Trivial implementation for testing.""" value = self._values[request.value_id] From 2cc10b26a25ce572b49a1009ce218e88c8724eb8 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Tue, 27 May 2025 14:37:41 -0500 Subject: [PATCH 2/4] add tests for panel_client, since enumerate_panels isn't exposed on panel --- tests/unit/test_panel_client.py | 54 ++++++++++++++++++++ tests/unit/test_python_panel_service_stub.py | 27 ++++++---- tests/utils/_fake_python_panel_servicer.py | 10 ++-- 3 files changed, 77 insertions(+), 14 deletions(-) create mode 100644 tests/unit/test_panel_client.py diff --git a/tests/unit/test_panel_client.py b/tests/unit/test_panel_client.py new file mode 100644 index 0000000..f3323cd --- /dev/null +++ b/tests/unit/test_panel_client.py @@ -0,0 +1,54 @@ +import grpc +import pytest + +from nipanel._panel_client import PanelClient + + +def test___enumerate_is_empty(fake_panel_channel: grpc.Channel): + client = create_panel_client(fake_panel_channel) + + assert client.enumerate_panels() == [] + + +def test___open_panels___enumerate_has_panels(fake_panel_channel: grpc.Channel): + client = create_panel_client(fake_panel_channel) + + client.open_panel("panel1", "uri1") + client.open_panel("panel2", "uri2") + + assert client.enumerate_panels() == ["panel1", "panel2"] + + +def test___open_panels___close_panel_1___enumerate_has_panel_2( + fake_panel_channel: grpc.Channel, +): + client = create_panel_client(fake_panel_channel) + client.open_panel("panel1", "uri1") + client.open_panel("panel2", "uri2") + + client.close_panel("panel1", reset=False) + + assert client.enumerate_panels() == ["panel2"] + + +def test___get_value_raises(fake_panel_channel: grpc.Channel): + client = create_panel_client(fake_panel_channel) + + with pytest.raises(Exception): + client.get_value("panel1", "unset_id") + + +def test___set_value___gets_value(fake_panel_channel: grpc.Channel): + client = create_panel_client(fake_panel_channel) + + client.set_value("panel1", "val1", "value1") + + assert client.get_value("panel1", "val1") == "value1" + + +def create_panel_client(fake_panel_channel: grpc.Channel) -> PanelClient: + return PanelClient( + provided_interface="iface", + service_class="svc", + grpc_channel=fake_panel_channel, + ) diff --git a/tests/unit/test_python_panel_service_stub.py b/tests/unit/test_python_panel_service_stub.py index 113647b..ffcab27 100644 --- a/tests/unit/test_python_panel_service_stub.py +++ b/tests/unit/test_python_panel_service_stub.py @@ -17,7 +17,12 @@ def test___open_panel___gets_response(python_panel_service_stub: PythonPanelServ assert response is not None # Ensure response is returned -def test___close_panel___gets_response(python_panel_service_stub: PythonPanelServiceStub) -> None: +def test___open_panel___close_panel___gets_response( + python_panel_service_stub: PythonPanelServiceStub, +) -> None: + open_request = OpenPanelRequest(panel_id="test_panel", panel_uri="path/to/panel") + python_panel_service_stub.OpenPanel(open_request) + request = ClosePanelRequest(panel_id="test_panel", reset=False) response = python_panel_service_stub.ClosePanel(request) @@ -31,25 +36,29 @@ def test___enumerate_panels___gets_response( response = python_panel_service_stub.EnumeratePanels(request) assert response is not None # Ensure response is returned - assert len(response.panel_ids) > 0 # Ensure there is at least one panel ID -def test___get_value___gets_response( +def test___set_value___gets_response( python_panel_service_stub: PythonPanelServiceStub, ) -> None: - request = GetValueRequest(panel_id="test_panel", value_id="test_value") - response = python_panel_service_stub.GetValue(request) + test_value = Any() + test_value.Pack(StringValue(value="test_value")) + request = SetValueRequest(panel_id="test_panel", value_id="test_value", value=test_value) + response = python_panel_service_stub.SetValue(request) assert response is not None # Ensure response is returned - assert isinstance(response.value, Any) # Ensure the value is of type `Any` -def test___set_value___gets_response( +def test___set_value___get_value___gets_response( python_panel_service_stub: PythonPanelServiceStub, ) -> None: test_value = Any() test_value.Pack(StringValue(value="test_value")) - request = SetValueRequest(panel_id="test_panel", value_id="test_value", value=test_value) - response = python_panel_service_stub.SetValue(request) + set_request = SetValueRequest(panel_id="test_panel", value_id="test_value", value=test_value) + python_panel_service_stub.SetValue(set_request) + + request = GetValueRequest(panel_id="test_panel", value_id="test_value") + response = python_panel_service_stub.GetValue(request) assert response is not None # Ensure response is returned + assert response.value == test_value # Ensure the value is correct diff --git a/tests/utils/_fake_python_panel_servicer.py b/tests/utils/_fake_python_panel_servicer.py index 4c51b5e..97ae9fc 100644 --- a/tests/utils/_fake_python_panel_servicer.py +++ b/tests/utils/_fake_python_panel_servicer.py @@ -1,7 +1,6 @@ from typing import Any import grpc -from google.protobuf import any_pb2 from ni.pythonpanel.v1.python_panel_service_pb2 import ( OpenPanelRequest, OpenPanelResponse, @@ -22,7 +21,8 @@ class FakePythonPanelServicer(PythonPanelServiceServicer): def __init__(self) -> None: """Initialize the fake PythonPanelServicer.""" - self._values = {"test_value": any_pb2.Any()} + self._values = {} + self._panel_ids = [] self._fail_next_open_panel = False def OpenPanel(self, request: OpenPanelRequest, context: Any) -> OpenPanelResponse: # noqa: N802 @@ -30,21 +30,21 @@ def OpenPanel(self, request: OpenPanelRequest, context: Any) -> OpenPanelRespons if self._fail_next_open_panel: self._fail_next_open_panel = False context.abort(grpc.StatusCode.UNAVAILABLE, "Simulated failure") + self._panel_ids.append(request.panel_id) return OpenPanelResponse() def ClosePanel( # noqa: N802 self, request: ClosePanelRequest, context: Any ) -> ClosePanelResponse: """Trivial implementation for testing.""" - # No action needed for close panel in this fake implementation. + self._panel_ids.remove(request.panel_id) return ClosePanelResponse() def EnumeratePanels( # noqa: N802 self, request: EnumeratePanelsRequest, context: Any ) -> EnumeratePanelsResponse: """Trivial implementation for testing.""" - # Return a panel id. - return EnumeratePanelsResponse(panel_ids=["test_panel"]) + return EnumeratePanelsResponse(panel_ids=self._panel_ids) def GetValue(self, request: GetValueRequest, context: Any) -> GetValueResponse: # noqa: N802 """Trivial implementation for testing.""" From 5886f79b2a9ddbb447ee2b35ab103dc9d9d9a096 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Tue, 27 May 2025 14:59:37 -0500 Subject: [PATCH 3/4] add tests for close() in test_streamlit_panel --- tests/unit/test_panel_client.py | 10 ++++---- tests/unit/test_streamlit_panel.py | 29 ++++++++++++++++++++++ tests/utils/_fake_python_panel_servicer.py | 6 +++-- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/tests/unit/test_panel_client.py b/tests/unit/test_panel_client.py index f3323cd..401c704 100644 --- a/tests/unit/test_panel_client.py +++ b/tests/unit/test_panel_client.py @@ -4,13 +4,13 @@ from nipanel._panel_client import PanelClient -def test___enumerate_is_empty(fake_panel_channel: grpc.Channel): +def test___enumerate_is_empty(fake_panel_channel: grpc.Channel) -> None: client = create_panel_client(fake_panel_channel) assert client.enumerate_panels() == [] -def test___open_panels___enumerate_has_panels(fake_panel_channel: grpc.Channel): +def test___open_panels___enumerate_has_panels(fake_panel_channel: grpc.Channel) -> None: client = create_panel_client(fake_panel_channel) client.open_panel("panel1", "uri1") @@ -21,7 +21,7 @@ def test___open_panels___enumerate_has_panels(fake_panel_channel: grpc.Channel): def test___open_panels___close_panel_1___enumerate_has_panel_2( fake_panel_channel: grpc.Channel, -): +) -> None: client = create_panel_client(fake_panel_channel) client.open_panel("panel1", "uri1") client.open_panel("panel2", "uri2") @@ -31,14 +31,14 @@ def test___open_panels___close_panel_1___enumerate_has_panel_2( assert client.enumerate_panels() == ["panel2"] -def test___get_value_raises(fake_panel_channel: grpc.Channel): +def test___get_unset_value_raises_exception(fake_panel_channel: grpc.Channel) -> None: client = create_panel_client(fake_panel_channel) with pytest.raises(Exception): client.get_value("panel1", "unset_id") -def test___set_value___gets_value(fake_panel_channel: grpc.Channel): +def test___set_value___gets_value(fake_panel_channel: grpc.Channel) -> None: client = create_panel_client(fake_panel_channel) client.set_value("panel1", "val1", "value1") diff --git a/tests/unit/test_streamlit_panel.py b/tests/unit/test_streamlit_panel.py index e1d0a29..f7813b8 100644 --- a/tests/unit/test_streamlit_panel.py +++ b/tests/unit/test_streamlit_panel.py @@ -64,6 +64,35 @@ def test___opened_panel___accessor_set_value___panel_gets_same_value( assert panel.get_value(value_id) == string_value +def test___opened_panel_with_value___close_without_reset___gets_value( + fake_panel_channel: grpc.Channel, +) -> None: + panel = StreamlitPanel("my_panel", "path/to/script", grpc_channel=fake_panel_channel) + panel.open_panel() + value_id = "test_id" + string_value = "test_value" + panel.set_value(value_id, string_value) + + panel.close_panel(reset=False) + + assert panel.get_value(value_id) == string_value + + +def test___opened_panel_with_value___close_with_reset___get_throws( + fake_panel_channel: grpc.Channel, +) -> None: + panel = StreamlitPanel("my_panel", "path/to/script", grpc_channel=fake_panel_channel) + panel.open_panel() + value_id = "test_id" + string_value = "test_value" + panel.set_value(value_id, string_value) + + panel.close_panel(reset=True) + + with pytest.raises(grpc.RpcError): + panel.get_value(value_id) + + def test___first_open_panel_fails___open_panel___gets_value( fake_python_panel_service: FakePythonPanelService, fake_panel_channel: grpc.Channel, diff --git a/tests/utils/_fake_python_panel_servicer.py b/tests/utils/_fake_python_panel_servicer.py index 97ae9fc..d14971f 100644 --- a/tests/utils/_fake_python_panel_servicer.py +++ b/tests/utils/_fake_python_panel_servicer.py @@ -21,8 +21,8 @@ class FakePythonPanelServicer(PythonPanelServiceServicer): def __init__(self) -> None: """Initialize the fake PythonPanelServicer.""" - self._values = {} - self._panel_ids = [] + self._values: dict[str, Any] = {} + self._panel_ids: list[str] = [] self._fail_next_open_panel = False def OpenPanel(self, request: OpenPanelRequest, context: Any) -> OpenPanelResponse: # noqa: N802 @@ -38,6 +38,8 @@ def ClosePanel( # noqa: N802 ) -> ClosePanelResponse: """Trivial implementation for testing.""" self._panel_ids.remove(request.panel_id) + if request.reset: + self._values.clear() return ClosePanelResponse() def EnumeratePanels( # noqa: N802 From a559650815287b8689b2107814955c7ebf5b51ba Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Wed, 28 May 2025 09:29:24 -0500 Subject: [PATCH 4/4] fix fake panel servicer to match real behavior better --- tests/unit/test_panel_client.py | 16 ++++++++++++++-- tests/utils/_fake_python_panel_servicer.py | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_panel_client.py b/tests/unit/test_panel_client.py index 401c704..a6d00f1 100644 --- a/tests/unit/test_panel_client.py +++ b/tests/unit/test_panel_client.py @@ -19,18 +19,30 @@ def test___open_panels___enumerate_has_panels(fake_panel_channel: grpc.Channel) assert client.enumerate_panels() == ["panel1", "panel2"] -def test___open_panels___close_panel_1___enumerate_has_panel_2( +def test___open_panels___close_panel_1_with_reset___enumerate_has_panel_2( fake_panel_channel: grpc.Channel, ) -> None: client = create_panel_client(fake_panel_channel) client.open_panel("panel1", "uri1") client.open_panel("panel2", "uri2") - client.close_panel("panel1", reset=False) + client.close_panel("panel1", reset=True) assert client.enumerate_panels() == ["panel2"] +def test___open_panels___close_panel_1_without_reset___enumerate_has_both_panels( + fake_panel_channel: grpc.Channel, +) -> None: + client = create_panel_client(fake_panel_channel) + client.open_panel("panel1", "uri1") + client.open_panel("panel2", "uri2") + + client.close_panel("panel1", reset=False) + + assert client.enumerate_panels() == ["panel1", "panel2"] + + def test___get_unset_value_raises_exception(fake_panel_channel: grpc.Channel) -> None: client = create_panel_client(fake_panel_channel) diff --git a/tests/utils/_fake_python_panel_servicer.py b/tests/utils/_fake_python_panel_servicer.py index d14971f..48d4edb 100644 --- a/tests/utils/_fake_python_panel_servicer.py +++ b/tests/utils/_fake_python_panel_servicer.py @@ -37,8 +37,8 @@ def ClosePanel( # noqa: N802 self, request: ClosePanelRequest, context: Any ) -> ClosePanelResponse: """Trivial implementation for testing.""" - self._panel_ids.remove(request.panel_id) if request.reset: + self._panel_ids.remove(request.panel_id) self._values.clear() return ClosePanelResponse()