Skip to content

Commit 21ff90f

Browse files
RSDK-9623: Add Discover Service and GetModelsFromModules to Python (#827)
Co-authored-by: Naveed Jooma <[email protected]>
1 parent 5634681 commit 21ff90f

File tree

7 files changed

+303
-0
lines changed

7 files changed

+303
-0
lines changed

src/viam/robot/client.py

+28
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,14 @@
2929
GetCloudMetadataResponse,
3030
GetMachineStatusRequest,
3131
GetMachineStatusResponse,
32+
GetModelsFromModulesRequest,
33+
GetModelsFromModulesResponse,
3234
GetOperationsRequest,
3335
GetOperationsResponse,
3436
GetVersionRequest,
3537
GetVersionResponse,
3638
LogRequest,
39+
ModuleModel,
3740
Operation,
3841
ResourceNamesRequest,
3942
ResourceNamesResponse,
@@ -771,6 +774,31 @@ async def discover_components(
771774
)
772775
return list(response.discovery)
773776

777+
#################
778+
# MODULE MODELS #
779+
#################
780+
781+
async def get_models_from_modules(
782+
self,
783+
) -> List[ModuleModel]:
784+
"""
785+
786+
Get a list of module models.
787+
788+
::
789+
# Get module models
790+
module_modles = await machine.get_models_from_modules(qs)
791+
792+
Args:
793+
794+
Returns:
795+
List[ModuleModel]: A list of discovered models.
796+
797+
"""
798+
request = GetModelsFromModulesRequest()
799+
response: GetModelsFromModulesResponse = await self._client.GetModelsFromModules(request)
800+
return list(response.models)
801+
774802
############
775803
# STOP ALL #
776804
############
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from viam.resource.registry import Registry, ResourceRegistration
2+
from viam.services.discovery.service import DiscoveryRPCService
3+
4+
from .client import DiscoveryClient
5+
from .discovery import Discovery
6+
7+
__all__ = [
8+
"DiscoveryClient",
9+
"Discovery",
10+
]
11+
12+
Registry.register_api(ResourceRegistration(Discovery, DiscoveryRPCService, lambda name, channel: DiscoveryClient(name, channel)))

src/viam/services/discovery/client.py

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from typing import Any, List, Mapping, Optional
2+
3+
from grpclib.client import Channel
4+
5+
from viam.proto.app.robot import ComponentConfig
6+
from viam.proto.common import DoCommandRequest, DoCommandResponse
7+
from viam.proto.service.discovery import (
8+
DiscoverResourcesRequest,
9+
DiscoverResourcesResponse,
10+
DiscoveryServiceStub,
11+
)
12+
from viam.resource.rpc_client_base import ReconfigurableResourceRPCClientBase
13+
from viam.utils import ValueTypes, dict_to_struct, struct_to_dict
14+
15+
from .discovery import Discovery
16+
17+
18+
class DiscoveryClient(Discovery, ReconfigurableResourceRPCClientBase):
19+
"""
20+
Connect to the Discovery service, which allows you to discover resources on a machine.
21+
"""
22+
23+
client: DiscoveryServiceStub
24+
25+
def __init__(self, name: str, channel: Channel):
26+
super().__init__(name)
27+
self.channel = channel
28+
self.client = DiscoveryServiceStub(channel)
29+
30+
async def discover_resources(
31+
self,
32+
*,
33+
extra: Optional[Mapping[str, Any]] = None,
34+
timeout: Optional[float] = None,
35+
**kwargs,
36+
) -> List[ComponentConfig]:
37+
md = kwargs.get("metadata", self.Metadata()).proto
38+
request = DiscoverResourcesRequest(
39+
name=self.name,
40+
extra=dict_to_struct(extra),
41+
)
42+
response: DiscoverResourcesResponse = await self.client.DiscoverResources(request, timeout=timeout, metadata=md)
43+
return list(response.discoveries)
44+
45+
async def do_command(
46+
self,
47+
command: Mapping[str, ValueTypes],
48+
*,
49+
timeout: Optional[float] = None,
50+
**kwargs,
51+
) -> Mapping[str, ValueTypes]:
52+
md = kwargs.get("metadata", self.Metadata()).proto
53+
request = DoCommandRequest(name=self.name, command=dict_to_struct(command))
54+
response: DoCommandResponse = await self.client.DoCommand(request, timeout=timeout, metadata=md)
55+
return struct_to_dict(response.result)
+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import abc
2+
from typing import Final, List, Mapping, Optional
3+
4+
from viam.proto.app.robot import ComponentConfig
5+
from viam.resource.types import RESOURCE_NAMESPACE_RDK, RESOURCE_TYPE_SERVICE, API
6+
from viam.utils import ValueTypes
7+
8+
from ..service_base import ServiceBase
9+
10+
11+
class Discovery(ServiceBase):
12+
"""
13+
Discovery represents a Discovery service.
14+
15+
This acts as an abstract base class for any drivers representing specific
16+
discovery implementations. This cannot be used on its own. If the ``__init__()`` function is
17+
overridden, it must call the ``super().__init__()`` function.
18+
19+
"""
20+
21+
API: Final = API( # pyright: ignore [reportIncompatibleVariableOverride]
22+
RESOURCE_NAMESPACE_RDK, RESOURCE_TYPE_SERVICE, "discovery"
23+
)
24+
25+
@abc.abstractmethod
26+
async def discover_resources(
27+
self,
28+
*,
29+
extra: Optional[Mapping[str, ValueTypes]] = None,
30+
timeout: Optional[float] = None,
31+
) -> List[ComponentConfig]:
32+
"""Get all component configs of discovered resources on a machine
33+
34+
::
35+
36+
my_discovery = DiscoveryClient.from_robot(machine, "my_discovery")
37+
38+
# Get the discovered resources
39+
result = await my_discovery.discover_resources(
40+
"my_discovery",
41+
)
42+
discoveries = result.discoveries
43+
44+
Args:
45+
name (str): The name of the discover service
46+
47+
Returns:
48+
List[ComponentConfig]: A list of ComponentConfigs that describe
49+
the components found by a discover service
50+
51+
"""
52+
...
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from grpclib.server import Stream
2+
3+
from viam.proto.common import DoCommandRequest, DoCommandResponse
4+
from viam.proto.service.discovery import (
5+
DiscoverResourcesRequest,
6+
DiscoverResourcesResponse,
7+
UnimplementedDiscoveryServiceBase,
8+
)
9+
from viam.resource.rpc_service_base import ResourceRPCServiceBase
10+
from viam.utils import dict_to_struct, struct_to_dict
11+
12+
from .discovery import Discovery
13+
14+
15+
class DiscoveryRPCService(UnimplementedDiscoveryServiceBase, ResourceRPCServiceBase):
16+
"""
17+
gRPC service for a Discovery service
18+
"""
19+
20+
RESOURCE_TYPE = Discovery
21+
22+
async def DiscoverResources(self, stream: Stream[DiscoverResourcesRequest, DiscoverResourcesResponse]) -> None:
23+
request = await stream.recv_message()
24+
assert request is not None
25+
discovery = self.get_resource(request.name)
26+
extra = struct_to_dict(request.extra)
27+
timeout = stream.deadline.time_remaining() if stream.deadline else None
28+
result = await discovery.discover_resources(
29+
extra=extra,
30+
timeout=timeout,
31+
)
32+
response = DiscoverResourcesResponse(
33+
discoveries=result,
34+
)
35+
await stream.send_message(response)
36+
37+
async def DoCommand(self, stream: Stream[DoCommandRequest, DoCommandResponse]) -> None:
38+
request = await stream.recv_message()
39+
assert request is not None
40+
discovery = self.get_resource(request.name)
41+
timeout = stream.deadline.time_remaining() if stream.deadline else None
42+
result = await discovery.do_command(struct_to_dict(request.command), timeout=timeout)
43+
await stream.send_message(DoCommandResponse(result=dict_to_struct(result)))

tests/mocks/services.py

+27
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@
266266
UnimplementedMLTrainingServiceBase,
267267
)
268268
from viam.proto.app.packages import PackageType
269+
from viam.proto.app.robot import ComponentConfig
269270
from viam.proto.common import (
270271
DoCommandRequest,
271272
DoCommandResponse,
@@ -324,6 +325,7 @@
324325
from viam.proto.service.navigation import MapType, Mode, Path, Waypoint
325326
from viam.proto.service.slam import MappingMode, SensorInfo, SensorType
326327
from viam.proto.service.vision import Classification, Detection
328+
from viam.services.discovery import Discovery
327329
from viam.services.generic import Generic as GenericService
328330
from viam.services.mlmodel import File, LabelType, Metadata, MLModel, TensorInfo
329331
from viam.services.mlmodel.utils import flat_tensors_to_ndarrays, ndarrays_to_flat_tensors
@@ -432,6 +434,31 @@ async def do_command(self, command: Mapping[str, ValueTypes], *, timeout: Option
432434
return {"cmd": command}
433435

434436

437+
class MockDiscovery(Discovery):
438+
def __init__(
439+
self,
440+
name: str,
441+
):
442+
self.extra: Optional[Mapping[str, Any]] = None
443+
self.timeout: Optional[float] = None
444+
super().__init__(name)
445+
446+
async def discover_resources(
447+
self,
448+
*,
449+
extra: Optional[Mapping[str, ValueTypes]] = None,
450+
timeout: Optional[float] = None,
451+
) -> List[ComponentConfig]:
452+
self.extra = extra
453+
self.timeout = timeout
454+
result = ComponentConfig()
455+
return [result]
456+
457+
async def do_command(self, command: Mapping[str, ValueTypes], *, timeout: Optional[float] = None) -> Mapping[str, ValueTypes]:
458+
self.timeout = timeout
459+
return {"cmd": command}
460+
461+
435462
class MockMLModel(MLModel):
436463
INT8_NDARRAY = np.array([[0, -1], [8, 8]], dtype=np.int8)
437464
INT16_NDARRAY = np.array([1, 0, 0, 69, -1, 16], dtype=np.int16)

tests/test_discovery_service.py

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import pytest
2+
from grpclib.testing import ChannelFor
3+
4+
from viam.proto.app.robot import ComponentConfig
5+
from viam.proto.common import (
6+
DoCommandRequest,
7+
DoCommandResponse,
8+
)
9+
from viam.proto.service.discovery import (
10+
DiscoverResourcesRequest,
11+
DiscoverResourcesResponse,
12+
DiscoveryServiceStub,
13+
)
14+
from viam.resource.manager import ResourceManager
15+
from viam.services.discovery import DiscoveryClient
16+
from viam.services.discovery.service import DiscoveryRPCService
17+
from viam.utils import dict_to_struct, struct_to_dict
18+
19+
from .mocks.services import MockDiscovery
20+
21+
DISCOVERIES = [ComponentConfig()]
22+
23+
24+
DISCOVERY_SERVICE_NAME = "discovery1"
25+
26+
27+
@pytest.fixture(scope="function")
28+
def discovery() -> MockDiscovery:
29+
return MockDiscovery(
30+
DISCOVERY_SERVICE_NAME,
31+
)
32+
33+
34+
@pytest.fixture(scope="function")
35+
def service(discovery: MockDiscovery) -> DiscoveryRPCService:
36+
rm = ResourceManager([discovery])
37+
return DiscoveryRPCService(rm)
38+
39+
40+
class TestDiscovery:
41+
async def test_discover_resources(self, discovery: MockDiscovery):
42+
extra = {"foo": "discovery"}
43+
response = await discovery.discover_resources(extra=extra)
44+
assert discovery.extra == extra
45+
assert response == DISCOVERIES
46+
47+
async def test_do(self, discovery: MockDiscovery):
48+
command = {"command": "args"}
49+
response = await discovery.do_command(command)
50+
assert response["cmd"] == command
51+
52+
53+
class TestService:
54+
async def test_discover_resources(self, discovery: MockDiscovery, service: DiscoveryRPCService):
55+
async with ChannelFor([service]) as channel:
56+
client = DiscoveryServiceStub(channel)
57+
extra = {"cmd": "discovery"}
58+
request = DiscoverResourcesRequest(name=discovery.name, extra=dict_to_struct(extra))
59+
response: DiscoverResourcesResponse = await client.DiscoverResources(request)
60+
assert discovery.extra == extra
61+
assert response.discoveries == DISCOVERIES
62+
63+
async def test_do(self, discovery: MockDiscovery, service: DiscoveryRPCService):
64+
async with ChannelFor([service]) as channel:
65+
client = DiscoveryServiceStub(channel)
66+
command = {"command": "args"}
67+
request = DoCommandRequest(name=discovery.name, command=dict_to_struct(command))
68+
response: DoCommandResponse = await client.DoCommand(request)
69+
assert struct_to_dict(response.result)["cmd"] == command
70+
71+
72+
class TestClient:
73+
async def test_discover_resources(self, discovery: MockDiscovery, service: DiscoveryRPCService):
74+
async with ChannelFor([service]) as channel:
75+
client = DiscoveryClient(DISCOVERY_SERVICE_NAME, channel)
76+
extra = {"foo": "discovery"}
77+
response = await client.discover_resources(name=DISCOVERY_SERVICE_NAME, extra=extra)
78+
assert response == DISCOVERIES
79+
assert discovery.extra == extra
80+
81+
async def test_do(self, service: DiscoveryRPCService):
82+
async with ChannelFor([service]) as channel:
83+
client = DiscoveryClient(DISCOVERY_SERVICE_NAME, channel)
84+
command = {"command": "args"}
85+
response = await client.do_command(command)
86+
assert response["cmd"] == command

0 commit comments

Comments
 (0)