Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ See [GitHub's official documentation](https://help.github.com/articles/using-pul
# Getting Started

This is the command to generate the files in /src/ni/pythonpanel/v1/:
`poetry run python -m grpc_tools.protoc --proto_path=protos --python_out=src/ --grpc_python_out=src/ ni/pythonpanel/v1/python_panel_service.proto`
`poetry run python -m grpc_tools.protoc --proto_path=protos --python_out=src/ --grpc_python_out=src/ --plugin=protoc-gen-mypy=.venv\Scripts\protoc-gen-mypy.exe --mypy_out=src/ --mypy_grpc_out=src/ ni/pythonpanel/v1/python_panel_service.proto`

# Testing

Expand Down
293 changes: 270 additions & 23 deletions poetry.lock

Large diffs are not rendered by default.

12 changes: 11 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ packages = [{ include = "nipanel", from = "src" }, { include = "ni", from = "src
python = "^3.9"
grpcio = {version=">=1.49.0,<2.0"}
protobuf = {version=">=4.21"}
ni-measurement-plugin-sdk = {version=">=2.3"}

[tool.poetry.group.dev.dependencies]
grpc-stubs = "^1.53"
types-protobuf = ">=4.21"

[tool.poetry.group.lint.dependencies]
bandit = { version = ">=1.7", extras = ["toml"] }
Expand Down Expand Up @@ -55,4 +60,9 @@ skips = [

[tool.pytest.ini_options]
addopts = "--doctest-modules --strict-markers"
testpaths = ["src/nipanel", "tests"]
testpaths = ["src/nipanel", "tests"]

[tool.black]
extend-exclude = '''
/src/ni/pythonpanel/v1/
'''
144 changes: 144 additions & 0 deletions src/ni/pythonpanel/v1/python_panel_service_pb2.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
"""
@generated by mypy-protobuf. Do not edit manually!
isort:skip_file
"""

import builtins
import google.protobuf.any_pb2
import google.protobuf.descriptor
import google.protobuf.message
import typing

DESCRIPTOR: google.protobuf.descriptor.FileDescriptor

@typing.final
class ConnectRequest(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor

PANEL_ID_FIELD_NUMBER: builtins.int
PANEL_URI_FIELD_NUMBER: builtins.int
panel_id: builtins.str
"""Unique ID of the panel"""
panel_uri: builtins.str
"""Absolute path of the panel's file on disk, or network path to the file"""
def __init__(
self,
*,
panel_id: builtins.str = ...,
panel_uri: builtins.str = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["panel_id", b"panel_id", "panel_uri", b"panel_uri"]) -> None: ...

global___ConnectRequest = ConnectRequest

@typing.final
class ConnectResponse(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor

def __init__(
self,
) -> None: ...

global___ConnectResponse = ConnectResponse

@typing.final
class DisconnectRequest(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor

PANEL_ID_FIELD_NUMBER: builtins.int
panel_id: builtins.str
"""Unique ID of the panel"""
def __init__(
self,
*,
panel_id: builtins.str = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["panel_id", b"panel_id"]) -> None: ...

global___DisconnectRequest = DisconnectRequest

@typing.final
class DisconnectResponse(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor

def __init__(
self,
) -> None: ...

global___DisconnectResponse = DisconnectResponse

@typing.final
class GetValueRequest(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor

PANEL_ID_FIELD_NUMBER: builtins.int
VALUE_ID_FIELD_NUMBER: builtins.int
panel_id: builtins.str
"""Unique ID of the panel"""
value_id: builtins.str
"""Unique ID of value"""
def __init__(
self,
*,
panel_id: builtins.str = ...,
value_id: builtins.str = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["panel_id", b"panel_id", "value_id", b"value_id"]) -> None: ...

global___GetValueRequest = GetValueRequest

@typing.final
class GetValueResponse(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor

VALUE_FIELD_NUMBER: builtins.int
@property
def value(self) -> google.protobuf.any_pb2.Any:
"""The value"""

def __init__(
self,
*,
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: ...

global___GetValueResponse = GetValueResponse

@typing.final
class SetValueRequest(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor

PANEL_ID_FIELD_NUMBER: builtins.int
VALUE_ID_FIELD_NUMBER: builtins.int
VALUE_FIELD_NUMBER: builtins.int
panel_id: builtins.str
"""Unique ID of the panel"""
value_id: builtins.str
"""Unique ID of the value"""
@property
def value(self) -> google.protobuf.any_pb2.Any:
"""The value"""

def __init__(
self,
*,
panel_id: builtins.str = ...,
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["panel_id", b"panel_id", "value", b"value", "value_id", b"value_id"]) -> None: ...

global___SetValueRequest = SetValueRequest

@typing.final
class SetValueResponse(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor

def __init__(
self,
) -> None: ...

global___SetValueResponse = SetValueResponse
146 changes: 146 additions & 0 deletions src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"""
@generated by mypy-protobuf. Do not edit manually!
isort:skip_file
"""

import abc
import collections.abc
import grpc
import grpc.aio
import ni.pythonpanel.v1.python_panel_service_pb2
import typing

_T = typing.TypeVar("_T")

class _MaybeAsyncIterator(collections.abc.AsyncIterator[_T], collections.abc.Iterator[_T], metaclass=abc.ABCMeta): ...

class _ServicerContext(grpc.ServicerContext, grpc.aio.ServicerContext): # type: ignore[misc, type-arg]
...

class PythonPanelServiceStub:
"""Service interface for connecting to python panels"""

def __init__(self, channel: typing.Union[grpc.Channel, grpc.aio.Channel]) -> None: ...
Connect: grpc.UnaryUnaryMultiCallable[
ni.pythonpanel.v1.python_panel_service_pb2.ConnectRequest,
ni.pythonpanel.v1.python_panel_service_pb2.ConnectResponse,
]
"""Connect to a panel and open it
Status Codes for errors:
- NOT_FOUND: the file for the panel was not found
"""

Disconnect: grpc.UnaryUnaryMultiCallable[
ni.pythonpanel.v1.python_panel_service_pb2.DisconnectRequest,
ni.pythonpanel.v1.python_panel_service_pb2.DisconnectResponse,
]
"""Disconnect from a panel (does not close the panel)
Status Codes for errors:
- NOT_FOUND: the panel with the specified id was not found
"""

GetValue: grpc.UnaryUnaryMultiCallable[
ni.pythonpanel.v1.python_panel_service_pb2.GetValueRequest,
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
"""

SetValue: grpc.UnaryUnaryMultiCallable[
ni.pythonpanel.v1.python_panel_service_pb2.SetValueRequest,
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
"""

class PythonPanelServiceAsyncStub:
"""Service interface for connecting to python panels"""

Connect: grpc.aio.UnaryUnaryMultiCallable[
ni.pythonpanel.v1.python_panel_service_pb2.ConnectRequest,
ni.pythonpanel.v1.python_panel_service_pb2.ConnectResponse,
]
"""Connect to a panel and open it
Status Codes for errors:
- NOT_FOUND: the file for the panel was not found
"""

Disconnect: grpc.aio.UnaryUnaryMultiCallable[
ni.pythonpanel.v1.python_panel_service_pb2.DisconnectRequest,
ni.pythonpanel.v1.python_panel_service_pb2.DisconnectResponse,
]
"""Disconnect from a panel (does not close the panel)
Status Codes for errors:
- NOT_FOUND: the panel with the specified id was not found
"""

GetValue: grpc.aio.UnaryUnaryMultiCallable[
ni.pythonpanel.v1.python_panel_service_pb2.GetValueRequest,
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
"""

SetValue: grpc.aio.UnaryUnaryMultiCallable[
ni.pythonpanel.v1.python_panel_service_pb2.SetValueRequest,
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
"""

class PythonPanelServiceServicer(metaclass=abc.ABCMeta):
"""Service interface for connecting to python panels"""

@abc.abstractmethod
def Connect(
self,
request: ni.pythonpanel.v1.python_panel_service_pb2.ConnectRequest,
context: _ServicerContext,
) -> typing.Union[ni.pythonpanel.v1.python_panel_service_pb2.ConnectResponse, collections.abc.Awaitable[ni.pythonpanel.v1.python_panel_service_pb2.ConnectResponse]]:
"""Connect to a panel and open it
Status Codes for errors:
- NOT_FOUND: the file for the panel was not found
"""

@abc.abstractmethod
def Disconnect(
self,
request: ni.pythonpanel.v1.python_panel_service_pb2.DisconnectRequest,
context: _ServicerContext,
) -> typing.Union[ni.pythonpanel.v1.python_panel_service_pb2.DisconnectResponse, collections.abc.Awaitable[ni.pythonpanel.v1.python_panel_service_pb2.DisconnectResponse]]:
"""Disconnect from a panel (does not close the panel)
Status Codes for errors:
- NOT_FOUND: the panel with the specified id was not found
"""

@abc.abstractmethod
def GetValue(
self,
request: ni.pythonpanel.v1.python_panel_service_pb2.GetValueRequest,
context: _ServicerContext,
) -> 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
"""

@abc.abstractmethod
def SetValue(
self,
request: ni.pythonpanel.v1.python_panel_service_pb2.SetValueRequest,
context: _ServicerContext,
) -> 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
"""

def add_PythonPanelServiceServicer_to_server(servicer: PythonPanelServiceServicer, server: typing.Union[grpc.Server, grpc.aio.Server]) -> None: ...
25 changes: 13 additions & 12 deletions src/nipanel/_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import sys
from abc import ABC, abstractmethod
from types import TracebackType
from typing import Optional, Type, TYPE_CHECKING
from typing import TYPE_CHECKING, Optional, Type

from ni.pythonpanel.v1.python_panel_service_pb2_grpc import PythonPanelServiceStub
from ni_measurement_plugin_sdk_service.discovery import DiscoveryClient

from nipanel._panel_client import PanelClient

if TYPE_CHECKING:
if sys.version_info >= (3, 11):
Expand All @@ -17,14 +19,15 @@
class Panel(ABC):
"""This class allows you to connect to a panel and specify values for its controls."""

_stub: PythonPanelServiceStub | None
_panel_client: PanelClient
_panel_id: str
_panel_uri: str

__slots__ = ["_stub", "_panel_id", "_panel_uri", "__weakref__"]
__slots__ = ["_panel_client", "_panel_id", "_panel_uri", "__weakref__"]

def __init__(self, panel_id: str, panel_uri: str) -> None:
"""Initialize the panel."""
self._panel_client = PanelClient(self._resolve_service_address)
self._panel_id = panel_id
self._panel_uri = panel_uri

Expand Down Expand Up @@ -55,13 +58,11 @@ def __exit__(

def connect(self) -> None:
"""Connect to the panel and open it."""
# TODO: AB#3095680 - Use gRPC pool management, create the _stub, and call _stub.Connect
self._resolve_service_location()
self._panel_client.connect(self._panel_id, self._panel_uri)

def disconnect(self) -> None:
"""Disconnect from the panel (does not close the panel)."""
# TODO: AB#3095680 - Use gRPC pool management, call _stub.Disconnect
pass
self._panel_client.disconnect(self._panel_id)

def get_value(self, value_id: str) -> object:
"""Get the value for a control on the panel.
Expand All @@ -72,7 +73,7 @@ def get_value(self, value_id: str) -> object:
Returns:
The value
"""
# TODO: AB#3095681 - get the Any from _stub.GetValue and convert it to the correct type
# TODO: AB#3095681 - get the Any from _client.get_value and convert it to the correct type
return "placeholder value"

def set_value(self, value_id: str, value: object) -> None:
Expand All @@ -82,10 +83,10 @@ def set_value(self, value_id: str, value: object) -> None:
value_id: The id of the value
value: The value
"""
# TODO: AB#3095681 - Convert the value to an Any and pass it to _stub.SetValue
# TODO: AB#3095681 - Convert the value to an Any and pass it to _client.set_value
pass

@abstractmethod
def _resolve_service_location(self) -> str:
"""Resolve the service location for the panel."""
def _resolve_service_address(self, discovery_client: DiscoveryClient) -> str:
"""Resolve the service address for the panel."""
raise NotImplementedError
Loading