diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 73de99c2..07915912 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,9 @@ Brewtils Changelog ------ TBD +- Added support to auto generate requests when calling functions locally that are annotated with command decorators. This only supports + plugin classes that extend `object` and does not support recursive command loops. This is disabled by default (#547) + Examples: Plugin(..., auto_self_client=True) - Added support for SystemClient to define choice_validation_enabled flag during initialization or command execution. Default behavior is to skip choice validation if a parent request is present. (#585) Examples: SystemClient(..., choice_validation_enabled=True) or SystemClient().call_command(..., _choice_validation_enabled=True) diff --git a/brewtils/plugin.py b/brewtils/plugin.py index 425bb252..bb9d606a 100644 --- a/brewtils/plugin.py +++ b/brewtils/plugin.py @@ -43,6 +43,7 @@ ) from brewtils.resolvers.manager import ResolutionManager from brewtils.rest.easy_client import EasyClient +from brewtils.rest.system_client import SystemClient from brewtils.specification import _CONNECTION_SPEC from typing import Optional, Union @@ -138,6 +139,7 @@ class Plugin(object): - ``require`` - ``requires`` - ``requires_timeout`` + - ``auto_self_client`` (defaults to "false") Connection information tells the Plugin how to communicate with Beer-garden. The most important of these is the ``bg_host`` (to tell the plugin where to find the @@ -250,6 +252,9 @@ class Plugin(object): prefix_topic (str): Prefix for Generated Command Topics + auto_self_client (bool): Whether to automatically invoke SystemClient for local + system commands via self + logger (:py:class:`logging.Logger`): Logger that will be used by the Plugin. Passing a logger will prevent the Plugin from preforming any additional logging configuration. @@ -283,6 +288,10 @@ def __init__(self, client=None, system=None, logger=None, **kwargs): self._custom_logger = False self._logger = self._setup_logging(logger=logger, **kwargs) + self._auto_self_client = kwargs.pop("auto_self_client", False) + if not isinstance(self._auto_self_client, bool): + self._auto_self_client = str(self._auto_self_client).lower() == "true" + # Need to pop out shutdown functions because these are not processed # until shutdown self.shutdown_functions = [] @@ -480,6 +489,26 @@ def _set_client(self, new_client): client_clazz._requires = self._system.requires client_clazz._current_request = client_clazz.current_request + # Can only inject System Clients if the Client inherits from a class object + # TODO: Should we add a configuration to explicitly disable this? + if self._auto_self_client and issubclass(client_clazz, object): + + def system_client_getattribute(self, name): + try: + if not name.startswith("_"): + request = get_current_request_read_only() + # Does not support recursive calls + if request and request.command != name: + if self.__class__._bg_commands: + for bg_command in self.__class__._bg_commands: + if bg_command.name == name: + return getattr(SystemClient(), name) + except Exception: + pass + return object.__getattribute__(self, name) + + new_client.__class__.__getattribute__ = system_client_getattribute + self._client = new_client global CLIENT CLIENT = new_client diff --git a/brewtils/rest/system_client.py b/brewtils/rest/system_client.py index f56b0e49..74b346c2 100644 --- a/brewtils/rest/system_client.py +++ b/brewtils/rest/system_client.py @@ -414,10 +414,33 @@ def send_bg_request(self, *args, **kwargs): # tried to pass a parameter without a key: # client.command_name(param) if args: - raise RequestProcessException( - "Using positional arguments when creating a request is not allowed. " - "Please use keyword arguments instead." - ) + if self.target_self: + _command = self._commands[kwargs["_command"]] + if _command: + arg_counter = 0 + for arg in args: + if arg_counter < len(_command.parameters): + if _command.parameters[arg_counter].key in kwargs: + raise RequestProcessException( + ( + "Keyword argument overlapped with provided positional " + "argument. Please use all keyword arguments instead." + ) + ) + kwargs[_command.parameters[arg_counter].key] = arg + arg_counter = arg_counter + 1 + else: + raise RequestProcessException( + ( + "More positional arguments provided that command parameters. " + "Please use all keyword arguments instead." + ) + ) + else: + raise RequestProcessException( + "Using positional arguments when creating a request is not allowed. " + "Please use keyword arguments instead." + ) # Need to pop here, otherwise we'll try to send as a request parameter raise_on_error = kwargs.pop("_raise_on_error", self._raise_on_error) diff --git a/brewtils/specification.py b/brewtils/specification.py index 2cbf112b..6c5dd289 100644 --- a/brewtils/specification.py +++ b/brewtils/specification.py @@ -215,6 +215,14 @@ def _is_json_dict(s): } _PLUGIN_SPEC = { + "auto_self_client": { + "type": "bool", + "description": ( + "Whether to automatically invoke SystemClient for local" + "system commands via self" + ), + "default": False, + }, "instance_name": { "type": "str", "description": "The instance name", diff --git a/test/plugin_test.py b/test/plugin_test.py index ec8ecad7..212ba3a7 100644 --- a/test/plugin_test.py +++ b/test/plugin_test.py @@ -201,6 +201,7 @@ def test_env(self, client, bg_system): os.environ["BG_GROUP"] = "GroupA" os.environ["BG_PREFIX_TOPIC"] = "custom.topic" os.environ["BG_REQUIRE"] = "SystemA" + os.environ["BG_AUTO_SELF_CLIENT"] = "False" plugin = Plugin(client, system=bg_system, max_concurrent=1) @@ -210,6 +211,7 @@ def test_env(self, client, bg_system): assert plugin._config.ssl_enabled is False assert plugin._config.ca_verify is False assert plugin._config.prefix_topic == "custom.topic" + assert plugin._config.auto_self_client is False assert "GroupA" == plugin._config.group assert "SystemA" in plugin._config.require