-
Notifications
You must be signed in to change notification settings - Fork 56
RSDK-7732 EasyResource mixin #643
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
9ff8e7f
EasyResource base class working
abe-winter b664569
fix model parser, add entrypoints
abe-winter 01abba5
don't inherit ResourceBase -- it crashes at mro walk in ResourceManag…
abe-winter 5b44f1c
easy_resource example + pyinstaller
abe-winter 318ade9
log level in argparse
abe-winter b657872
test EasyResource
abe-winter 76b9655
rename + test parse_module_args
abe-winter 590ee13
meh we trust argparse
abe-winter 0507d12
docstrings, pyright, lint
abe-winter 4e19ecf
private methods
abe-winter bbd8c5b
hasattr check in run_with_models
abe-winter 469c92f
usage in run_from_registry
abe-winter 7b31548
remove todo comment
abe-winter 6cbe3e1
remove remaining todo comment
abe-winter 711bb71
use argparse everywhere
abe-winter ac9fc40
Merge branch 'main' into easy-resource
abe-winter 3347b53
clarify comment
abe-winter File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
dist/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
dist/main: main.py | ||
pyinstaller --onefile --hidden-import="googleapiclient" $^ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
# easy_resource | ||
|
||
This is an example using the `EasyResource` mixin, a helper for creating Viam modules. | ||
|
||
## Instructions | ||
|
||
### Run standalone in this directory | ||
|
||
If you know how to set up a python virtualenv, do that first to give yourself an isolated environment. (If not, it's okay to proceed without it). | ||
|
||
```sh | ||
# install the SDK | ||
pip install viam-sdk | ||
# or if you plan to edit the SDK: `pip install -e ../..` | ||
|
||
# run the module | ||
./main.py socket-path | ||
|
||
# Ctrl-C the running process when you're done | ||
``` | ||
|
||
Running it this way won't do anything interesting, but it's a quick way to ensure your module can boot. | ||
|
||
### Run with viam-server | ||
|
||
If you are running viam-server on your laptop, you can use easy_resource as a module. | ||
|
||
```sh | ||
# first, build a binary for the module: | ||
pip install pyinstaller | ||
make dist/main | ||
``` | ||
|
||
Then, in the Viam config builder for your machine: | ||
1. Add a local module with executable path `/PATH/TO/REPO/examples/easy_resource/dist/main`. (Replace `/PATH/TO/REPO`). | ||
1. Then add a local component with type 'sensor' and triplet 'viam:sensor:easy-resource-example' (see screenshot). | ||
1. Save, then go to the control page to interact with the sensor. | ||
|
||
Screenshot of 'local component' screen: | ||
|
||
 |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
#!/usr/bin/env python3 | ||
|
||
import asyncio | ||
from viam.components.sensor import Sensor | ||
from viam.resource.easy_resource import EasyResource | ||
from viam.module.module import Module | ||
|
||
|
||
class MySensor(Sensor, EasyResource): | ||
MODEL = "viam:sensor:easy-resource-example" | ||
|
||
async def get_readings(self, **kwargs): | ||
return {"ok": True} | ||
|
||
|
||
if __name__ == '__main__': | ||
asyncio.run(Module.run_from_registry()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,6 @@ | ||
import argparse | ||
import io | ||
import logging as pylogging | ||
import sys | ||
from inspect import iscoroutinefunction | ||
from threading import Lock | ||
|
@@ -35,6 +37,16 @@ | |
LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
def _parse_module_args() -> argparse.Namespace: | ||
""" | ||
Parse command-line args. Used by the various `Module` entrypoints. | ||
""" | ||
p = argparse.ArgumentParser(description="Start this viam python module") | ||
p.add_argument('socket_path', help="path where this module will serve a unix socket") | ||
p.add_argument('--log-level', type=lambda name: pylogging._nameToLevel[name.upper()], default=logging.INFO) | ||
return p.parse_args() | ||
|
||
|
||
class Module: | ||
_address: str | ||
_parent_address: Optional[str] = None | ||
|
@@ -57,12 +69,38 @@ def from_args(cls) -> Self: | |
Returns: | ||
Module: a new Module instance | ||
""" | ||
args = sys.argv | ||
if len(args) < 2: | ||
raise Exception("Need socket path as command line argument") | ||
address = args[1] | ||
log_level = logging.DEBUG if (len(args) == 3 and "=debug" in args[2].lower()) else logging.INFO | ||
return cls(address, log_level=log_level) | ||
args = _parse_module_args() | ||
return cls(args.socket_path, log_level=args.log_level) | ||
|
||
@classmethod | ||
async def run_with_models(cls, *models: ResourceBase): | ||
""" | ||
Module entrypoint that takes a list of ResourceBase implementations. | ||
In most cases you'll want to use run_from_registry instead (see below). | ||
""" | ||
module = cls.from_args() | ||
for model in models: | ||
if not hasattr(model, 'MODEL'): | ||
raise TypeError(f"missing MODEL field on {model}. Resource implementations must define MODEL") | ||
module.add_model_from_registry(model.SUBTYPE, model.MODEL) # pyright: ignore [reportAttributeAccessIssue] | ||
await module.start() | ||
|
||
@classmethod | ||
async def run_from_registry(cls): | ||
""" | ||
Module entrypoint that automatically includes all the resources you've created in your program. | ||
|
||
Example: | ||
|
||
if __name__ == '__main__': | ||
asyncio.run(Module.run_from_registry()) | ||
|
||
Full example at examples/easy_resource/main.py. | ||
""" | ||
module = cls.from_args() | ||
for key in Registry.REGISTERED_RESOURCE_CREATORS().keys(): | ||
module.add_model_from_registry(*key.split('/')) # pyright: ignore [reportArgumentType] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dislike the disable pyright here but also seems dumb to create a Subtype just to stringify it again in the next method call. No action needed, just writing it down |
||
await module.start() | ||
|
||
def __init__(self, address: str, *, log_level: int = logging.INFO) -> None: | ||
# When a module is launched by viam-server, its stdout is not connected to a tty. In | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import re | ||
from typing import Mapping, Union | ||
|
||
from viam.proto.app.robot import ComponentConfig | ||
from viam.proto.common import ResourceName | ||
|
||
from .. import logging | ||
from .base import ResourceBase | ||
from .types import Model, ModelFamily, Subtype | ||
from .registry import Registry, ResourceCreatorRegistration | ||
|
||
modelRegex = re.compile(r"^([^:]+):([^:]+):([^:]+)$") | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
def _parse_model(orig: Union[str, Model]) -> Model: | ||
"take a model or string and turn it into a Model" | ||
if isinstance(orig, Model): | ||
return orig | ||
match = modelRegex.match(orig) | ||
if not match: | ||
raise ValueError(f"MODEL {orig} doesn't match expected format 'org:type:name'") | ||
*family, name = match.groups() | ||
return Model(ModelFamily(*family), name) | ||
|
||
|
||
class EasyResource: | ||
""" | ||
EasyResource is a mixin that simplifies the process of creating Viam modules (extension programs) | ||
and resources (the resource classes provided by those extension programs). | ||
|
||
Basic usage: | ||
|
||
class MyModel(Sensor, EasyResource): | ||
MODEL = "my-org:sensor:my-sensor" | ||
|
||
async def get_readings(self, **kwargs): | ||
return {"ok": True} | ||
|
||
See examples/easy_resource/main.py for extended usage. | ||
""" | ||
SUBTYPE: Subtype | ||
MODEL: Model | ||
|
||
def __init_subclass__(cls, register=True, **kwargs): | ||
""" | ||
When you subclass this mixin, it parses cls.MODEL and registers cls in global registry. | ||
""" | ||
super().__init_subclass__(**kwargs) | ||
if not hasattr(cls, "MODEL"): | ||
raise ValueError("please define a MODEL like 'org:type:name' on your class, for example 'viam:camera:IMX219'") | ||
cls.MODEL = _parse_model(cls.MODEL) | ||
if register: | ||
cls.register() | ||
|
||
def __init__(self, name: str): | ||
# note: this mirrors the constructor for ComponentBase and ServiceBase. | ||
self.name = name | ||
|
||
@classmethod | ||
def new(cls, config: ComponentConfig, dependencies: Mapping[ResourceName, ResourceBase]): | ||
""" | ||
This is passed to register_resource_creator; the default implementation calls reconfigure() | ||
when an instance of your model is instantiated. You can override this in your subclass. | ||
""" | ||
self = cls(config.name) | ||
logger.debug("created %s %s %s", self.SUBTYPE, self.MODEL, config.name) | ||
self.reconfigure(config, dependencies) | ||
return self | ||
|
||
@classmethod | ||
def register(cls): | ||
""" | ||
This adds the model to the global registry. It is called by __init_subclass__ and you typically | ||
won't call it directly. | ||
""" | ||
logger.debug('registering %s %s', cls.SUBTYPE, cls.MODEL) | ||
# note: We could fix this pyright-ignore if EasyResource inherited ResourceBase, but that crashes in the mro() | ||
# walk in ResourceManager.register. | ||
Registry.register_resource_creator( | ||
cls.SUBTYPE, cls.MODEL, ResourceCreatorRegistration(cls.new)) # pyright: ignore [reportArgumentType] | ||
|
||
def reconfigure(self, config: ComponentConfig, dependencies: Mapping[ResourceName, ResourceBase]): | ||
logger.debug('reconfigure %s %s', self.SUBTYPE, self.MODEL) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import pytest | ||
|
||
from viam.components.generic import Generic | ||
from viam.proto.app.robot import ComponentConfig | ||
from viam.resource.easy_resource import _parse_model, EasyResource | ||
from viam.resource.registry import Registry | ||
from viam.resource.types import Model, ModelFamily | ||
|
||
|
||
@pytest.fixture | ||
def clear_registry(monkeypatch): | ||
"helper to patch registry global state for duration of test" | ||
monkeypatch.setattr(Registry, '_SUBTYPES', {}) | ||
monkeypatch.setattr(Registry, '_RESOURCES', {}) | ||
|
||
|
||
class TestEasyResource: | ||
def test_parse_model(self): | ||
model = Model(ModelFamily('viam', 'type'), 'name') | ||
assert _parse_model('viam:type:name') == model | ||
assert _parse_model(model) == model | ||
with pytest.raises(ValueError): | ||
_parse_model('not parseable') | ||
|
||
def test_subclass(self, clear_registry): | ||
class SubclassTest(Generic, EasyResource): | ||
MODEL = "org:type:name" | ||
# did it register correctly: | ||
assert set(Registry._RESOURCES.keys()) == {f'rdk:component:generic/{SubclassTest.MODEL}'} | ||
|
||
# can it be instantiated: | ||
resource = SubclassTest.new(ComponentConfig(name="hello"), {}) | ||
assert resource.name == "hello" |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ResourceBase
does not have aMODEL
field at all, sinceMODEL
s are for implementation of aResourceBase
, andResourceBase
is for creating the resource definition/interface. This would fail in the possible event that someone passesArm
as the param instead ofMyModuleArm
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So would be useful to maybe do a
hasattr
here and fail if someone doesn't pass the correct type. And also add that disclaimer to the docstringThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added hasattr check w/ helpful error
I didn't expand docstring because when I started writing it I was saying 'the provided resources have to be complete + runnable' and it felt weird. but can add something if you think it's helpful