Skip to content

Commit

Permalink
Merge branch 'releases/2.1' into users/jothees/cherry-pick-resolve-se…
Browse files Browse the repository at this point in the history
…rvice-updation-in-generator
  • Loading branch information
Jotheeswaran-Nandagopal committed Sep 27, 2024
2 parents 07b53d2 + 4acfa3c commit 5b04005
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import logging
import threading
from typing import Optional, Sequence
from typing import Optional, Sequence, Tuple

import grpc
from deprecation import deprecated
Expand Down Expand Up @@ -240,10 +240,45 @@ def resolve_service(

response = self._get_stub().ResolveService(request)

return ServiceLocation(
location=response.location,
insecure_port=response.insecure_port,
ssl_authenticated_port=response.ssl_authenticated_port,
return ServiceLocation._from_grpc(response)

def resolve_service_with_information(
self,
provided_interface: str,
service_class: str = "",
deployment_target: str = "",
version: str = "",
) -> Tuple[ServiceLocation, ServiceInfo]:
"""Resolve the location of a service along with its information.
Given a description of a service, returns information for the service in addition to
the location of the service. If necessary, the service will be started by the discovery
service if it has not already been started.
Args:
provided_interface: The gRPC full name of the service.
service_class: The service "class" that should be matched. If the value is not
specified and there is more than one matching service registered, an error
is returned.
deployment_target: The deployment target from which the service should be resolved.
version: The version of the service to resolve. If not specified, the latest version
will be resolved.
Returns:
A tuple containing the service location and service information.
"""
request = discovery_service_pb2.ResolveServiceWithInformationRequest(
provided_interface=provided_interface,
service_class=service_class,
deployment_target=deployment_target,
version=version,
)

response = self._get_stub().ResolveServiceWithInformation(request)

return (
ServiceLocation._from_grpc(response.service_location),
ServiceInfo._from_grpc(response.service_descriptor),
)

def enumerate_services(self, provided_interface: str) -> Sequence[ServiceInfo]:
Expand All @@ -261,14 +296,4 @@ def enumerate_services(self, provided_interface: str) -> Sequence[ServiceInfo]:

response = self._get_stub().EnumerateServices(request)

return [
ServiceInfo(
service_class=service.service_class,
description_url=service.description_url,
provided_interfaces=list(service.provided_interfaces),
annotations=dict(service.annotations),
display_name=service.display_name,
versions=list(service.versions),
)
for service in response.available_services
]
return [ServiceInfo._from_grpc(service) for service in response.available_services]
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
"""Data types for the NI Discovery Service."""

from __future__ import annotations

import typing

from ni_measurement_plugin_sdk_service._internal.stubs.ni.measurementlink.discovery.v1 import (
discovery_service_pb2,
)


class ServiceLocation(typing.NamedTuple):
"""Represents the location of a service."""
Expand All @@ -19,3 +25,11 @@ def insecure_address(self) -> str:
def ssl_authenticated_address(self) -> str:
"""Get the service's SSL-authenticated address in the format host:port."""
return f"{self.location}:{self.ssl_authenticated_port}"

@classmethod
def _from_grpc(cls, other: discovery_service_pb2.ServiceLocation) -> ServiceLocation:
return ServiceLocation(
location=other.location,
insecure_port=other.insecure_port,
ssl_authenticated_port=other.ssl_authenticated_port,
)
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
from pathlib import Path
from typing import Dict, List, NamedTuple

from ni_measurement_plugin_sdk_service._internal.stubs.ni.measurementlink.discovery.v1 import (
discovery_service_pb2,
)


class MeasurementInfo(NamedTuple):
"""A named tuple providing information about a measurement."""
Expand Down Expand Up @@ -65,6 +69,17 @@ class ServiceInfo(NamedTuple):
"""The list of versions associated with this service in
the form major.minor.build[.revision] (e.g. 1.0.0)."""

@classmethod
def _from_grpc(cls, other: discovery_service_pb2.ServiceDescriptor) -> ServiceInfo:
return ServiceInfo(
service_class=other.service_class,
description_url=other.description_url,
provided_interfaces=list(other.provided_interfaces),
annotations=dict(other.annotations),
display_name=other.display_name,
versions=list(other.versions),
)


class TypeSpecialization(enum.Enum):
"""Enum that represents the type specializations for measurement parameters."""
Expand Down
62 changes: 62 additions & 0 deletions packages/service/tests/unit/test_discovery_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
RegisterServiceRequest,
RegisterServiceResponse,
ResolveServiceRequest,
ResolveServiceWithInformationRequest,
ResolveServiceWithInformationResponse,
ServiceDescriptor as GrpcServiceDescriptor,
ServiceLocation as GrpcServiceLocation,
UnregisterServiceRequest,
Expand Down Expand Up @@ -395,6 +397,65 @@ def test___no_registered_measurements___enumerate_services___returns_empty_list(
assert not available_measurements


@pytest.mark.parametrize("programming_language", ["Python", "LabVIEW"])
def test___registered_measurements___resolve_service_with_information___sends_request(
discovery_client: DiscoveryClient, discovery_service_stub: Mock, programming_language: str
):
expected_service_info = copy.deepcopy(_TEST_SERVICE_INFO)
expected_service_info.annotations[SERVICE_PROGRAMMINGLANGUAGE_KEY] = programming_language
discovery_service_stub.ResolveServiceWithInformation.return_value = (
ResolveServiceWithInformationResponse(
service_location=GrpcServiceLocation(
location=_TEST_SERVICE_LOCATION.location,
insecure_port=_TEST_SERVICE_LOCATION.insecure_port,
ssl_authenticated_port=_TEST_SERVICE_LOCATION.ssl_authenticated_port,
),
service_descriptor=GrpcServiceDescriptor(
display_name=expected_service_info.display_name,
description_url=expected_service_info.description_url,
provided_interfaces=expected_service_info.provided_interfaces,
annotations=expected_service_info.annotations,
service_class=expected_service_info.service_class,
versions=expected_service_info.versions,
),
)
)

service_location, service_info = discovery_client.resolve_service_with_information(
provided_interface=_TEST_SERVICE_INFO.provided_interfaces[0],
service_class=_TEST_SERVICE_INFO.service_class,
version=_TEST_SERVICE_INFO.versions[0],
)

discovery_service_stub.ResolveServiceWithInformation.assert_called_once()
request: ResolveServiceWithInformationRequest = (
discovery_service_stub.ResolveServiceWithInformation.call_args.args[0]
)
assert _TEST_SERVICE_INFO.provided_interfaces[0] == request.provided_interface
assert _TEST_SERVICE_INFO.service_class == request.service_class
assert _TEST_SERVICE_INFO.versions[0] == request.version
_assert_service_location_equal(_TEST_SERVICE_LOCATION, service_location)
_assert_service_info_equal(expected_service_info, service_info)


def test___no_registered_measurements___resolve_service_with_information___raises_not_found_error(
discovery_client: DiscoveryClient, discovery_service_stub: Mock
):
discovery_service_stub.ResolveServiceWithInformation.side_effect = FakeRpcError(
grpc.StatusCode.NOT_FOUND, details="Service not found"
)

with pytest.raises(grpc.RpcError) as exc_info:
_ = discovery_client.resolve_service_with_information(
provided_interface=_TEST_SERVICE_INFO.provided_interfaces[0],
service_class=_TEST_SERVICE_INFO.service_class,
version=_TEST_SERVICE_INFO.versions[0],
)

discovery_service_stub.ResolveServiceWithInformation.assert_called_once()
assert exc_info.value.code() == grpc.StatusCode.NOT_FOUND


@pytest.fixture(scope="module")
def subprocess_popen_kwargs() -> Dict[str, Any]:
kwargs: Dict[str, Any] = {}
Expand Down Expand Up @@ -423,6 +484,7 @@ def discovery_service_stub(mocker: MockerFixture) -> Mock:
stub.UnregisterService = mocker.create_autospec(grpc.UnaryUnaryMultiCallable)
stub.EnumerateServices = mocker.create_autospec(grpc.UnaryUnaryMultiCallable)
stub.ResolveService = mocker.create_autospec(grpc.UnaryUnaryMultiCallable)
stub.ResolveServiceWithInformation = mocker.create_autospec(grpc.UnaryUnaryMultiCallable)
return stub


Expand Down

0 comments on commit 5b04005

Please sign in to comment.