Skip to content

Commit

Permalink
Various updates
Browse files Browse the repository at this point in the history
  • Loading branch information
TheBurchLog committed Oct 19, 2023
1 parent 2af230f commit aebc06c
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 71 deletions.
17 changes: 17 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
Brewtils Changelog
==================

3.19.0
------
TBD
- Checks connection status when Plugin is initialized
- Added SystemClient(system_namespaces=[]) feature that round robins requests across multiple system_namespaces
- Expanded Auto Generation to support Doc String parameter extraction
- Plugins will break if Type Hinting and Parameter Type assignment do not matches
- Expanded Auto Generated parameter Typing from Type Hinting or Doc String to be
- str -> String
- int -> Integer
- float -> Float
- bool -> Boolean
- object -> Dictionary
- dict -> Dictionary
- DateTime -> DateTime
- bytes -> Bytes

3.18.0
------
10/13/2023
Expand Down
2 changes: 2 additions & 0 deletions brewtils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from brewtils.rest import normalize_url_prefix
from brewtils.rest.easy_client import get_easy_client, EasyClient
from brewtils.rest.system_client import SystemClient
from brewtils.auto_decorator import AutoDecorator

__all__ = [
"__version__",
Expand All @@ -23,6 +24,7 @@
"load_config",
"configure_logging",
"normalize_url_prefix",
"AutoDecorator",
]

# Aliased for compatibility
Expand Down
2 changes: 1 addition & 1 deletion brewtils/__version__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-

__version__ = "3.18.0"
__version__ = "3.19.0"
66 changes: 3 additions & 63 deletions brewtils/auto_decorator.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import inspect

from brewtils.models import Parameter
from brewtils.models import Parameter, Command


class AutoDecorator:
Expand All @@ -25,69 +25,9 @@ def addFunctions(self, client):
for func in dir(client):
if callable(getattr(client, func)):
if not func.startswith("_"):
# https://docs.python.org/3/library/inspect.html#inspect.signature
_wrapped = getattr(client, func)
signature = inspect.signature(_wrapped)

for func_parameter in signature.parameters:
func_parameter_value = signature.parameters[func_parameter]

key = func_parameter_value.name
if key == "self":
continue

func_parameter_value.default
typeValue = "String"
default = None
optional = False
is_kwarg = False

if str(func_parameter_value.annotation) in [
"<class 'inspect._empty'>",
"<class 'str'>",
]:
pass
elif str(func_parameter_value.annotation) in ["<class 'int'>"]:
typeValue = "Integer"
elif str(func_parameter_value.annotation) in [
"<class 'float'>"
]:
typeValue = "Float"
elif str(func_parameter_value.annotation) in ["<class 'bool'>"]:
typeValue = "Boolean"
elif str(func_parameter_value.annotation) in [
"<class 'object'>",
"<class 'dict'>",
]:
typeValue = "Dictionary"

if (
str(func_parameter_value.default)
!= "<class 'inspect._empty'>"
):
default = func_parameter_value.default

new_parameter = Parameter(
key=key,
type=typeValue,
multi=False,
display_name=key,
optional=optional,
default=default,
description=None,
choices=None,
parameters=None,
nullable=None,
maximum=None,
minimum=None,
regex=None,
form_input_type=None,
type_info=None,
is_kwarg=is_kwarg,
model=None,
)

_wrapped.parameters = getattr(_wrapped, "parameters", [])
_wrapped.parameters.append(new_parameter)
# decorators.py will handle all of the markings
_wrapped._command = Command()

return client
108 changes: 101 additions & 7 deletions brewtils/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,12 +413,17 @@ def _parse_method(method):
# Need to initialize existing parameters before attempting to add parameters
# pulled from the method signature.
method_command.parameters = _initialize_parameters(
method_command.parameters + getattr(method, "parameters", [])
method_command.parameters + getattr(method, "parameters", []), method=method
)

# Add and update parameters based on the method signature
_signature_parameters(method_command, method)

# Checks Docstring and Type Hints for undefined properties
for cmd_parameter in method_command.parameters:
if cmd_parameter.description is None:
cmd_parameter.description = _parameter_docstring(method, cmd_parameter.key)

# Verify that all parameters conform to the method signature
_signature_validate(method_command, method)

Expand Down Expand Up @@ -456,6 +461,9 @@ def _initialize_command(method):
cmd.name = _method_name(method)
cmd.description = cmd.description or _method_docstring(method)

