| import logging |
| import subprocess |
| from typing import Any, Optional |
|
|
|
|
| def setup_logging(name: Optional[str] = None): |
| """Configure and setup logging for the application""" |
|
|
| formatter = logging.Formatter( |
| "%(asctime)s - %(name)s - %(levelname)s - %(message)s", |
| datefmt="%Y-%m-%d %H:%M:%S", |
| ) |
|
|
| console_handler = logging.StreamHandler() |
| console_handler.setFormatter(formatter) |
|
|
| logger = logging.getLogger(name or __name__) |
| logger.setLevel(logging.INFO) |
|
|
| if not logger.handlers: |
| logger.addHandler(console_handler) |
|
|
| return logger |
|
|
|
|
| class KernelBotError(Exception): |
| """ |
| This class represents an Exception that has been sanitized, |
| i.e., whose message can be safely displayed to the user without |
| risk of leaking internal bot details. |
| """ |
|
|
| def __init__(self, message, code: int = 400): |
| super().__init__(message) |
| self.http_code = code |
|
|
|
|
| def get_github_branch_name(): |
| try: |
| result = subprocess.run( |
| ["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], |
| capture_output=True, |
| text=True, |
| check=True, |
| ) |
| return result.stdout.strip().split("/", 1)[1] |
| except subprocess.CalledProcessError: |
| return "main" |
|
|
|
|
| class LRUCache: |
| def __init__(self, max_size: int): |
| """LRU Cache implementation, as functools.lru doesn't work in async code |
| Note: Implementation uses list for convenience because cache is small, so |
| runtime complexity does not matter here. |
| Args: |
| max_size (int): Maximum size of the cache |
| """ |
| self._cache = {} |
| self._max_size = max_size |
| self._q = [] |
|
|
| def __getitem__(self, key: Any) -> Any | None: |
| if key not in self._cache: |
| return None |
|
|
| self._q.remove(key) |
| self._q.append(key) |
| return self._cache[key] |
|
|
| def __setitem__(self, key: Any, value: Any) -> None: |
| if key in self._cache: |
| self._q.remove(key) |
| self._q.append(key) |
| self._cache[key] = value |
| return |
|
|
| if len(self._cache) >= self._max_size: |
| self._cache.pop(self._q.pop(0)) |
|
|
| self._cache[key] = value |
| self._q.append(key) |
|
|
| def __contains__(self, key: Any) -> bool: |
| return key in self._cache |
|
|
| def __len__(self) -> int: |
| return len(self._cache) |
|
|
| def invalidate(self): |
| """Invalidate the cache, clearing all entries, should be called when updating the underlying |
| data in db |
| """ |
| self._cache.clear() |
| self._q.clear() |
|
|
|
|
| def format_time(nanoseconds: float | str, err: Optional[float | str] = None): |
| if nanoseconds is None: |
| logging.warning("Expected a number, got None", stack_info=True) |
| return "–" |
|
|
| |
| nanoseconds = float(nanoseconds) |
|
|
| scale = 1 |
| unit = "ns" |
| if nanoseconds > 2_000_000: |
| scale = 1000_000 |
| unit = "ms" |
| elif nanoseconds > 2000: |
| scale = 1000 |
| unit = "µs" |
|
|
| time_in_unit = nanoseconds / scale |
| if err is not None: |
| err = float(err) |
| err /= scale |
| if time_in_unit < 1: |
| if err: |
| return f"{time_in_unit} ± {err} {unit}" |
| else: |
| return f"{time_in_unit} {unit}" |
| elif time_in_unit < 10: |
| if err: |
| return f"{time_in_unit:.2f} ± {err:.3f} {unit}" |
| else: |
| return f"{time_in_unit:.2f} {unit}" |
| elif time_in_unit < 100: |
| if err: |
| return f"{time_in_unit:.1f} ± {err:.2f} {unit}" |
| else: |
| return f"{time_in_unit:.1f} {unit}" |
| else: |
| if err: |
| return f"{time_in_unit:.0f} ± {err:.1f} {unit}" |
| else: |
| return f"{time_in_unit:.0f} {unit}" |
|
|
|
|
| def limit_length(text: str, maxlen: int): |
| assert maxlen > 6 |
| if len(text) > maxlen: |
| return text[: maxlen - 6] + " [...]" |
| else: |
| return text |
|
|