#!/usr/bin/env python3 from abc import ABC, abstractmethod import logging import time from typing import Dict, Optional, Set from pyutils.decorator_utils import invocation_logged logger = logging.getLogger(__name__) class renderer(ABC): """Base class for something that can render.""" @abstractmethod def render(self): pass @abstractmethod def get_name(self): pass class abstaining_renderer(renderer): """A renderer that doesn't do it all the time.""" def __init__(self, name_to_timeout_dict: Dict[str, int]) -> None: self.name_to_timeout_dict = name_to_timeout_dict self.last_runs = {} for key in name_to_timeout_dict: self.last_runs[key] = 0.0 def should_render(self, keys_to_skip: Set[str]) -> Optional[str]: now = time.time() for key in self.name_to_timeout_dict: if ( (now - self.last_runs[key]) > self.name_to_timeout_dict[key] ) and key not in keys_to_skip: return key return None @invocation_logged def render(self) -> None: tries_per_key: Dict[str, int] = {} keys_to_skip: Set[str] = set() while True: key = self.should_render(keys_to_skip) if key is None: logger.info(f'Found nothing to do in "{self.get_name()}"; returning.') break if key in tries_per_key: tries_per_key[key] += 1 else: tries_per_key[key] = 0 op = f"{self.get_name()}.{key}" if tries_per_key[key] >= 3: logger.warning(f'Too many failures in "{op}"; giving up.') keys_to_skip.add(key) else: msg = f'Executing "{op}"' if tries_per_key[key] > 1: msg = msg + f" (retry #{tries_per_key[key]})" logger.info(msg) if self.periodic_render(key): logger.debug(f'"{op}" succeeded.') self.last_runs[key] = time.time() else: logger.warning(f'"{op}" failed; returned False.') @invocation_logged @abstractmethod def periodic_render(self, key) -> bool: pass def get_name(self) -> str: return self.__class__.__name__