if cmd.output_type is None and str(inspect.signature(method)._return_annotation) in ["<class 'object'>", "<class 'dict'>"]:
cmd.output_type = "JSON"

try:
base_dir = os.path.dirname(inspect.getfile(method))

Expand Down Expand Up @@ -512,6 +520,78 @@ def _method_docstring(method):

return docstring.split("\n")[0] if docstring else None

def _parameter_docstring(method, parameter):
# type: (...) -> str
"""Parse out the description associated with parameter of the docstring from a method
Args:
method: Method to inspect
parameter: Parameter to extract
Returns:
Description of parameter, if found
"""
if hasattr(method, "func_doc"):
docstring = method.func_doc
else:
docstring = method.__doc__

delimiters = [":", "--"]
for line in docstring.expandtabs().split("\n"):
line = line.strip()
for delimiter in delimiters:
if delimiter in line:
if line.startswith(parameter + " ") or line.startswith(parameter + delimiter):
return line.split(delimiter)[1].strip()

return None

def _parameter_type_hint(method, cmd_parameter):
for _, arg in enumerate(signature(method).parameters.values()):
if arg.name == cmd_parameter:
if str(arg.annotation) in ["<class 'str'>"]:
return "String"
if str(arg.annotation) in ["<class 'int'>"]:
return "Integer"
if str(arg.annotation) in ["<class 'float'>"]:
return "Float"
if str(arg.annotation) in ["<class 'bool'>"]:
return "Boolean"
if str(arg.annotation) in [ "<class 'object'>", "<class 'dict'>"]:
return "Dictionary"
if str(arg.annotation).lower() in ["<class 'datetime'>"]:
return "DateTime"
if str(arg.annotation) in ["<class 'bytes'>"]:
return "Bytes"

if hasattr(method, "func_doc"):
docstring = method.func_doc
else:
docstring = method.__doc__

for line in docstring.expandtabs().split("\n"):
line = line.strip()

if line.startswith(cmd_parameter + " ") and line.find(")") > line.find("("):
docType = line.split("(")[1].split(")")[0]

if docType in ["str"]:
return "String"
if docType in ["int"]:
return "Integer"
if docType in ["float"]:
return "Float"
if docType in ["bool"]:
return "Boolean"
if docType in ["obj", "object", "dict"]:
return "Dictionary"
if docType.lower() in ["datetime"]:
return "DateTime"
if docType in ["bytes"]:
return "Bytes"
return None

def _sig_info(arg):
# type: (InspectParameter) -> Tuple[Any, bool]
Expand Down Expand Up @@ -573,6 +653,7 @@ def _initialize_parameter(
type_info=None,
is_kwarg=None,
model=None,
method=None,
):
# type: (...) -> Parameter
"""Initialize a Parameter
Expand Down Expand Up @@ -619,6 +700,10 @@ def _initialize_parameter(
if param.key is None:
raise PluginParamError("Attempted to create a parameter without a key")

# Extract Parameter Type if not present
if param.type is None and method is not None:
param.type = _parameter_type_hint(method, param.key)

# Type and type info
# Type info is where type specific information goes. For now, this is specific
# to file types. See #289 for more details.
Expand Down Expand Up @@ -680,8 +765,8 @@ def _format_type(param_type):
return str(param_type).title()


def _initialize_parameters(parameter_list):
# type: (Iterable[Parameter, object, dict]) -> List[Parameter]
def _initialize_parameters(parameter_list, method=None):
# type: (Iterable[Parameter, object, dict], obj) -> List[Parameter]
"""Initialize Parameters from a list of parameter definitions
This exists for backwards compatibility with the old way of specifying Models.
Expand Down Expand Up @@ -709,7 +794,7 @@ def _initialize_parameters(parameter_list):
# This is already a Parameter. Only really need to interpret the choices
# definition and recurse down into nested Parameters
if isinstance(param, Parameter):
initialized_params.append(_initialize_parameter(param=param))
initialized_params.append(_initialize_parameter(param=param, method=method))

# This is a model class object. Needed for backwards compatibility
# See https://github.com/beer-garden/beer-garden/issues/354
Expand All @@ -718,11 +803,11 @@ def _initialize_parameters(parameter_list):
"Constructing a nested Parameters list using model class objects "
"is deprecated. Please pass the model's parameter list directly."
)
initialized_params += _initialize_parameters(param.parameters)
initialized_params += _initialize_parameters(param.parameters, method=method)

# This is a dict of Parameter kwargs
elif isinstance(param, dict):
initialized_params.append(_initialize_parameter(**param))
initialized_params.append(_initialize_parameter(method=method, **param))

