chg: typing - add type hints for plugin model

This commit is contained in:
Bharath Vignesh J K 2024-08-08 17:17:04 +05:30
parent 80a1e0e56d
commit 7969d85f3c
1 changed files with 168 additions and 108 deletions

View File

@ -14,10 +14,23 @@ I am your father...
import copy
import re
from argparse import Namespace
from typing import Any, Callable, Dict, List, Literal, Optional, Union
from glances.actions import GlancesActions
from glances.config import Config
from glances.events_list import glances_events
from glances.globals import dictlist, iterkeys, itervalues, json_dumps, json_dumps_dictlist, listkeys, mean, nativestr
from glances.globals import (
dictlist,
iterkeys,
itervalues,
json_dumps,
json_dumps_dictlist,
listkeys,
listvalues,
mean,
nativestr,
)
from glances.history import GlancesHistory
from glances.logger import logger
from glances.outputs.glances_unicode import unicode_message
@ -41,11 +54,20 @@ fields_unit_type = {
'bytes': 'int',
}
InputMethod = Literal["local", "snmp", "glances"]
class GlancesPluginModel:
"""Main class for Glances plugin model."""
def __init__(self, args=None, config=None, items_history_list=None, stats_init_value={}, fields_description=None):
def __init__(
self,
args: Namespace,
config: Config,
items_history_list: Union[List[Dict[str, str]], None] = None,
stats_init_value: Union[Dict, List, None] = None,
fields_description: Union[Dict, None] = None,
):
"""Init the plugin of plugins model class.
All Glances' plugins model should inherit from this class. Most of the
@ -85,15 +107,15 @@ class GlancesPluginModel:
self._align = 'left'
# Init the input method
self._input_method = 'local'
self._short_system_name = None
self._input_method: InputMethod = 'local'
self._short_system_name: Union[str, None] = None
# Init the history list
self.items_history_list = items_history_list
self.items_history_list = [] if items_history_list is None else items_history_list
self.stats_history = self.init_stats_history()
# Init the limits (configuration keys) dictionary
self._limits = {}
self._limits: Dict[str, Union[int, float, List]] = {}
if config is not None:
logger.debug(f'Load section {self.plugin_name} in Glances configuration file')
self.load_limits(config=config)
@ -105,54 +127,55 @@ class GlancesPluginModel:
self.actions = GlancesActions(args=args)
# Init the views
self.views = {}
self.views: Dict = {}
# Hide stats if all the hide_zero_fields has never been != 0
# Default is False, always display stats
self.hide_zero = False
self.hide_zero_fields = []
self.hide_zero_fields: List = []
# Set the initial refresh time to display stats the first time
self.refresh_timer = Timer(0)
# Init stats description
self.fields_description = fields_description
self.fields_description = {} if fields_description is None else fields_description
# Init the stats
self.stats_init_value = stats_init_value
actual_stats_init_value: Union[Dict, List] = {} if stats_init_value is None else stats_init_value
self.stats_init_value = actual_stats_init_value
self.time_since_last_update = None
self.stats = None
self.stats_previous = None
self.stats = actual_stats_init_value
self.stats_previous = actual_stats_init_value
self.reset()
def __repr__(self):
def __repr__(self) -> str:
"""Return the raw stats."""
return str(self.stats)
def __str__(self):
def __str__(self) -> str:
"""Return the human-readable stats."""
return str(self.stats)
def get_init_value(self):
def get_init_value(self) -> Union[Dict, List]:
"""Return a copy of the init value."""
return copy.copy(self.stats_init_value)
def reset(self):
def reset(self) -> None:
"""Reset the stats.
This method should be overwritten by child classes.
"""
self.stats = self.get_init_value()
def exit(self):
def exit(self) -> None:
"""Just log an event when Glances exit."""
logger.debug(f"Stop the {self.plugin_name} plugin")
def get_key(self):
def get_key(self) -> str | None:
"""Return the key of the list."""
return
return None
def is_enabled(self, plugin_name=None):
def is_enabled(self, plugin_name: Union[str, None] = None) -> bool:
"""Return true if plugin is enabled."""
if not plugin_name:
plugin_name = self.plugin_name
@ -162,33 +185,33 @@ class GlancesPluginModel:
d = getattr(self.args, 'enable_' + plugin_name, True)
return d is False
def is_disabled(self, plugin_name=None):
def is_disabled(self, plugin_name: Union[str, None] = None) -> bool:
"""Return true if plugin is disabled."""
return not self.is_enabled(plugin_name=plugin_name)
def history_enable(self):
def history_enable(self) -> bool:
return self.args is not None and not self.args.disable_history and self.get_items_history_list() is not None
def init_stats_history(self):
def init_stats_history(self) -> GlancesHistory:
"""Init the stats history (dict of GlancesAttribute)."""
if self.history_enable():
init_list = [a['name'] for a in self.get_items_history_list()]
logger.debug(f"Stats history activated for plugin {self.plugin_name} (items: {init_list})")
return GlancesHistory()
def reset_stats_history(self):
def reset_stats_history(self) -> None:
"""Reset the stats history (dict of GlancesAttribute)."""
if self.history_enable():
reset_list = [a['name'] for a in self.get_items_history_list()]
logger.debug(f"Reset history for plugin {self.plugin_name} (items: {reset_list})")
self.stats_history.reset()
def update_stats_history(self):
def update_stats_history(self) -> None:
"""Update stats history."""
# Build the history
if not (self.get_export() and self.history_enable()):
return
# Itern through items history
# Iterate through items history
item_name = '' if self.get_key() is None else self.get_key()
for i in self.get_items_history_list():
if isinstance(self.get_export(), list):
@ -212,11 +235,11 @@ class GlancesPluginModel:
history_max_size=self._limits['history_size'],
)
def get_items_history_list(self):
def get_items_history_list(self) -> List:
"""Return the items history list."""
return self.items_history_list
def get_raw_history(self, item=None, nb=0):
def get_raw_history(self, item: Union[int, str, None] = None, nb: int = 0) -> Any:
"""Return the history (RAW format).
- the stats history (dict of list) if item is None
@ -230,11 +253,11 @@ class GlancesPluginModel:
return s[item]
return None
def get_export_history(self, item=None):
def get_export_history(self, item: Union[int, str, None] = None):
"""Return the stats history object to export."""
return self.get_raw_history(item=item)
def get_stats_history(self, item=None, nb=0):
def get_stats_history(self, item: Union[int, str, None] = None, nb: int = 0) -> bytes | None:
"""Return the stats history (JSON format)."""
s = self.stats_history.get_json(nb=nb)
@ -243,10 +266,10 @@ class GlancesPluginModel:
return json_dumps_dictlist(s, item)
def get_trend(self, item, nb=30):
"""Get the trend regarding to the last nb values.
def get_trend(self, item: Union[int, str, None], nb: int = 30) -> float | None:
"""Get the trend in regard to the last nb values.
The trend is the diffirence between the mean of the last 0 to nb / 2
The trend is the difference between the mean of the last 0 to nb / 2
and nb / 2 to nb values.
"""
raw_history = self.get_raw_history(item=item, nb=nb)
@ -256,12 +279,12 @@ class GlancesPluginModel:
return mean(last_nb[nb // 2 :]) - mean(last_nb[: nb // 2])
@property
def input_method(self):
def input_method(self) -> InputMethod:
"""Get the input method."""
return self._input_method
@input_method.setter
def input_method(self, input_method):
def input_method(self, input_method: InputMethod) -> None:
"""Set the input method.
* local: system local grab (psutil or direct access)
@ -271,11 +294,16 @@ class GlancesPluginModel:
self._input_method = input_method
@property
def short_system_name(self):
def short_system_name(self) -> str | None:
"""Get the short detected OS name (SNMP)."""
return self._short_system_name
def sorted_stats(self):
@short_system_name.setter
def short_system_name(self, short_name: str) -> None:
"""Set the short detected OS name (SNMP)."""
self._short_system_name = short_name
def sorted_stats(self) -> Union[List, Dict]:
"""Get the stats sorted by an alias (if present) or key."""
key = self.get_key()
if key is None:
@ -297,16 +325,11 @@ class GlancesPluginModel:
),
)
@short_system_name.setter
def short_system_name(self, short_name):
"""Set the short detected OS name (SNMP)."""
self._short_system_name = short_name
def set_stats(self, input_stats):
"""Set the stats to input_stats."""
self.stats = input_stats
def get_stats_snmp(self, bulk=False, snmp_oid=None):
def get_stats_snmp(self, bulk: bool = False, snmp_oid: Union[Dict, None] = None):
"""Update stats using SNMP.
If bulk=True, use a bulk request instead of a get request.
@ -333,16 +356,15 @@ class GlancesPluginModel:
# Bulk command for only one OID
# Note: key is the item indexed but the OID result
for item in snmp_result:
if iterkeys(item)[0].startswith(itervalues(snmp_oid)[0]):
ret[iterkeys(snmp_oid)[0] + iterkeys(item)[0].split(itervalues(snmp_oid)[0])[1]] = itervalues(
item
)[0]
if listkeys(item)[0].startswith(listkeys(snmp_oid)[0]):
key = listkeys(snmp_oid)[0] + listkeys(item)[0].split(listvalues(snmp_oid)[0])[1]
ret[key] = listvalues(item)[0]
else:
# Build the internal dict with the SNMP result
# Note: key is the first item in the snmp_oid
index = 1
for item in snmp_result:
item_stats = {}
item_stats: Dict[str, Any] = {}
item_key = None
for key in iterkeys(snmp_oid):
oid = snmp_oid[key] + '.' + str(index)
@ -364,40 +386,40 @@ class GlancesPluginModel:
return ret
def get_raw(self):
def get_raw(self) -> Any:
"""Return the stats object."""
return self.stats
def get_export(self):
def get_export(self) -> Any:
"""Return the stats object to export.
By default, return the raw stats.
Note: this method could be overwritten by the plugin if a specific format is needed (ex: processlist)
"""
return self.get_raw()
def get_stats(self):
def get_stats(self) -> bytes:
"""Return the stats object in JSON format."""
return json_dumps(self.get_raw())
def get_json(self):
def get_json(self) -> bytes:
"""Return the stats object in JSON format."""
return self.get_stats()
def get_raw_stats_item(self, item):
def get_raw_stats_item(self, item: str) -> List:
"""Return the stats object for a specific item in RAW format.
Stats should be a list of dict (processlist, network...)
"""
return dictlist(self.get_raw(), item)
def get_stats_item(self, item):
def get_stats_item(self, item: str) -> bytes:
"""Return the stats object for a specific item in JSON format.
Stats should be a list of dict (processlist, network...)
"""
return json_dumps_dictlist(self.get_raw(), item)
def get_raw_stats_value(self, item, value):
def get_raw_stats_value(self, item: str, value: Union[int, float, str]) -> Union[Dict, None]:
"""Return the stats object for a specific item=value.
Return None if the item=value does not exist
@ -414,7 +436,7 @@ class GlancesPluginModel:
logger.error(f"Cannot get item({item})=value({value}) ({e})")
return None
def get_stats_value(self, item, value):
def get_stats_value(self, item: str, value: Union[int, float, str]) -> bytes | None:
"""Return the stats object for a specific item=value in JSON format.
Stats should be a list of dict (processlist, network...)
@ -424,13 +446,13 @@ class GlancesPluginModel:
return None
return json_dumps(rsv)
def get_item_info(self, item, key, default=None):
def get_item_info(self, item: str, key: str, default: Union[str, None] = None) -> str | None:
"""Return the item info grabbed into self.fields_description."""
if self.fields_description is None or item not in self.fields_description:
return default
return self.fields_description[item].get(key, default)
def update_views_hidden(self):
def update_views_hidden(self) -> bool:
"""Update the hidden views
If the self.hide_zero is set then update the hidden field of the view
@ -470,7 +492,7 @@ class GlancesPluginModel:
self.views[f]['hidden'] = self.views['_zero'] and self.views[f] == 0
return True
def update_views(self):
def update_views(self) -> Dict:
"""Update the stats views.
The V of MVC
@ -483,7 +505,7 @@ class GlancesPluginModel:
'hidden': False, >>> Is the stats should be hidden in the UI
'_zero': True} >>> For internal purpose only
"""
ret = {}
ret: Dict[str, Dict] = {}
if isinstance(self.get_raw(), list) and self.get_raw() is not None and self.get_key() is not None:
# Stats are stored in a list of dict (ex: DISKIO, NETWORK, FS...)
@ -522,15 +544,17 @@ class GlancesPluginModel:
return self.views
def set_views(self, input_views):
def set_views(self, input_views: Dict) -> None:
"""Set the views to input_views."""
self.views = input_views
def reset_views(self):
def reset_views(self) -> None:
"""Reset the views to input_views."""
self.views = {}
def get_views(self, item=None, key=None, option=None):
def get_views(
self, item: Union[str, None] = None, key: Union[str, None] = None, option: Union[str, None] = None
) -> Any:
"""Return the views object.
If key is None, return all the view for the current plugin
@ -552,11 +576,13 @@ class GlancesPluginModel:
return item_views[key][option]
return 'DEFAULT'
def get_json_views(self, item=None, key=None, option=None):
def get_json_views(
self, item: Union[str, None] = None, key: Union[str, None] = None, option: Union[str, None] = None
) -> bytes:
"""Return the views (in JSON)."""
return json_dumps(self.get_views(item, key, option))
def load_limits(self, config):
def load_limits(self, config: Config) -> bool:
"""Load limits from the configuration file, if it exists."""
# By default set the history length to 3 points per second during one day
self._limits['history_size'] = 28800
@ -584,41 +610,41 @@ class GlancesPluginModel:
return True
@property
def limits(self):
def limits(self) -> Dict:
"""Return the limits object."""
return self._limits
@limits.setter
def limits(self, input_limits):
def limits(self, input_limits: Dict) -> None:
"""Set the limits to input_limits."""
self._limits = input_limits
def set_refresh(self, value):
def set_refresh(self, value: Union[int, float]) -> None:
"""Set the plugin refresh rate"""
self.set_limits('refresh', value)
def get_refresh(self):
def get_refresh(self) -> Union[int, float]:
"""Return the plugin refresh time"""
ret = self.get_limits(item='refresh')
ret: Union[int, float, None] = self.get_limits(item='refresh')
if ret is None:
ret = self.args.time if hasattr(self.args, 'time') else 2
return ret
def get_refresh_time(self):
def get_refresh_time(self) -> Union[int, float]:
"""Return the plugin refresh time"""
return self.get_refresh()
def set_limits(self, item, value):
def set_limits(self, item: str, value: Any) -> None:
"""Set the limits object."""
self._limits[f'{self.plugin_name}_{item}'] = value
def get_limits(self, item=None):
def get_limits(self, item: Union[str, None] = None) -> Union[Dict, int, float, None]:
"""Return the limits object."""
if item is None:
return self._limits
return self._limits.get(f'{self.plugin_name}_{item}', None)
def get_stats_action(self):
def get_stats_action(self) -> Union[List, Dict]:
"""Return stats for the action.
By default return all the stats.
@ -627,7 +653,7 @@ class GlancesPluginModel:
"""
return self.stats
def get_stat_name(self, header=""):
def get_stat_name(self, header: str = "") -> str:
"""Return the stat name with an optional header"""
ret = self.plugin_name
if header != "":
@ -636,15 +662,15 @@ class GlancesPluginModel:
def get_alert(
self,
current=0,
minimum=0,
maximum=100,
highlight_zero=True,
is_max=False,
header="",
action_key=None,
log=False,
):
current: float = 0,
minimum: float = 0,
maximum: float = 100,
highlight_zero: bool = True,
is_max: bool = False,
header: str = "",
action_key: Optional[str] = None,
log: bool = False,
) -> str:
"""Return the alert status relative to a current value.
Use this function for minor stats.
@ -727,7 +753,7 @@ class GlancesPluginModel:
# Default is 'OK'
return ret + log_str
def filter_stats(self, stats):
def filter_stats(self, stats: Any) -> Dict[str, Union[float, int]] | List:
"""Filter the stats to keep only the fields we want (the one defined in fields_description)."""
if hasattr(stats, '_asdict'):
return {k: v for k, v in stats._asdict().items() if k in self.fields_description}
@ -737,11 +763,11 @@ class GlancesPluginModel:
return [self.filter_stats(s) for s in stats]
return stats
def manage_threshold(self, stat_name, trigger):
def manage_threshold(self, stat_name: str, trigger: str):
"""Manage the threshold for the current stat."""
glances_thresholds.add(stat_name, trigger)
def manage_action(self, stat_name, trigger, header, action_key):
def manage_action(self, stat_name: str, trigger: str, header: str, action_key: Optional[str]):
"""Manage the action for the current stat."""
# Here is a command line for the current trigger ?
try:
@ -771,19 +797,26 @@ class GlancesPluginModel:
# 2) Run the action
self.actions.run(stat_name, trigger, command, repeat, mustache_dict=mustache_dict)
def get_alert_log(self, current=0, minimum=0, maximum=100, header="", action_key=None):
def get_alert_log(
self,
current: Union[int, float] = 0,
minimum: int = 0,
maximum: int = 100,
header: str = "",
action_key: None = None,
) -> str:
"""Get the alert log."""
return self.get_alert(
current=current, minimum=minimum, maximum=maximum, header=header, action_key=action_key, log=True
)
def is_limit(self, criticality, stat_name=""):
def is_limit(self, criticality: str, stat_name: str = "") -> bool:
"""Return true if the criticality limit exist for the given stat_name"""
if stat_name == "":
return self.plugin_name + '_' + criticality in self._limits
return stat_name + '_' + criticality in self._limits
def get_limit(self, criticality=None, stat_name=""):
def get_limit(self, criticality: Optional[str] = None, stat_name: str = "") -> Union[float, Dict]:
"""Return the limit value for the given criticality.
If criticality is None, return the dict of all the limits."""
if criticality is None:
@ -799,7 +832,7 @@ class GlancesPluginModel:
# No key found, the raise an error
raise KeyError
def get_limit_action(self, criticality, stat_name=""):
def get_limit_action(self, criticality: str, stat_name: str = ""):
"""Return the tuple (action, repeat) for the alert.
- action is a command line
@ -821,7 +854,7 @@ class GlancesPluginModel:
# No key found, the raise an error
raise KeyError
def get_limit_log(self, stat_name, default_action=False):
def get_limit_log(self, stat_name: str, default_action: bool = False) -> bool:
"""Return the log tag for the alert."""
# Get the log tag for stat + header
# Example: network_wlan0_rx_log
@ -831,7 +864,13 @@ class GlancesPluginModel:
return self._limits[self.plugin_name + '_log'][0].lower() == 'true'
return default_action
def get_conf_value(self, value, header="", plugin_name=None, default=[]):
def get_conf_value(
self,
value: str,
header: str = "",
plugin_name: None = None,
default: Union[int, List[str], List[None], str] = [],
) -> Union[List[None], List[str], float, str]:
"""Return the configuration (header_) value for the current plugin.
...or the one given by the plugin_name var.
@ -864,7 +903,7 @@ class GlancesPluginModel:
j for j in [re.fullmatch(i.lower(), value.lower()) for i in self.get_conf_value('show', header=header)]
)
def is_hide(self, value, header=""):
def is_hide(self, value: str, header: str = "") -> bool:
"""Return True if the value is in the hide configuration list.
The hide configuration list is defined in the glances.conf file.
@ -877,26 +916,30 @@ class GlancesPluginModel:
j for j in [re.fullmatch(i.lower(), value.lower()) for i in self.get_conf_value('hide', header=header)]
)
def is_display(self, value, header=""):
def is_display(self, value: str, header: str = "") -> bool:
"""Return True if the value should be displayed in the UI"""
if self.get_conf_value('show', header=header) != []:
return self.is_show(value, header=header)
return not self.is_hide(value, header=header)
def read_alias(self):
def read_alias(self) -> Dict[Any, Any]:
if self.plugin_name + '_' + 'alias' in self._limits:
return {i.split(':')[0].lower(): i.split(':')[1] for i in self._limits[self.plugin_name + '_' + 'alias']}
return {}
def has_alias(self, header):
"""Return the alias name for the relative header it it exists otherwise None."""
def has_alias(self, header: str) -> str | None:
"""Return the alias name for the relative header if it exists otherwise None."""
return self.alias.get(header, None)
def msg_curse(self, args=None, max_width=None):
def msg_curse(
self, args: Optional[Namespace] = None, max_width: Union[int, None] = None
) -> List[Dict[str, Union[str, bool]]]:
"""Return default string to display in the curse interface."""
return [self.curse_add_line(str(self.stats))]
def get_stats_display(self, args=None, max_width=None):
def get_stats_display(
self, args: Optional[Namespace] = None, max_width: Union[int, None] = None
) -> Dict[str, Union[bool, List[Dict[str, Union[str, bool]]], str]]:
"""Return a dict with all the information needed to display the stat.
key | description
@ -919,7 +962,14 @@ class GlancesPluginModel:
return ret
def curse_add_line(self, msg, decoration="DEFAULT", optional=False, additional=False, splittable=False):
def curse_add_line(
self,
msg: str,
decoration: str = "DEFAULT",
optional: bool = False,
additional: bool = False,
splittable: bool = False,
) -> Dict[str, Union[str, bool]]:
"""Return a dict with.
Where:
@ -953,11 +1003,19 @@ class GlancesPluginModel:
'splittable': splittable,
}
def curse_new_line(self):
def curse_new_line(self) -> Dict[str, Union[str, bool]]:
"""Go to a new line."""
return self.curse_add_line('\n')
def curse_add_stat(self, key, width=None, header='', display_key=True, separator='', trailer=''):
def curse_add_stat(
self,
key: str,
width: Optional[int] = None,
header: str = '',
display_key: bool = True,
separator: str = '',
trailer: str = '',
) -> List[Dict[str, Union[str, bool]]]:
"""Return a list of dict messages with the 'key: value' result
<=== width ===>
@ -1068,7 +1126,9 @@ class GlancesPluginModel:
"""
self._align = value
def auto_unit(self, number, low_precision=False, min_symbol='K', none_symbol='-'):
def auto_unit(
self, number: Optional[int], low_precision: bool = False, min_symbol: str = 'K', none_symbol: str = '-'
) -> str:
"""Make a nice human-readable string out of number.
Number of decimal places increases as quantity approaches 1.
@ -1123,7 +1183,7 @@ class GlancesPluginModel:
return '{:.{decimal}f}{symbol}'.format(value, decimal=decimal_precision, symbol=symbol)
return f'{number!s}'
def trend_msg(self, trend, significant=1):
def trend_msg(self, trend: None, significant: int = 1) -> str:
"""Return the trend message.
Do not take into account if trend < significant
@ -1137,7 +1197,7 @@ class GlancesPluginModel:
ret = unicode_message('ARROW_DOWN', self.args)
return ret
def _check_decorator(fct):
def _check_decorator(fct: Callable) -> Callable:
"""Check decorator for update method.
It checks:
@ -1160,7 +1220,7 @@ class GlancesPluginModel:
return wrapper
def _log_result_decorator(fct):
def _log_result_decorator(fct: Callable) -> Callable:
"""Log (DEBUG) the result of the function fct."""
def wrapper(*args, **kw):
@ -1174,7 +1234,7 @@ class GlancesPluginModel:
return wrapper
def _manage_rate(fct):
def _manage_rate(fct: Callable) -> Callable:
"""Manage rate decorator for update method."""
def compute_rate(self, stat, stat_previous):