summaryrefslogtreecommitdiff
path: root/robusta_krr
diff options
context:
space:
mode:
authorПавел Жуков <33721692+LeaveMyYard@users.noreply.github.com>2023-05-26 23:02:47 +0300
committerПавел Жуков <33721692+LeaveMyYard@users.noreply.github.com>2023-05-26 23:02:47 +0300
commited73cfbd95b9222c57950ff727397e251ebca247 (patch)
tree3c0bd4a24db102f73a9843cea5e63ebd0b77e056 /robusta_krr
parent0609ae5b62947b0d55505dec23b9b4d3a5909d89 (diff)
Refactor Formatters, use functional approach
Diffstat (limited to 'robusta_krr')
-rw-r--r--robusta_krr/api/formatters.py4
-rw-r--r--robusta_krr/core/abstract/formatters.py73
-rw-r--r--robusta_krr/core/models/config.py8
-rw-r--r--robusta_krr/core/models/result.py9
-rw-r--r--robusta_krr/formatters/__init__.py8
-rw-r--r--robusta_krr/formatters/json.py22
-rw-r--r--robusta_krr/formatters/pprint.py22
-rw-r--r--robusta_krr/formatters/table.py171
-rw-r--r--robusta_krr/formatters/yaml.py23
-rw-r--r--robusta_krr/main.py5
10 files changed, 146 insertions, 199 deletions
diff --git a/robusta_krr/api/formatters.py b/robusta_krr/api/formatters.py
index 3bd6927..d942efe 100644
--- a/robusta_krr/api/formatters.py
+++ b/robusta_krr/api/formatters.py
@@ -1,3 +1,3 @@
-from robusta_krr.core.abstract.formatters import BaseFormatter
+from robusta_krr.core.abstract.formatters import register, find, list_available
-__all__ = ["BaseFormatter"]
+__all__ = ["register", "find", "list_available"]
diff --git a/robusta_krr/core/abstract/formatters.py b/robusta_krr/core/abstract/formatters.py
index a38e9f3..3422c4d 100644
--- a/robusta_krr/core/abstract/formatters.py
+++ b/robusta_krr/core/abstract/formatters.py
@@ -1,62 +1,55 @@
from __future__ import annotations
-import abc
-import os
-from typing import TYPE_CHECKING, Any, TypeVar
+from typing import Any, Optional, Callable
-from robusta_krr.utils.display_name import add_display_name
+from robusta_krr.core.models.result import Result
-if TYPE_CHECKING:
- from robusta_krr.core.models.result import Result
+FormatterFunc = Callable[[Result], Any]
-DEFAULT_FORMATTERS_PATH = os.path.join(os.path.dirname(__file__), "formatters")
+FORMATTERS_REGISTRY: dict[str, FormatterFunc] = {}
-Self = TypeVar("Self", bound="BaseFormatter")
+# NOTE: Here asterisk is used to make the argument `rich_console` keyword-only
+# This is done to avoid the following usage, where it is unclear what the boolean value is for:
+# @register("My Formatter", True)
+# def my_formatter(result: Result) -> str:
+# return "My formatter"
+#
+# Instead, the following usage is enforced:
+# @register("My Formatter", rich_console=True)
+# def my_formatter(result: Result) -> str:
+# return "My formatter"
+def register(display_name: Optional[str] = None, *, rich_console: bool = False) -> Callable[[FormatterFunc], FormatterFunc]:
+ """Decorator to register a formatter."""
-@add_display_name(postfix="Formatter")
-class BaseFormatter(abc.ABC):
- """Base class for result formatters."""
+ def decorator(func: FormatterFunc) -> FormatterFunc:
+ name = display_name or func.__name__
- __display_name__: str
- __rich_console__: bool = False
+ FORMATTERS_REGISTRY[name] = func
- def __str__(self) -> str:
- return self.__display_name__.title()
+ func.__display_name__ = name # type: ignore
+ func.__rich_console__ = rich_console # type: ignore
- @abc.abstractmethod
- def format(self, result: Result) -> Any:
- """Format the result.
+ return func
- Args:
- result: The result to format.
+ return decorator
- Returns:
- The formatted result.
- """
- @classmethod
- def get_all(cls: type[Self]) -> dict[str, type[Self]]:
- """Get all available formatters."""
+def find(name: str) -> FormatterFunc:
+ """Find a formatter by name."""
- # NOTE: Load default formatters
- from robusta_krr import formatters as _ # noqa: F401
+ try:
+ return FORMATTERS_REGISTRY[name]
+ except KeyError as e:
+ raise ValueError(f"Formatter '{name}' not found") from e
- return {sub_cls.__display_name__.lower(): sub_cls for sub_cls in cls.__subclasses__()}
- @staticmethod
- def find(name: str) -> type[BaseFormatter]:
- """Get a strategy from its name."""
+def list_available() -> list[str]:
+ """List available formatters."""
- formatters = BaseFormatter.get_all()
+ return list(FORMATTERS_REGISTRY)
- l_name = name.lower()
- if l_name in formatters:
- return formatters[l_name]
- raise ValueError(f"Unknown formatter name: {name}. Available formatters: {', '.join(formatters)}")
-
-
-__all__ = ["BaseFormatter"]
+__all__ = ["register", "find"]
diff --git a/robusta_krr/core/models/config.py b/robusta_krr/core/models/config.py
index ff147af..6a79a19 100644
--- a/robusta_krr/core/models/config.py
+++ b/robusta_krr/core/models/config.py
@@ -4,7 +4,7 @@ import pydantic as pd
from kubernetes import config
from kubernetes.config.config_exception import ConfigException
-from robusta_krr.core.abstract.formatters import BaseFormatter
+from robusta_krr.core.abstract import formatters
from robusta_krr.core.abstract.strategies import AnyStrategy, BaseStrategy
try:
@@ -44,8 +44,8 @@ class Config(pd.BaseSettings):
other_args: dict[str, Any]
@property
- def Formatter(self) -> type[BaseFormatter]:
- return BaseFormatter.find(self.format)
+ def Formatter(self) -> formatters.FormatterFunc:
+ return formatters.find(self.format)
@pd.validator("namespaces")
def validate_namespaces(cls, v: Union[list[str], Literal["*"]]) -> Union[list[str], Literal["*"]]:
@@ -66,7 +66,7 @@ class Config(pd.BaseSettings):
@pd.validator("format")
def validate_format(cls, v: str) -> str:
- BaseFormatter.find(v) # NOTE: raises if strategy is not found
+ formatters.find(v) # NOTE: raises if strategy is not found
return v
@property
diff --git a/robusta_krr/core/models/result.py b/robusta_krr/core/models/result.py
index f3df5d7..6e860b7 100644
--- a/robusta_krr/core/models/result.py
+++ b/robusta_krr/core/models/result.py
@@ -7,7 +7,7 @@ from typing import Any, Optional, Union
import pydantic as pd
-from robusta_krr.core.abstract.formatters import BaseFormatter
+from robusta_krr.core.abstract import formatters
from robusta_krr.core.models.allocations import RecommendationValue, ResourceAllocations, ResourceType
from robusta_krr.core.models.objects import K8sObjectData
@@ -115,7 +115,7 @@ class Result(pd.BaseModel):
super().__init__(*args, **kwargs)
self.score = self.__calculate_score()
- def format(self, formatter: Union[type[BaseFormatter], str], **kwargs: Any) -> Any:
+ def format(self, formatter: Union[formatters.FormatterFunc, str]) -> Any:
"""Format the result.
Args:
@@ -125,9 +125,8 @@ class Result(pd.BaseModel):
The formatted result.
"""
- FormatterType = BaseFormatter.find(formatter) if isinstance(formatter, str) else formatter
- _formatter = FormatterType(**kwargs)
- return _formatter.format(self)
+ formatter = formatters.find(formatter) if isinstance(formatter, str) else formatter
+ return formatter(self)
@staticmethod
def __percentage_difference(current: RecommendationValue, recommended: RecommendationValue) -> float:
diff --git a/robusta_krr/formatters/__init__.py b/robusta_krr/formatters/__init__.py
index 0fc1c80..325cf01 100644
--- a/robusta_krr/formatters/__init__.py
+++ b/robusta_krr/formatters/__init__.py
@@ -1,4 +1,4 @@
-from .json import JSONFormatter
-from .pprint import PPrintFormatter
-from .table import TableFormatter
-from .yaml import YAMLFormatter
+from .json import json
+from .pprint import pprint
+from .table import table
+from .yaml import yaml
diff --git a/robusta_krr/formatters/json.py b/robusta_krr/formatters/json.py
index 2c3a51e..c391da2 100644
--- a/robusta_krr/formatters/json.py
+++ b/robusta_krr/formatters/json.py
@@ -1,21 +1,7 @@
-from __future__ import annotations
-
-from robusta_krr.core.abstract.formatters import BaseFormatter
+from robusta_krr.core.abstract import formatters
from robusta_krr.core.models.result import Result
-class JSONFormatter(BaseFormatter):
- """Formatter for JSON output."""
-
- __display_name__ = "json"
-
- def format(self, result: Result) -> str:
- """Format the result as JSON.
-
- :param result: The results to format.
- :type result: :class:`core.result.Result`
- :returns: The formatted results.
- :rtype: str
- """
-
- return result.json(indent=2)
+@formatters.register()
+def json(result: Result) -> str:
+ return result.json(indent=2)
diff --git a/robusta_krr/formatters/pprint.py b/robusta_krr/formatters/pprint.py
index bdfcc4c..9be637c 100644
--- a/robusta_krr/formatters/pprint.py
+++ b/robusta_krr/formatters/pprint.py
@@ -1,23 +1,9 @@
-from __future__ import annotations
-
from pprint import pformat
-from robusta_krr.core.abstract.formatters import BaseFormatter
+from robusta_krr.core.abstract import formatters
from robusta_krr.core.models.result import Result
-class PPrintFormatter(BaseFormatter):
- """Formatter for object output with python's pprint module."""
-
- __display_name__ = "pprint"
-
- def format(self, result: Result) -> str:
- """Format the result using pprint.pformat(...)
-
- :param result: The results to format.
- :type result: :class:`core.result.Result`
- :returns: The formatted results.
- :rtype: str
- """
-
- return pformat(result.dict())
+@formatters.register()
+def pprint(result: Result) -> str:
+ return pformat(result.dict())
diff --git a/robusta_krr/formatters/table.py b/robusta_krr/formatters/table.py
index 8ef1696..9776d80 100644
--- a/robusta_krr/formatters/table.py
+++ b/robusta_krr/formatters/table.py
@@ -1,10 +1,8 @@
-from __future__ import annotations
-
import itertools
from rich.table import Table
-from robusta_krr.core.abstract.formatters import BaseFormatter
+from robusta_krr.core.abstract import formatters
from robusta_krr.core.models.allocations import RecommendationValue
from robusta_krr.core.models.result import ResourceScan, ResourceType, Result
from robusta_krr.utils import resource_units
@@ -13,88 +11,85 @@ NONE_LITERAL = "unset"
NAN_LITERAL = "?"
-class TableFormatter(BaseFormatter):
- """Formatter for text output."""
-
- __display_name__ = "table"
- __rich_console__ = True
-
- def _format(self, value: RecommendationValue) -> str:
- if value is None:
- return NONE_LITERAL
- elif isinstance(value, str):
- return NAN_LITERAL
- else:
- return resource_units.format(value)
-
- def _format_request_str(self, item: ResourceScan, resource: ResourceType, selector: str) -> str:
- allocated = getattr(item.object.allocations, selector)[resource]
- recommended = getattr(item.recommended, selector)[resource]
- severity = recommended.severity
-
- return (
- f"[{severity.color}]"
- + self._format(allocated)
- + " -> "
- + self._format(recommended.value)
- + f"[/{severity.color}]"
- )
-
- def format(self, result: Result) -> Table:
- """Format the result as text.
-
- :param result: The result to format.
- :type result: :class:`core.result.Result`
- :returns: The formatted results.
- :rtype: str
- """
-
- table = Table(
- show_header=True,
- header_style="bold magenta",
- title=f"\n{result.description}\n" if result.description else None,
- title_justify="left",
- title_style="",
- # TODO: Fix points calculation at [MAIN-270]
- # caption=f"Scan result ({result.score} points)",
- )
-
- table.add_column("Number", justify="right", no_wrap=True)
- table.add_column("Cluster", style="cyan")
- table.add_column("Namespace", style="cyan")
- table.add_column("Name", style="cyan")
- table.add_column("Pods", style="cyan")
- table.add_column("Old Pods", style="cyan")
- table.add_column("Type", style="cyan")
- table.add_column("Container", style="cyan")
- for resource in ResourceType:
- table.add_column(f"{resource.name} Requests")
- table.add_column(f"{resource.name} Limits")
-
- for _, group in itertools.groupby(
- enumerate(result.scans), key=lambda x: (x[1].object.cluster, x[1].object.namespace, x[1].object.name)
- ):
- group_items = list(group)
-
- for j, (i, item) in enumerate(group_items):
- last_row = j == len(group_items) - 1
- full_info_row = j == 0
-
- table.add_row(
- f"[{item.severity.color}]{i + 1}.[/{item.severity.color}]",
- item.object.cluster if full_info_row else "",
- item.object.namespace if full_info_row else "",
- item.object.name if full_info_row else "",
- f"{item.object.current_pods_count}" if full_info_row else "",
- f"{item.object.deleted_pods_count}" if full_info_row else "",
- item.object.kind if full_info_row else "",
- item.object.container,
- *[
- self._format_request_str(item, resource, selector)
- for resource in ResourceType
- for selector in ["requests", "limits"]
- ],
- end_section=last_row,
- )
-
- return table
+def _format(value: RecommendationValue) -> str:
+ if value is None:
+ return NONE_LITERAL
+ elif isinstance(value, str):
+ return NAN_LITERAL
+ else:
+ return resource_units.format(value)
+
+
+def _format_request_str(item: ResourceScan, resource: ResourceType, selector: str) -> str:
+ allocated = getattr(item.object.allocations, selector)[resource]
+ recommended = getattr(item.recommended, selector)[resource]
+ severity = recommended.severity
+
+ return (
+ f"[{severity.color}]"
+ + _format(allocated)
+ + " -> "
+ + _format(recommended.value)
+ + f"[/{severity.color}]"
+ )
+
+
+@formatters.register(rich_console=True)
+def table(result: Result) -> Table:
+ """Format the result as text.
+
+ :param result: The result to format.
+ :type result: :class:`core.result.Result`
+ :returns: The formatted results.
+ :rtype: str
+ """
+
+ table = Table(
+ show_header=True,
+ header_style="bold magenta",
+ title=f"\n{result.description}\n" if result.description else None,
+ title_justify="left",
+ title_style="",
+ # TODO: Fix points calculation at [MAIN-270]
+ # caption=f"Scan result ({result.score} points)",
+ )
+
+ table.add_column("Number", justify="right", no_wrap=True)
+ table.add_column("Cluster", style="cyan")
+ table.add_column("Namespace", style="cyan")
+ table.add_column("Name", style="cyan")
+ table.add_column("Pods", style="cyan")
+ table.add_column("Old Pods", style="cyan")
+ table.add_column("Type", style="cyan")
+ table.add_column("Container", style="cyan")
+ for resource in ResourceType:
+ table.add_column(f"{resource.name} Requests")
+ table.add_column(f"{resource.name} Limits")
+
+ for _, group in itertools.groupby(
+ enumerate(result.scans), key=lambda x: (x[1].object.cluster, x[1].object.namespace, x[1].object.name)
+ ):
+ group_items = list(group)
+
+ for j, (i, item) in enumerate(group_items):
+ last_row = j == len(group_items) - 1
+ full_info_row = j == 0
+
+ table.add_row(
+ f"[{item.severity.color}]{i + 1}.[/{item.severity.color}]",
+ item.object.cluster if full_info_row else "",
+ item.object.namespace if full_info_row else "",
+ item.object.name if full_info_row else "",
+ f"{item.object.current_pods_count}" if full_info_row else "",
+ f"{item.object.deleted_pods_count}" if full_info_row else "",
+ item.object.kind if full_info_row else "",
+ item.object.container,
+ *[
+ _format_request_str(item, resource, selector)
+ for resource in ResourceType
+ for selector in ["requests", "limits"]
+ ],
+ end_section=last_row,
+ )
+
+ return table
diff --git a/robusta_krr/formatters/yaml.py b/robusta_krr/formatters/yaml.py
index c494bc1..37a030c 100644
--- a/robusta_krr/formatters/yaml.py
+++ b/robusta_krr/formatters/yaml.py
@@ -1,24 +1,11 @@
-from __future__ import annotations
-
import json
-import yaml
+import yaml as yaml_module
-from robusta_krr.core.abstract.formatters import BaseFormatter
+from robusta_krr.core.abstract import formatters
from robusta_krr.core.models.result import Result
-class YAMLFormatter(BaseFormatter):
- """Formatter for YAML output."""
-
- __display_name__ = "yaml"
-
- def format(self, result: Result) -> str:
- """Format the result as YAML.
-
- :param result: The results to format.
- :type result: :class:`core.result.Result`
- :returns: The formatted results.
- :rtype: str
- """
- return yaml.dump(json.loads(result.json()), sort_keys=False)
+@formatters.register()
+def yaml(result: Result) -> str:
+ return yaml_module.dump(json.loads(result.json()), sort_keys=False)
diff --git a/robusta_krr/main.py b/robusta_krr/main.py
index fdd654d..1734788 100644
--- a/robusta_krr/main.py
+++ b/robusta_krr/main.py
@@ -9,8 +9,9 @@ from uuid import UUID
import typer
import urllib3
-from robusta_krr.core.abstract.formatters import BaseFormatter
+from robusta_krr import formatters as concrete_formatters # noqa: F401
from robusta_krr.core.abstract.strategies import AnyStrategy, BaseStrategy
+from robusta_krr.core.abstract import formatters
from robusta_krr.core.models.config import Config
from robusta_krr.core.runner import Runner
from robusta_krr.utils.version import get_version
@@ -115,7 +116,7 @@ def load_commands() -> None:
f"'{field_name}': {field_name}" for field_name in strategy_type.get_settings_type().__fields__
)
+ "}",
- formatters=", ".join(BaseFormatter.get_all()),
+ formatters=", ".join(formatters.list_available()),
),
globals()
| {strategy.__name__: strategy for strategy in AnyStrategy.get_all().values()} # Defined strategies