Skip to content

Commit 09e7d9b

Browse files
authored
RSDK-6375 Add generic service wrappers (#526)
1 parent 75ab8e5 commit 09e7d9b

File tree

9 files changed

+234
-8
lines changed

9 files changed

+234
-8
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import viam.gen.service.generic.v1.generic_pb2 # Need this import for Generic service descriptors to resolve
2+
from viam.resource.registry import Registry, ResourceRegistration
3+
4+
from .client import GenericClient
5+
from .generic import Generic
6+
from .service import GenericRPCService
7+
8+
__all__ = [
9+
"Generic",
10+
]
11+
12+
Registry.register_subtype(
13+
ResourceRegistration(
14+
Generic,
15+
GenericRPCService,
16+
lambda name, channel: GenericClient(name, channel),
17+
)
18+
)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from typing import Any, Mapping, Optional
2+
3+
from grpclib import GRPCError, Status
4+
from grpclib.client import Channel
5+
6+
from viam.proto.common import DoCommandRequest, DoCommandResponse
7+
from viam.proto.service.generic import GenericServiceStub
8+
from viam.resource.rpc_client_base import ReconfigurableResourceRPCClientBase
9+
from viam.utils import ValueTypes, dict_to_struct, struct_to_dict
10+
11+
from .generic import Generic
12+
13+
14+
class GenericClient(Generic, ReconfigurableResourceRPCClientBase):
15+
"""
16+
gRPC client for the Generic service.
17+
"""
18+
19+
def __init__(self, name: str, channel: Channel):
20+
self.client = GenericServiceStub(channel)
21+
super().__init__(name)
22+
23+
async def do_command(
24+
self,
25+
command: Mapping[str, Any],
26+
*,
27+
timeout: Optional[float] = None,
28+
**__,
29+
) -> Mapping[str, Any]:
30+
request = DoCommandRequest(name=self.name, command=dict_to_struct(command))
31+
try:
32+
response: DoCommandResponse = await self.client.DoCommand(request, timeout=timeout)
33+
except GRPCError as e:
34+
if e.status == Status.UNIMPLEMENTED:
35+
raise NotImplementedError()
36+
raise e
37+
38+
return struct_to_dict(response.result)
39+
40+
41+
async def do_command(
42+
channel: Channel, name: str, command: Mapping[str, ValueTypes], *, timeout: Optional[float] = None
43+
) -> Mapping[str, ValueTypes]:
44+
"""Convenience method to allow service clients to execute ``do_command`` functions
45+
46+
Args:
47+
channel (Channel): A gRPC channel
48+
name (str): The name of the component
49+
command (Dict[str, Any]): The command to execute
50+
51+
Returns:
52+
Dict[str, Any]: The result of the executed command
53+
"""
54+
client = GenericClient(name, channel)
55+
return await client.do_command(command, timeout=timeout)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from typing import Final
2+
3+
from viam.resource.types import RESOURCE_NAMESPACE_RDK, RESOURCE_TYPE_SERVICE, Subtype
4+
5+
from ..service_base import ServiceBase
6+
7+
8+
class Generic(ServiceBase):
9+
"""
10+
Generic service, which represents any type of service that can execute arbitrary commands
11+
12+
This acts as an abstract base class for any drivers representing generic services.
13+
This cannot be used on its own. If the ``__init__()`` function is overridden, it must call the ``super().__init__()`` function.
14+
15+
To create a Generic service (an arbitrary service that can process commands), this ``Generic`` service should be subclassed
16+
and the ``do_command`` function implemented.
17+
18+
Example::
19+
20+
class ComplexService(Generic):
21+
22+
async def do_command(
23+
self,
24+
command: Mapping[str, ValueTypes],
25+
*,
26+
timeout: Optional[float] = None,
27+
**kwargs
28+
) -> Mapping[str, ValueTypes]:
29+
result = {key: False for key in command.keys()}
30+
for (name, args) in command.items():
31+
if name == 'set_val':
32+
self.set_val(*args)
33+
result[name] = True
34+
if name == 'get_val':
35+
result[name] = self.val
36+
if name == 'complex_command':
37+
self.complex_command(*args)
38+
result[name] = True
39+
return result
40+
41+
def set_val(self, val: int):
42+
self.val = val
43+
44+
def complex_command(self, arg1, arg2, arg3):
45+
...
46+
47+
To execute commands, simply call the ``do_command`` function with the appropriate parameters.
48+
::
49+
50+
await service.do_command({'set_val': 10})
51+
service.val # 10
52+
await service.do_command({'set_val': 5})
53+
service.val # 5
54+
"""
55+
56+
SUBTYPE: Final = Subtype( # pyright: ignore [reportIncompatibleVariableOverride]
57+
RESOURCE_NAMESPACE_RDK, RESOURCE_TYPE_SERVICE, "generic"
58+
)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from grpclib import GRPCError, Status
2+
from grpclib.server import Stream
3+
4+
from viam.services.service_base import ServiceBase
5+
from viam.proto.common import DoCommandRequest, DoCommandResponse
6+
from viam.proto.service.generic import GenericServiceBase
7+
from viam.resource.rpc_service_base import ResourceRPCServiceBase
8+
from viam.utils import dict_to_struct, struct_to_dict
9+
10+
11+
class GenericRPCService(GenericServiceBase, ResourceRPCServiceBase):
12+
"""
13+
gRPC Service for a Generic service
14+
"""
15+
16+
RESOURCE_TYPE = ServiceBase
17+
18+
async def DoCommand(self, stream: Stream[DoCommandRequest, DoCommandResponse]) -> None:
19+
request = await stream.recv_message()
20+
assert request is not None
21+
name = request.name
22+
service = self.get_resource(name)
23+
try:
24+
timeout = stream.deadline.time_remaining() if stream.deadline else None
25+
result = await service.do_command(struct_to_dict(request.command), timeout=timeout, metadata=stream.metadata)
26+
except NotImplementedError:
27+
raise GRPCError(Status.UNIMPLEMENTED, f"``DO`` command is unimplemented for service named: {name}")
28+
response = DoCommandResponse(result=dict_to_struct(result))
29+
await stream.send_message(response)

tests/mocks/components.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,7 @@ async def do_command(self, command: Mapping[str, ValueTypes], *, timeout: Option
549549
return {"command": command}
550550

551551

552-
class MockGeneric(GenericComponent):
552+
class MockGenericComponent(GenericComponent):
553553
timeout: Optional[float] = None
554554
geometries = GEOMETRIES
555555

tests/mocks/services.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@
287287
)
288288
from viam.proto.service.slam import MappingMode
289289
from viam.proto.service.vision import Classification, Detection
290+
from viam.services.generic import Generic as GenericService
290291
from viam.services.mlmodel import File, LabelType, Metadata, MLModel, TensorInfo
291292
from viam.services.mlmodel.utils import flat_tensors_to_ndarrays, ndarrays_to_flat_tensors
292293
from viam.services.navigation import Navigation
@@ -1390,3 +1391,11 @@ async def DeleteRegistryItem(self, stream: Stream[DeleteRegistryItemRequest, Del
13901391

13911392
async def GetRegistryItem(self, stream: Stream[GetRegistryItemRequest, GetRegistryItemResponse]) -> None:
13921393
raise NotImplementedError()
1394+
1395+
1396+
class MockGenericService(GenericService):
1397+
timeout: Optional[float] = None
1398+
1399+
async def do_command(self, command: Mapping[str, ValueTypes], *, timeout: Optional[float] = None, **kwargs) -> Mapping[str, ValueTypes]:
1400+
self.timeout = timeout
1401+
return {key: True for key in command.keys()}

tests/test_generic.py renamed to tests/test_generic_component.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
from viam.utils import dict_to_struct, struct_to_dict
99

1010
from . import loose_approx
11-
from .mocks.components import GEOMETRIES, MockGeneric
11+
from .mocks.components import GEOMETRIES, MockGenericComponent
1212

1313

14-
class TestGeneric:
15-
generic = MockGeneric(name="generic")
14+
class TestGenericComponent:
15+
generic = MockGenericComponent(name="generic")
1616

1717
@pytest.mark.asyncio
1818
async def test_do(self):
@@ -30,7 +30,7 @@ class TestService:
3030
@classmethod
3131
def setup_class(cls):
3232
cls.name = "generic"
33-
cls.generic = MockGeneric(name=cls.name)
33+
cls.generic = MockGenericComponent(name=cls.name)
3434
cls.manager = ResourceManager([cls.generic])
3535
cls.service = GenericRPCService(cls.manager)
3636

@@ -57,7 +57,7 @@ class TestClient:
5757
@classmethod
5858
def setup_class(cls):
5959
cls.name = "generic"
60-
cls.generic = MockGeneric(name=cls.name)
60+
cls.generic = MockGenericComponent(name=cls.name)
6161
cls.manager = ResourceManager([cls.generic])
6262
cls.service = GenericRPCService(cls.manager)
6363

tests/test_generic_service.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import pytest
2+
from grpclib.testing import ChannelFor
3+
4+
from viam.components.generic import GenericClient, GenericRPCService
5+
from viam.proto.common import DoCommandRequest, DoCommandResponse
6+
from viam.proto.component.generic import GenericServiceStub
7+
from viam.resource.manager import ResourceManager
8+
from viam.utils import dict_to_struct, struct_to_dict
9+
10+
from . import loose_approx
11+
from .mocks.services import MockGenericService
12+
13+
14+
class TestGenericService:
15+
generic = MockGenericService(name="generic")
16+
17+
@pytest.mark.asyncio
18+
async def test_do(self):
19+
result = await self.generic.do_command({"command": "args"}, timeout=1.82)
20+
assert result == {"command": True}
21+
assert self.generic.timeout == loose_approx(1.82)
22+
23+
24+
class TestService:
25+
@classmethod
26+
def setup_class(cls):
27+
cls.name = "generic"
28+
cls.generic = MockGenericService(name=cls.name)
29+
cls.manager = ResourceManager([cls.generic])
30+
cls.service = GenericRPCService(cls.manager)
31+
32+
@pytest.mark.asyncio
33+
async def test_do(self):
34+
async with ChannelFor([self.service]) as channel:
35+
client = GenericServiceStub(channel)
36+
request = DoCommandRequest(name=self.name, command=dict_to_struct({"command": "args"}))
37+
response: DoCommandResponse = await client.DoCommand(request, timeout=4.4)
38+
result = struct_to_dict(response.result)
39+
assert result == {"command": True}
40+
assert self.generic.timeout == loose_approx(4.4)
41+
42+
43+
class TestClient:
44+
@classmethod
45+
def setup_class(cls):
46+
cls.name = "generic"
47+
cls.generic = MockGenericService(name=cls.name)
48+
cls.manager = ResourceManager([cls.generic])
49+
cls.service = GenericRPCService(cls.manager)
50+
51+
@pytest.mark.asyncio
52+
async def test_do(self):
53+
async with ChannelFor([self.service]) as channel:
54+
client = GenericClient(self.name, channel)
55+
result = await client.do_command({"command": "args"}, timeout=7.86)
56+
assert result == {"command": True}
57+
assert self.generic.timeout == loose_approx(7.86)

tests/test_motor.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,9 @@ async def test_set_power(self, motor: MockMotor, service: MotorRPCService):
156156
async with ChannelFor([service]) as channel:
157157
client = MotorServiceStub(channel)
158158
request = SetPowerRequest(name=motor.name, power_pct=13)
159-
await client.SetPower(request, timeout=1.23)
159+
await client.SetPower(request, timeout=2.34)
160160
assert motor.power == 13
161-
assert motor.timeout == loose_approx(1.23)
161+
assert motor.timeout == loose_approx(2.34)
162162

163163
@pytest.mark.asyncio
164164
async def test_get_position(self, motor: MockMotor, service: MotorRPCService):

0 commit comments

Comments
 (0)