Source code for gulik.helpers

# -*- coding: utf-8 -*-

import colorsys


[docs]def pretty_si(number): """ Return a SI-postfixed string representation of a number (int or float). """ postfixes = ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'] value = number for postfix in postfixes: if value / 1000.0 < 1: break value /= 1000.0 return "%.2f%s" % (value, postfix)
[docs]def pretty_bytes(bytecount): """ Return a human-readable representation given a size in bytes. """ units = ['Byte', 'kbyte', 'Mbyte', 'Gbyte', 'Tbyte'] value = bytecount for unit in units: if value / 1024.0 < 1: break value /= 1024.0 return "%.2f %s" % (value, unit)
[docs]def pretty_bits(bytecount): """ Return a human-readable representation in bits given a size in bytes. """ units = ['bit', 'kbit', 'Mbit', 'Gbit', 'Tbit'] value = bytecount * 8 # bytes to bits for unit in units: if value / 1024.0 < 1: break value /= 1024.0 return "%.2f %s" % (value, unit)
def pretty_time(seconds): days = int(seconds / 86400) seconds = seconds % 86400 hours = int(seconds / 3600) seconds = seconds % 3600 minutes = int(seconds / 60) seconds = seconds % 60 parts = [] if days > 0: parts.append(f"{days}d") if hours > 0: parts.append(f"{hours}h") if minutes > 0: parts.append(f"{minutes}m") parts.append("{seconds}s") return " ".join(parts)
[docs]def ignore_none(*args): """ Return the first passed value that isn't ``None``. """ for arg in args: if not arg is None: return arg
def condense_addr_parts(items): matching = [] max_match = min([len(x) for x in items]) for i in range(0, max_match): s = set() for item_idx in range(0, len(items)): s.add(items[item_idx][i]) if len(s) > 1: # lazy hack, means not all items have the same part at index i break matching.append(items[item_idx][i]) return '.'.join(matching) def alignment_offset(align, size): x_align, y_align = align.split('_') if x_align == 'left': x_offset = 0 elif x_align == 'center': x_offset = -size[0] / 2 elif x_align == 'right': x_offset = -size[0] else: raise ValueError("unknown horizontal alignment: '%s', must be one of: left, center, right" % x_align) if y_align == 'top': y_offset = 0 elif y_align == 'center': y_offset = -size[1] / 2 elif y_align == 'bottom': y_offset = -size[1] else: raise ValueError("unknown horizontal alignment: '%s', must be one of: top, center, bottom" % y_align) return (x_offset, y_offset)
[docs]class Color(object): """ Magic color class implementing and supplying on-the-fly manipulation of RGB and HSV (and alpha) attributes. """ def __init__(self, red=None, green=None, blue=None, alpha=None, hue=None, saturation=None, value=None): rgb_passed = bool(red)|bool(green)|bool(blue) hsv_passed = bool(hue)|bool(saturation)|bool(value) if not alpha: alpha = 0.0 if rgb_passed and hsv_passed: raise ValueError("Color can't be initialized with RGB and HSV at the same time.") elif hsv_passed: if not hue: hue = 0.0 if not saturation: saturation = 0.0 if not value: value = 0.0 super(Color, self).__setattr__('hue', hue) super(Color, self).__setattr__('saturation', saturation) super(Color, self).__setattr__('value', value) self._update_rgb() else: if not red: red = 0 if not green: green = 0 if not blue: blue = 0 super(Color, self).__setattr__('red', red) super(Color, self).__setattr__('green', green) super(Color, self).__setattr__('blue', blue) self._update_hsv() super(Color, self).__setattr__('alpha', alpha) def __setattr__(self, key, value): if key in ('red', 'green', 'blue'): if value > 1.0: value = value % 1.0 super(Color, self).__setattr__(key, value) self._update_hsv() elif key in ('hue', 'saturation', 'value'): if key == 'hue' and (value >= 360.0 or value < 0): value = value % 360.0 elif key != 'hue' and value > 1.0: value = 1.0 super(Color, self).__setattr__(key, value) self._update_rgb() else: if key == 'alpha' and value > 1.0: # TODO: Might this be more fitting in another place? value = 1.0 super(Color, self).__setattr__(key, value) def __repr__(self): return '<%s: red %f, green %f, blue %f, hue %f, saturation %f, value %f, alpha %f>' % ( self.__class__.__name__, self.red, self.green, self.blue, self.hue, self.saturation, self.value, self.alpha ) def clone(self): return Color(red=self.red, green=self.green, blue=self.blue, alpha=self.alpha) def blend(self, other, mode='normal'): if self.alpha != 1.0: # no clue how to blend with a translucent bottom layer self.red = self.red * self.alpha self.green = self.green * self.alpha self.blue = self.blue * self.alpha self.alpha = 1.0 if mode == 'normal': own_influence = 1.0 - other.alpha self.red = (self.red * own_influence) + (other.red * other.alpha) self.green = (self.green * own_influence) + (other.green * other.alpha) self.blue = (self.blue * own_influence) + (other.blue * other.alpha) def lighten(self, other): if isinstance(other, int) or isinstance(other, float): other = Color(red=other, green=other, blue=other, alpha=1.0) if self.alpha != 1.0: self.red = self.red * self.alpha self.green = self.green * self.alpha self.blue = self.blue * self.alpha self.alpha = 1.0 red = self.red + (other.red * other.alpha) green = self.green + (other.green * other.alpha) blue = self.blue + (other.blue * other.alpha) if red > 1.0: red = 1.0 if green > 1.0: green = 1.0 if blue > 1.0: blue = 1.0 self.red = red self.green = green self.blue = blue def darken(self, other): if isinstance(other, int) or isinstance(other, float): other = Color(red=other, green=other, blue=other, alpha=1.0) red = self.red - other.red green = self.green - other.green blue = self.blue - other.blue if red < 0: red = 0 if green < 0: green = 0 if blue < 0: blue = 0 self.red = red self.green = green self.blue = blue
[docs] def tuple_rgb(self): """ return color (without alpha) as tuple, channels being float 0.0-1.0 """ return (self.red, self.green, self.blue)
[docs] def tuple_rgba(self): """ return color (*with* alpha) as tuple, channels being float 0.0-1.0 """ return (self.red, self.green, self.blue, self.alpha)
def _update_hsv(self): hue, saturation, value = colorsys.rgb_to_hsv(self.red, self.green, self.blue) super(Color, self).__setattr__('hue', hue * 360.0) super(Color, self).__setattr__('saturation', saturation) super(Color, self).__setattr__('value', value) def _update_rgb(self): red, green, blue = colorsys.hsv_to_rgb(self.hue / 360.0, self.saturation, self.value) super(Color, self).__setattr__('red', red) super(Color, self).__setattr__('green', green) super(Color, self).__setattr__('blue', blue)
[docs]class DotDict(dict): """ A dictionary with its data being readable through faked attributes. Used to avoid [[[][][][][]] in caption formatting. """ def __getattribute__(self, name): #data = super(DotDict, self).__getattribute__('data') keys = super(DotDict, self).keys() if name in keys: return self.get(name) return super(DotDict, self).__getattribute__(name)
[docs]class Box(object): """ Can wrap multiple :ref:`visualizer`\s, used for layouting. Orders added visualizers from left to right and top to bottom. This is basically a smart helper for :func:`Gulik.add_visualizer`. """ def __init__(self, app, x, y, width, height): self._last_right = 0 self._last_top = 0 self._last_bottom = 0 self._next_row_x = 0 self.app = app self.x = x self.y = y self.width = width self.height = height
[docs] def place(self, component, cls, **kwargs): """ place a new :ref:`visualizer`. Parameters: component (str): The :ref:`component` string identifying the data source. cls (:class:`type`, child of :class:`visualizers.Visualizer`): The visualizer class to instantiate. **kwargs: passed on to ``cls``\' constructor. width and height defined in here are honored. """ width = kwargs.get('width', None) height = kwargs.get('height', None) if width is None: width = self.width - self._last_right if height is None: height = self._last_bottom - self._last_top # same height as previous visualizer if height == 0: # should only happen on first visualizer height = self.height elif height is None: height = self.height - self._last_bottom if self._last_right + width > self.width: # move to next "row" #print("next row", component, cls, kwargs) x = self._next_row_x y = self._last_bottom self._next_row_x = 0 # this will probably break adding a third multi-stacked column, but works for now else: x = self._last_right y = self._last_top kwargs['x'] = self.x + x kwargs['y'] = self.y + y kwargs['width'] = width kwargs['height'] = height self.app.add_visualizer(component, cls, **kwargs) if y + height < self._last_bottom: self._next_row_x = self._last_right self._last_right = x + width self._last_top = y self._last_bottom = y + height assert self._last_bottom <= self.y + self.height, f"Box already full! Can't place {cls.__name__}"