Skip to content

Commit 1cd58d8

Browse files
Rsdk 2071 support modular validation and implicit dependencies (#244)
1 parent ff0803a commit 1cd58d8

File tree

13 files changed

+216
-38
lines changed

13 files changed

+216
-38
lines changed

examples/module/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ The definition of the new resources are in the `src` directory. Within this dire
1313

1414
The `proto` directory contains the `gizmo.proto` and `summation.proto` definitions of all the message types and calls that can be made to the Gizmo component and Summation service. It also has the compiled python output of the protobuf definition.
1515

16-
The `gizmo` directory contains all the necessary definitions for creating a custom `Gizmo` component type. The `api.py` file defines what a `Gizmo` can do (mirroring the `proto` definition), implements the gRPC `GizmoService` for receiving calls, and the gRPC `GizmoClient` for making calls. See the [API docs](https://docs.viam.com/program/extend/modular-resources/#apis) for more info. The `my_gizmo.py` file in contains the unique implementation of a `Gizmo`. This is defined as a specific `Model`. See the [Model docs](https://docs.viam.com/program/extend/modular-resources/#models) for more info.
16+
The `gizmo` directory contains all the necessary definitions for creating a custom `Gizmo` component type. The `api.py` file defines what a `Gizmo` can do (mirroring the `proto` definition), implements the gRPC `GizmoService` for receiving calls, and the gRPC `GizmoClient` for making calls. See the [API docs](https://docs.viam.com/program/extend/modular-resources/#apis) for more info. The `my_gizmo.py` file in contains the unique implementation of a `Gizmo`. This is defined as a specific `Model`. See the [Model docs](https://docs.viam.com/program/extend/modular-resources/#models) for more info. This implementation uses the `validate_config` function to specify an implicit dependency on a `motor` by default and throw an error if there is an `invalid` attribute.
1717

1818
Similarly, the `summation` directory contains the analogous definitions for the `Summation` service type. The files in this directory mirror the files in the `gizmo` directory.
1919

@@ -38,7 +38,8 @@ An example configuration for a Gizmo component and a Summation service could loo
3838
"namespace": "acme",
3939
"model": "acme:demo:mygizmo",
4040
"attributes": {
41-
"arg1": "arg1"
41+
"arg1": "arg1",
42+
"motor": "motor1"
4243
},
4344
"depends_on": []
4445
}

examples/module/src/gizmo/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
This file registers the Gizmo subtype with the Viam Registry, as well as the specific MyGizmo model.
33
"""
44

5-
from viam.resource.registry import ResourceRegistration, Registry
5+
from viam.components.motor import * # noqa: F403 Need to import motor so the component registers itself
6+
from viam.resource.registry import Registry, ResourceCreatorRegistration, ResourceRegistration
67

78
from .api import Gizmo, GizmoClient, GizmoService
89
from .my_gizmo import MyGizmo
910

1011
Registry.register_subtype(ResourceRegistration(Gizmo, GizmoService, lambda name, channel: GizmoClient(name, channel)))
1112

12-
Registry.register_resource_creator(Gizmo.SUBTYPE, MyGizmo.MODEL, MyGizmo.new)
13+
Registry.register_resource_creator(Gizmo.SUBTYPE, MyGizmo.MODEL, ResourceCreatorRegistration(MyGizmo.new, MyGizmo.validate_config))

examples/module/src/gizmo/my_gizmo.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,21 @@ def new(cls, config: ComponentConfig, dependencies: Mapping[ResourceName, Resour
2828
gizmo.my_arg = config.attributes.fields["arg1"].string_value
2929
return gizmo
3030

31+
@classmethod
32+
def validate_config(cls, config: ComponentConfig) -> Sequence[str]:
33+
# Custom validation can be done by specifiying a validate function like this one. Validate functions
34+
# can raise errors that will be returned to the parent through gRPC. Validate functions can
35+
# also return a sequence of strings representing the implicit dependencies of the resource.
36+
if "invalid" in config.attributes.fields:
37+
raise Exception(f"'invalid' attribute not allowed for model {cls.SUBTYPE}:{cls.MODEL}")
38+
arg1 = config.attributes.fields["arg1"].string_value
39+
if arg1 == "":
40+
raise Exception("A arg1 attribute is required for Gizmo component.")
41+
motor = [config.attributes.fields["motor"].string_value]
42+
if motor == [""]:
43+
raise Exception("A motor is required for Gizmo component.")
44+
return motor
45+
3146
async def do_one(self, arg1: str, **kwargs) -> bool:
3247
return arg1 == self.my_arg
3348

examples/module/src/summation/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
This file registers the Summation subtype with the Viam Registry, as well as the specific MySummation model.
33
"""
44

5-
from viam.resource.registry import Registry, ResourceRegistration
5+
from viam.resource.registry import Registry, ResourceCreatorRegistration, ResourceRegistration
66

77
from .api import SummationClient, SummationRPCService, SummationService
88
from .my_summation import MySummationService
99

1010
Registry.register_subtype(ResourceRegistration(SummationService, SummationRPCService, lambda name, channel: SummationClient(name, channel)))
1111

12-
Registry.register_resource_creator(SummationService.SUBTYPE, MySummationService.MODEL, MySummationService.new)
12+
Registry.register_resource_creator(SummationService.SUBTYPE, MySummationService.MODEL, ResourceCreatorRegistration(MySummationService.new))

src/viam/errors.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,13 @@ class NotSupportedError(ViamGRPCError):
8383
def __init__(self, message: str):
8484
self.message = message
8585
self.grpc_code = Status.UNIMPLEMENTED
86+
87+
88+
class ValidationError(ViamGRPCError):
89+
"""
90+
Exception raised when there is an error during module validation
91+
"""
92+
93+
def __init__(self, message: str):
94+
self.message = message
95+
self.grpc_code = Status.INVALID_ARGUMENT

src/viam/module/module.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from viam import logging
88
from viam.components.component_base import ComponentBase
9-
from viam.errors import ResourceNotFoundError
9+
from viam.errors import ResourceNotFoundError, ValidationError
1010
from viam.proto.app.robot import ComponentConfig
1111
from viam.proto.module import (
1212
AddResourceRequest,
@@ -16,6 +16,8 @@
1616
ReadyResponse,
1717
ReconfigureResourceRequest,
1818
RemoveResourceRequest,
19+
ValidateConfigRequest,
20+
ValidateConfigResponse,
1921
)
2022
from viam.proto.robot import ResourceRPCSubtype
2123
from viam.resource.base import ResourceBase
@@ -179,3 +181,14 @@ def add_model_from_registry(self, subtype: Subtype, model: Model):
179181
Registry.lookup_resource_creator(subtype, model)
180182
except ResourceNotFoundError:
181183
raise ValueError(f"Cannot add model because it has not been registered. Subtype: {subtype}. Model: {model}")
184+
185+
async def validate_config(self, request: ValidateConfigRequest) -> ValidateConfigResponse:
186+
config: ComponentConfig = request.config
187+
subtype = Subtype.from_string(config.api)
188+
model = Model.from_string(config.model)
189+
validator = Registry.lookup_validator(subtype, model)
190+
try:
191+
dependencies = validator(config)
192+
return ValidateConfigResponse(dependencies=dependencies)
193+
except Exception as e:
194+
raise ValidationError(f"{type(Exception)}: {e}").grpc_error

src/viam/module/service.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from grpclib.server import Stream
44

5-
from viam.errors import MethodNotImplementedError
65
from viam.proto.module import (
76
AddResourceRequest,
87
AddResourceResponse,
@@ -52,4 +51,8 @@ async def Ready(self, stream: Stream[ReadyRequest, ReadyResponse]) -> None:
5251
await stream.send_message(response)
5352

5453
async def ValidateConfig(self, stream: Stream[ValidateConfigRequest, ValidateConfigResponse]) -> None:
55-
raise MethodNotImplementedError("ValidateConfig").grpc_error
54+
request = await stream.recv_message()
55+
assert request is not None
56+
response = await self._module.validate_config(request)
57+
if response is not None:
58+
await stream.send_message(response)

src/viam/resource/registry.py

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@
1616
from google.protobuf.struct_pb2 import Struct
1717
from grpclib.client import Channel
1818

19-
from viam.errors import DuplicateResourceError, ResourceNotFoundError
19+
from viam.errors import DuplicateResourceError, ResourceNotFoundError, ValidationError
2020
from viam.proto.robot import Status
2121

2222
from .base import ResourceBase
2323

2424
if TYPE_CHECKING:
2525
from .rpc_service_base import ResourceRPCServiceBase
26-
from .types import Model, ResourceCreator, Subtype
26+
from .types import Model, ResourceCreator, Subtype, Validator
2727

2828
Resource = TypeVar("Resource", bound=ResourceBase)
2929

@@ -32,6 +32,25 @@ async def default_create_status(resource: ResourceBase) -> Status:
3232
return Status(name=resource.get_resource_name(resource.name), status=Struct())
3333

3434

35+
@dataclass
36+
class ResourceCreatorRegistration:
37+
"""An object representing a resource creator to be registered.
38+
39+
If creating a custom Resource creator, you should register the creator by creating a ``ResourceCreatorRegistration`` object and
40+
registering it to the ``Registry``.
41+
"""
42+
43+
creator: "ResourceCreator"
44+
"""A function that can create a resource given a mapping of dependencies (``ResourceName`` to ``ResourceBase``
45+
"""
46+
47+
validator: "Validator" = lambda x: []
48+
"""A function that can validate a resource and return implicit dependencies.
49+
50+
If called without a validator function, default to a function returning an empty Sequence
51+
"""
52+
53+
3554
@dataclass
3655
class ResourceRegistration(Generic[Resource]):
3756
"""An object representing a resource to be registered.
@@ -75,7 +94,7 @@ class Registry:
7594
"""
7695

7796
_SUBTYPES: ClassVar[Dict["Subtype", ResourceRegistration]] = {}
78-
_RESOURCES: ClassVar[Dict[str, "ResourceCreator"]] = {}
97+
_RESOURCES: ClassVar[Dict[str, ResourceCreatorRegistration]] = {}
7998
_lock: ClassVar[Lock] = Lock()
8099

81100
@classmethod
@@ -87,30 +106,39 @@ def register_subtype(cls, registration: ResourceRegistration[Resource]):
87106
88107
Raises:
89108
DuplicateResourceError: Raised if the Subtype to register is already in the registry
109+
ValidationError: Raised if registration is missing any necessary parameters
90110
"""
91111
with cls._lock:
92112
if registration.resource_type.SUBTYPE in cls._SUBTYPES:
93113
raise DuplicateResourceError(str(registration.resource_type.SUBTYPE))
94-
cls._SUBTYPES[registration.resource_type.SUBTYPE] = registration
114+
115+
if registration.resource_type and registration.rpc_service and registration.create_rpc_client:
116+
cls._SUBTYPES[registration.resource_type.SUBTYPE] = registration
117+
else:
118+
raise ValidationError("Passed resource registration does not have correct parameters")
95119

96120
@classmethod
97-
def register_resource_creator(cls, subtype: "Subtype", model: "Model", creator: "ResourceCreator"):
98-
"""Register a specific ``Model`` for the specific resource ``Subtype`` with the Registry
121+
def register_resource_creator(cls, subtype: "Subtype", model: "Model", registration: ResourceCreatorRegistration):
122+
"""Register a specific ``Model`` and validator function for the specific resource ``Subtype`` with the Registry
99123
100124
Args:
101125
subtype (Subtype): The Subtype of the resource
102126
model (Model): The Model of the resource
103-
creator (ResourceCreator): A function that can create a resource given a mapping of dependencies (``ResourceName`` to
104-
``ResourceBase``).
127+
registration (ResourceCreatorRegistration): The registration functions of the model
105128
106129
Raises:
107130
DuplicateResourceError: Raised if the Subtype and Model pairing is already registered
131+
ValidationError: Raised if registration does not have creator
108132
"""
109133
key = f"{subtype}/{model}"
110134
with cls._lock:
111135
if key in cls._RESOURCES:
112136
raise DuplicateResourceError(key)
113-
cls._RESOURCES[key] = creator
137+
138+
if registration.creator:
139+
cls._RESOURCES[key] = registration
140+
else:
141+
raise ValidationError("A creator function was not provided")
114142

115143
@classmethod
116144
def lookup_subtype(cls, subtype: "Subtype") -> ResourceRegistration:
@@ -147,10 +175,28 @@ def lookup_resource_creator(cls, subtype: "Subtype", model: "Model") -> "Resourc
147175
"""
148176
with cls._lock:
149177
try:
150-
return cls._RESOURCES[f"{subtype}/{model}"]
178+
return cls._RESOURCES[f"{subtype}/{model}"].creator
151179
except KeyError:
152180
raise ResourceNotFoundError(subtype.resource_type, subtype.resource_subtype)
153181

182+
@classmethod
183+
def lookup_validator(cls, subtype: "Subtype", model: "Model") -> "Validator":
184+
"""Lookup and retrieve a registered validator function by its subtype and model. If there is none, return None
185+
186+
Args:
187+
subtype (Subtype): The Subtype of the resource
188+
model (Model): The Model of the resource
189+
190+
Returns:
191+
Validator: The function to validate the resource
192+
"""
193+
try:
194+
return cls._RESOURCES[f"{subtype}/{model}"].validator
195+
except AttributeError:
196+
return lambda x: []
197+
except KeyError:
198+
raise ResourceNotFoundError(subtype.resource_type, subtype.resource_subtype)
199+
154200
@classmethod
155201
def REGISTERED_SUBTYPES(cls) -> Mapping["Subtype", ResourceRegistration]:
156202
"""The dictionary of all registered resources
@@ -164,13 +210,13 @@ def REGISTERED_SUBTYPES(cls) -> Mapping["Subtype", ResourceRegistration]:
164210
return cls._SUBTYPES.copy()
165211

166212
@classmethod
167-
def REGISTERED_RESOURCE_CREATORS(cls) -> Mapping[str, "ResourceCreator"]:
213+
def REGISTERED_RESOURCE_CREATORS(cls) -> Mapping[str, "ResourceCreatorRegistration"]:
168214
"""The dictionary of all registered resources
169215
- Key: subtype/model
170-
- Value: The ResourceCreator for the resource
216+
- Value: The ResourceCreatorRegistration for the resource
171217
172218
Returns:
173-
Mapping[str, ResourceCreator]: All registered resources
219+
Mapping[str, ResourceCreatorRegistration]: All registered resources
174220
"""
175221
with cls._lock:
176222
return cls._RESOURCES.copy()

src/viam/resource/types.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import re
22
import sys
3-
from typing import TYPE_CHECKING, Callable, ClassVar, Mapping
3+
from typing import TYPE_CHECKING, Callable, ClassVar, Mapping, Sequence
44

55
if sys.version_info >= (3, 10):
66
from typing import TypeAlias
@@ -197,3 +197,4 @@ def resource_name_from_string(string: str) -> ResourceName:
197197

198198

199199
ResourceCreator: TypeAlias = Callable[[ComponentConfig, Mapping[ResourceName, "ResourceBase"]], "ResourceBase"]
200+
Validator: TypeAlias = Callable[[ComponentConfig], Sequence[str]]
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
from viam.resource.registry import Registry, ResourceRegistration
1+
from viam.components.motor import * # noqa: F403 Need to import motor so the component registers itself
2+
from viam.resource.registry import Registry, ResourceRegistration, ResourceCreatorRegistration
23

34
from .api import Gizmo, GizmoClient, GizmoService
45
from .my_gizmo import MyGizmo
56

67
Registry.register_subtype(ResourceRegistration(Gizmo, GizmoService, lambda name, channel: GizmoClient(name, channel)))
78

8-
Registry.register_resource_creator(Gizmo.SUBTYPE, MyGizmo.MODEL, MyGizmo.new)
9+
Registry.register_resource_creator(Gizmo.SUBTYPE, MyGizmo.MODEL, ResourceCreatorRegistration(MyGizmo.new, MyGizmo.validate_config))

0 commit comments

Comments
 (0)