Skip to content
Snippets Groups Projects
conditions.py 4.17 KiB
import functools
import operator
import typing as tp
from abc import ABCMeta, abstractmethod

import psutil

__all__ = ['GB', 'MB', 'KB', 'Any', 'All', 'GlobalAbsoluteValue', 'GlobalRelativeValue',
           'LocalRelativeValue', 'LocalAbsoluteValue', 'BaseCondition', 'ZerothSeverity',
           'CustomCondition', 'Not']

from satella.coding.typing import NoArgCallable

GB = 1024 * 1024 * 1024
MB = 1024 * 1024
KB = 1024


class BaseCondition(metaclass=ABCMeta):
    __slots__ = ()

    @abstractmethod
    def can_fire(self, local_memory_data, local_maximum_consume: tp.Optional[int]) -> bool:
        """Has this severity level been reached?"""


class MemoryCondition(BaseCondition, metaclass=ABCMeta):
    __slots__ = ('value',)

    def __init__(self, value: int):
        self.value = value


class ZerothSeverity(BaseCondition):
    __slots__ = ()

    def can_fire(self, local_memory_data, local_maximum_consume: tp.Optional[int]) -> bool:
        return True


class OperationJoin(BaseCondition):
    __slots__ = 'conditions',

    _OPERATOR = lambda y, z: y and z        # pylint: disable=invalid-name
    _STARTING_VALUE = False                 # pylint: disable=invalid-name

    def __init__(self, *conditions: BaseCondition):
        self.conditions = conditions

    def can_fire(self, local_memory_data, local_maximum_consume: tp.Optional[int]) -> bool:
        return functools.reduce(self._OPERATOR, (
            condition.can_fire(local_memory_data, local_maximum_consume) for condition in
            self.conditions), self._STARTING_VALUE)


class Any(OperationJoin):
    """This is true if one of the arguments is True"""
    __slots__ = ()

    _OPERATOR = operator.or_
    _STARTING_VALUE = False


class All(OperationJoin):
    """This is true if all arguments are True"""
    __slots__ = ()

    _OPERATOR = operator.and_
    _STARTING_VALUE = True


class Not(BaseCondition):
    """True only if provided condition is false"""
    __slots__ = 'condition',

    def __init__(self, condition: BaseCondition):
        self.condition = condition

    def can_fire(self, local_memory_data, local_maximum_consume: tp.Optional[int]) -> bool:
        return not self.condition.can_fire(local_memory_data, local_maximum_consume)


class GlobalAbsoluteValue(MemoryCondition):
    """If free memory globally falls below this many bytes, given severity level starts"""
    __slots__ = ()

    def can_fire(self, local_memory_data, local_maximum_consume: tp.Optional[int]) -> bool:
        return psutil.virtual_memory().available < self.value


class GlobalRelativeValue(MemoryCondition):
    """
    If percentage of global free memory falls below this much percents, given severity level starts
    """
    __slots__ = ()

    def can_fire(self, local_memory_data, local_maximum_consume: tp.Optional[int]) -> bool:
        return psutil.virtual_memory().available / psutil.virtual_memory().total < (
                self.value / 100)


class LocalAbsoluteValue(MemoryCondition):
    """
    If free memory falls below this many bytes from what the program can maximally consume this
    severity level starts
    """
    __slots__ = ()

    def can_fire(self, local_memory_data, local_maximum_consume: tp.Optional[int]) -> bool:
        return local_maximum_consume - local_memory_data.rss < self.value


class LocalRelativeValue(MemoryCondition):
    """
    If percentage of memory available to this process in regards to what the program can
    maximally consume falls below this level, given severity level starts
    """
    __slots__ = ()

    def can_fire(self, local_memory_data, local_maximum_consume: tp.Optional[int]) -> bool:
        return local_memory_data.rss / local_maximum_consume < (1 - self.value / 100)


class CustomCondition(BaseCondition):
    """
    A custom condition. Condition that is true if attached callable/0 returns True.

    :param callable_: callable to call upon asking whether this condition is valid. This
        should be relatively cheap to compute.
    """
    __slots__ = 'callable',

    def __init__(self, callable_: NoArgCallable[bool]):
        self.callable = callable_

    def can_fire(self, local_memory_data, local_maximum_consume: tp.Optional[int]) -> bool:
        return self.callable()