# No clue!
else:
Expand Down Expand Up @@ -786,7 +871,7 @@ def _signature_parameters(cmd, method):
if arg.name not in cmd.parameter_keys():
cmd.parameters.append(
_initialize_parameter(
key=arg.name, default=sig_default, optional=sig_optional
key=arg.name, default=sig_default, optional=sig_optional, type=sig_type, method=method
)
)

Expand All @@ -801,6 +886,9 @@ def _signature_parameters(cmd, method):
if param.optional is None:
param.optional = sig_optional

if param.description is None:
param.description = _parameter_docstring(method, param.key)

return cmd


Expand Down Expand Up @@ -835,6 +923,12 @@ def _signature_validate(cmd, method):
if p.kind == InspectParameter.VAR_KEYWORD:
has_kwargs = True

if _parameter_type_hint(method, param.key) and param.type != _parameter_type_hint(method, param.key):
raise PluginParamError(
"Parameter Type assigned in the @parameter(type=?) does not match either the function Type Hint "
"or the Doc String definition. Please evaluate your type matching."
)

# Couldn't find the parameter. That's OK if this parameter is meant to be part
# of the **kwargs AND the function has a **kwargs parameter.
if sig_param is None:
Expand Down
3 changes: 3 additions & 0 deletions brewtils/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,9 @@ def __init__(self, client=None, system=None, logger=None, **kwargs):
# Now that the config is loaded we can create the EasyClient
self._ez_client = EasyClient(logger=self._logger, **self._config)

if not self._ez_client.can_connect():
raise RestConnectionError("Cannot connect to the Beer-garden server")

# With the EasyClient we can determine if this is an old garden
self._legacy = self._legacy_garden()

Expand Down
36 changes: 36 additions & 0 deletions brewtils/rest/system_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ class SystemClient(object):
If not set the System definition will be loaded when making the first
request and will only be reloaded if a Request fails.
system_namespaces:
If the targeted system is stateless and if a collection of systems could
handle the Request. This will allow the plugin to round robin the requests
to each namespace to help load balance the requests. It will rotate per
Request to the target system.
Loading the System:
The System definition is lazily loaded, so nothing happens until the first
attempt to send a Request. At that point the SystemClient will query Beer-garden
Expand Down Expand Up @@ -155,6 +161,8 @@ class SystemClient(object):
Args:
system_name (str): Name of the System to make Requests on
system_namespace (str): Namespace of the System to make Requests on
system_namespaces (list): Namespaces of the System to round robin Requests to.
The target System should be stateless.
version_constraint (str): System version to make Requests on. Can be specific
('1.0.0') or 'latest'.
default_instance (str): Name of the Instance to make Requests on
Expand Down Expand Up @@ -237,6 +245,22 @@ def __init__(self, *args, **kwargs):
self._system_namespace = kwargs.get(
"system_namespace", brewtils.plugin.CONFIG.namespace or ""
)
self._system_namespaces = kwargs.get(
"system_namespaces", []
)

# if both system namespaces are defined, combine the inputs
if len(self._system_namespaces) > 0:
self._current_system_namespace = 0
if kwargs.get("system_namespace", None):
if self._system_namespace not in self._system_namespaces:
self._system_namespaces.append(self._system_namespace)

elif len(self._system_namespaces) == 1:
self._system_namespace = self._system_namespaces[0]
self._current_system_namespace = -1
else:
self._current_system_namespace = -1

self._always_update = kwargs.get("always_update", False)
self._timeout = kwargs.get("timeout", None)
Expand Down Expand Up @@ -270,6 +294,16 @@ def bg_system(self):
@property
def bg_default_instance(self):
return self._default_instance

def _rotate_namespace(self):
if self._current_system_namespace > -1:
self._system_namespace = self._system_namespaces[self._current_system_namespace]
self._current_system_namespace += 1
if self._current_system_namespace == len(self._system_namespaces):
self._current_system_namespace = 0

# Set loaded to False to force the reload of the System
self._loaded = False

def create_bg_request(self, command_name, **kwargs):
# type: (str, **Any) -> partial
Expand Down Expand Up @@ -306,6 +340,8 @@ def create_bg_request(self, command_name, **kwargs):
AttributeError: System does not have a Command with the given command_name
"""

self._rotate_namespace()

if not self._loaded or self._always_update:
self.load_bg_system()

Expand Down

0 comments on commit aebc06c

Please sign in to comment.