[core] restructure to allow PIP packaging
OK - so I have to admit I *hate* the fact that PIP seems to require a subdirectory named like the library. But since the PIP package is something really nifty to have (thanks to @tony again!!!), I updated the codebase to hopefully conform with what PIP expects. Testruns so far look promising...
This commit is contained in:
parent
1d25be2059
commit
320827d577
146 changed files with 2509 additions and 2 deletions
0
bumblebee_status/util/__init__.py
Normal file
0
bumblebee_status/util/__init__.py
Normal file
30
bumblebee_status/util/algorithm.py
Normal file
30
bumblebee_status/util/algorithm.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
import copy
|
||||
|
||||
|
||||
def merge(target, *args):
|
||||
"""Merges arbitrary data - copied from http://blog.impressiver.com/post/31434674390/deep-merge-multiple-python-dicts
|
||||
|
||||
:param target: the data structure to fill
|
||||
:param args: a list of data structures to merge into target
|
||||
|
||||
:return: target, with all data in args merged into it
|
||||
:rtype: whatever type was originally passed in
|
||||
"""
|
||||
if len(args) > 1:
|
||||
for item in args:
|
||||
merge(target, item)
|
||||
return target
|
||||
|
||||
item = args[0]
|
||||
if not isinstance(item, dict):
|
||||
return item
|
||||
for key, value in item.items():
|
||||
if key in target and isinstance(target[key], dict):
|
||||
merge(target[key], value)
|
||||
else:
|
||||
if not key in target:
|
||||
target[key] = copy.deepcopy(value)
|
||||
return target
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
44
bumblebee_status/util/cli.py
Normal file
44
bumblebee_status/util/cli.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import logging
|
||||
|
||||
|
||||
def execute(cmd, wait=True, ignore_errors=False, include_stderr=False, env=None):
|
||||
"""Executes a commandline utility and returns its output
|
||||
|
||||
:param cmd: the command (as string) to execute, returns the program's output
|
||||
:param wait: set to True to wait for command completion, False to return immediately, defaults to True
|
||||
:param ignore_errors: set to True to return a string when an exception is thrown, otherwise might throw, defaults to False
|
||||
:param include_stderr: set to True to include stderr output in the return value, defaults to False
|
||||
:param env: provide a dict here to specify a custom execution environment, defaults to None
|
||||
|
||||
:raises RuntimeError: the command either didn't exist or didn't exit cleanly, and ignore_errors was set to False
|
||||
|
||||
:return: output of cmd, or stderr, if ignore_errors is True and the command failed
|
||||
:rtype: string
|
||||
"""
|
||||
args = shlex.split(cmd)
|
||||
logging.debug(cmd)
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
args,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT if include_stderr else subprocess.PIPE,
|
||||
env=env,
|
||||
)
|
||||
except FileNotFoundError as e:
|
||||
raise RuntimeError("{} not found".format(cmd))
|
||||
|
||||
if wait:
|
||||
out, _ = proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
err = "{} exited with code {}".format(cmd, proc.returncode)
|
||||
if ignore_errors:
|
||||
return err
|
||||
raise RuntimeError(err)
|
||||
return out.decode("utf-8")
|
||||
return ""
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
143
bumblebee_status/util/format.py
Normal file
143
bumblebee_status/util/format.py
Normal file
|
@ -0,0 +1,143 @@
|
|||
import re
|
||||
|
||||
|
||||
def asbool(val):
|
||||
"""Converts a value into a boolean
|
||||
|
||||
:param val: value to convert; accepts a wide range of
|
||||
possible representations, such as yes, no, true, false, on, off
|
||||
|
||||
:return: True of val maps to true, False otherwise
|
||||
:rtype: boolean
|
||||
"""
|
||||
if val is None:
|
||||
return False
|
||||
if isinstance(val, bool):
|
||||
return val
|
||||
val = str(val).strip().lower()
|
||||
return val in ("t", "true", "y", "yes", "on", "1")
|
||||
|
||||
|
||||
def asint(val, minimum=None, maximum=None):
|
||||
"""Converts a value into an integer
|
||||
|
||||
:param val: value to convert
|
||||
:param minimum: if specified, this determines the lower
|
||||
boundary for the returned value, defaults to None
|
||||
:param maximum: if specified, this determines the upper
|
||||
boundary for the returned value, defaults to None
|
||||
|
||||
:return: integer representation of value
|
||||
:rtype: integer
|
||||
"""
|
||||
if val is None:
|
||||
val = 0
|
||||
val = int(val)
|
||||
val = min(val, maximum if maximum else val)
|
||||
val = max(val, minimum if minimum else val)
|
||||
return val
|
||||
|
||||
|
||||
def aslist(val):
|
||||
"""Converts a comma-separated value string into a list
|
||||
|
||||
:param val: value to convert, either a single value or a comma-separated string
|
||||
|
||||
:return: list representation of the value passed in
|
||||
:rtype: list of strings
|
||||
"""
|
||||
if val is None:
|
||||
return []
|
||||
if isinstance(val, list):
|
||||
return val
|
||||
return str(val).replace(" ", "").split(",")
|
||||
|
||||
|
||||
__UNITS = {"metric": "C", "kelvin": "K", "imperial": "F", "default": "C"}
|
||||
|
||||
|
||||
def astemperature(val, unit="metric"):
|
||||
"""Returns a temperature representation of the input value
|
||||
|
||||
:param val: value to format, must be convertible into an integer
|
||||
:param unit: unit of the input value, supported units are:
|
||||
metric, kelvin, imperial, defaults to metric
|
||||
|
||||
:return: temperature representation of the input value
|
||||
:rtype: string
|
||||
"""
|
||||
return "{}°{}".format(int(val), __UNITS.get(unit, __UNITS["default"]))
|
||||
|
||||
|
||||
def byte(val, fmt="{:.2f}"):
|
||||
"""Returns a byte representation of the input value
|
||||
|
||||
:param val: value to format, must be convertible into a float
|
||||
:param fmt: optional output format string, defaults to {:.2f}
|
||||
|
||||
:return: byte representation (e.g. <X> KiB, GiB, etc.) of the input value
|
||||
:rtype: string
|
||||
"""
|
||||
|
||||
val = float(val)
|
||||
for unit in ["", "Ki", "Mi", "Gi"]:
|
||||
if val < 1024.0:
|
||||
return "{}{}B".format(fmt, unit).format(val)
|
||||
val /= 1024.0
|
||||
return "{}GiB".format(fmt).format(val * 1024.0)
|
||||
|
||||
|
||||
__seconds_pattern = re.compile("(([\d\.?]+)h)?(([\d\.]+)m)?([\d\.]+)?s?")
|
||||
|
||||
|
||||
def seconds(duration):
|
||||
"""Returns a time duration (in seconds) representation of the input value
|
||||
|
||||
:param duration: value to format (e.g. 5h30m2s)
|
||||
|
||||
:return: duration in seconds of the input value
|
||||
:rtype: float
|
||||
"""
|
||||
if isinstance(duration, int) or isinstance(duration, float):
|
||||
return float(duration)
|
||||
|
||||
matches = __seconds_pattern.match(duration)
|
||||
result = 0.0
|
||||
if matches.group(2):
|
||||
result += float(matches.group(2)) * 3600 # hours
|
||||
if matches.group(4):
|
||||
result += float(matches.group(4)) * 60 # minutes
|
||||
if matches.group(5):
|
||||
result += float(matches.group(5)) # seconds
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def duration(duration, compact=False, unit=False):
|
||||
"""Returns a time duration string representing the input value
|
||||
|
||||
:param duration: value to format, must be convertible into an into
|
||||
:param compact: whether to show also seconds, defaults to False
|
||||
:param unit: whether to display he unit, defaults to False
|
||||
|
||||
:return: duration representation (e.g. 5:02s) of the input value
|
||||
:rtype: string
|
||||
"""
|
||||
duration = int(duration)
|
||||
if duration < 0:
|
||||
return "n/a"
|
||||
minutes, seconds = divmod(duration, 60)
|
||||
hours, minutes = divmod(minutes, 60)
|
||||
suf = "m"
|
||||
res = "{:02d}:{:02d}".format(minutes, seconds)
|
||||
if hours > 0:
|
||||
if compact:
|
||||
res = "{:02d}:{:02d}".format(hours, minutes)
|
||||
else:
|
||||
res = "{:02d}:{}".format(hours, res)
|
||||
suf = "h"
|
||||
|
||||
return "{}{}".format(res, suf if unit else "")
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
220
bumblebee_status/util/graph.py
Normal file
220
bumblebee_status/util/graph.py
Normal file
|
@ -0,0 +1,220 @@
|
|||
MAX_PERCENTS = 100.0
|
||||
|
||||
|
||||
class Bar(object):
|
||||
"""superclass"""
|
||||
|
||||
bars = None
|
||||
|
||||
def __init__(self, value):
|
||||
"""
|
||||
Args:
|
||||
|
||||
value (float): value between 0. and 100. meaning percents
|
||||
"""
|
||||
self.value = value
|
||||
|
||||
|
||||
class HBar(Bar):
|
||||
"""horizontal bar (1 char)"""
|
||||
|
||||
bars = [
|
||||
"\u2581",
|
||||
"\u2582",
|
||||
"\u2583",
|
||||
"\u2584",
|
||||
"\u2585",
|
||||
"\u2586",
|
||||
"\u2587",
|
||||
"\u2588",
|
||||
]
|
||||
|
||||
def __init__(self, value):
|
||||
"""
|
||||
Args:
|
||||
|
||||
value (float): value between 0. and 100. meaning percents
|
||||
"""
|
||||
super(HBar, self).__init__(value)
|
||||
self.step = MAX_PERCENTS / len(HBar.bars)
|
||||
|
||||
def get_char(self):
|
||||
"""
|
||||
Decide which char to draw
|
||||
|
||||
Return: str
|
||||
"""
|
||||
for i in range(len(HBar.bars)):
|
||||
left = i * self.step
|
||||
right = (i + 1) * self.step
|
||||
if left <= self.value < right:
|
||||
return self.bars[i]
|
||||
return self.bars[-1]
|
||||
|
||||
|
||||
def hbar(value):
|
||||
"""wrapper function"""
|
||||
return HBar(value).get_char()
|
||||
|
||||
|
||||
class VBar(Bar):
|
||||
"""vertical bar (can be more than 1 char)"""
|
||||
|
||||
bars = [
|
||||
"\u258f",
|
||||
"\u258e",
|
||||
"\u258d",
|
||||
"\u258c",
|
||||
"\u258b",
|
||||
"\u258a",
|
||||
"\u2589",
|
||||
"\u2588",
|
||||
]
|
||||
|
||||
def __init__(self, value, width=1):
|
||||
"""
|
||||
Args:
|
||||
|
||||
value (float): value between 0. and 100. meaning percents
|
||||
|
||||
width (int): width
|
||||
"""
|
||||
super(VBar, self).__init__(value)
|
||||
self.step = MAX_PERCENTS / (len(VBar.bars) * width)
|
||||
self.width = width
|
||||
|
||||
def get_chars(self):
|
||||
"""
|
||||
Decide which char to draw
|
||||
|
||||
Return: str
|
||||
"""
|
||||
if self.value == 100:
|
||||
return self.bars[-1] * self.width
|
||||
if self.width == 1:
|
||||
for i in range(len(VBar.bars)):
|
||||
left = i * self.step
|
||||
right = (i + 1) * self.step
|
||||
if left <= self.value < right:
|
||||
return self.bars[i]
|
||||
else:
|
||||
full_parts = int(self.value // (self.step * len(Vbar.bars)))
|
||||
remainder = self.value - full_parts * self.step * CHARS
|
||||
empty_parts = self.width - full_parts
|
||||
if remainder >= 0:
|
||||
empty_parts -= 1
|
||||
part_vbar = VBar(remainder * self.width) # scale to width
|
||||
chars = self.bars[-1] * full_parts
|
||||
chars += part_vbar.get_chars()
|
||||
chars += " " * empty_parts
|
||||
return chars
|
||||
|
||||
|
||||
def vbar(value, width):
|
||||
"""wrapper function"""
|
||||
return VBar(value, width).get_chars()
|
||||
|
||||
|
||||
class BrailleGraph(object):
|
||||
"""
|
||||
graph using Braille chars
|
||||
scaled to passed values
|
||||
"""
|
||||
|
||||
chars = {
|
||||
(0, 0): " ",
|
||||
(1, 0): "\u2840",
|
||||
(2, 0): "\u2844",
|
||||
(3, 0): "\u2846",
|
||||
(4, 0): "\u2847",
|
||||
(0, 1): "\u2880",
|
||||
(0, 2): "\u28a0",
|
||||
(0, 3): "\u28b0",
|
||||
(0, 4): "\u28b8",
|
||||
(1, 1): "\u28c0",
|
||||
(2, 1): "\u28c4",
|
||||
(3, 1): "\u28c6",
|
||||
(4, 1): "\u28c7",
|
||||
(1, 2): "\u28e0",
|
||||
(2, 2): "\u28e4",
|
||||
(3, 2): "\u28e6",
|
||||
(4, 2): "\u28e7",
|
||||
(1, 3): "\u28f0",
|
||||
(2, 3): "\u28f4",
|
||||
(3, 3): "\u28f6",
|
||||
(4, 3): "\u28f7",
|
||||
(1, 4): "\u28f8",
|
||||
(2, 4): "\u28fc",
|
||||
(3, 4): "\u28fe",
|
||||
(4, 4): "\u28ff",
|
||||
}
|
||||
|
||||
def __init__(self, values):
|
||||
"""
|
||||
Args:
|
||||
|
||||
values (list): list of values
|
||||
"""
|
||||
self.values = values
|
||||
# length of values list must be even
|
||||
# because one Braille char displays two values
|
||||
if len(self.values) % 2 == 1:
|
||||
self.values.append(0)
|
||||
self.steps = self.get_steps()
|
||||
self.parts = [tuple(self.steps[i : i + 2]) for i in range(len(self.steps))[::2]]
|
||||
|
||||
@staticmethod
|
||||
def get_height(value, unit):
|
||||
"""
|
||||
Compute height of a value relative to unit
|
||||
|
||||
Args:
|
||||
|
||||
value (number): value
|
||||
|
||||
unit (number): unit
|
||||
"""
|
||||
if value < unit / 10.0:
|
||||
return 0
|
||||
elif value <= unit:
|
||||
return 1
|
||||
elif value <= unit * 2:
|
||||
return 2
|
||||
elif value <= unit * 3:
|
||||
return 3
|
||||
else:
|
||||
return 4
|
||||
|
||||
def get_steps(self):
|
||||
"""
|
||||
Convert the list of values to a list of steps
|
||||
|
||||
Return: list
|
||||
"""
|
||||
maxval = max(self.values)
|
||||
unit = maxval / 4.0
|
||||
if unit == 0:
|
||||
return [0] * len(self.values)
|
||||
stepslist = []
|
||||
for value in self.values:
|
||||
stepslist.append(self.get_height(value, unit))
|
||||
return stepslist
|
||||
|
||||
def get_chars(self):
|
||||
"""
|
||||
Decide which chars to draw
|
||||
|
||||
Return: str
|
||||
"""
|
||||
chars = []
|
||||
for part in self.parts:
|
||||
chars.append(BrailleGraph.chars[part])
|
||||
return "".join(chars)
|
||||
|
||||
|
||||
def braille(values):
|
||||
"""wrapper function"""
|
||||
return BrailleGraph(values).get_chars()
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
103
bumblebee_status/util/location.py
Normal file
103
bumblebee_status/util/location.py
Normal file
|
@ -0,0 +1,103 @@
|
|||
"""Retrieves location information from an external
|
||||
service and caches it for 12h (retries are done every
|
||||
30m in case of problems)
|
||||
|
||||
Right now, it uses (in order of preference):
|
||||
- http://free.ipwhois.io/
|
||||
- http://ipapi.co/
|
||||
"""
|
||||
|
||||
|
||||
import json
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
__document = None
|
||||
__data = {}
|
||||
__next = 0
|
||||
__sources = [
|
||||
{
|
||||
"url": "http://free.ipwhois.io/json/",
|
||||
"mapping": {
|
||||
"latitude": "latitude",
|
||||
"longitude": "longitude",
|
||||
"country": "country",
|
||||
"ip": "public_ip",
|
||||
},
|
||||
},
|
||||
{
|
||||
"url": "http://ipapi.co/json",
|
||||
"mapping": {
|
||||
"latitude": "latitude",
|
||||
"longitude": "longitude",
|
||||
"country_name": "country",
|
||||
"ip": "public_ip",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def __expired():
|
||||
global __next
|
||||
return __next <= time.time()
|
||||
|
||||
|
||||
def __load():
|
||||
global __data
|
||||
global __next
|
||||
|
||||
__data = {}
|
||||
for src in __sources:
|
||||
try:
|
||||
tmp = json.loads(urllib.request.urlopen(src["url"]).read())
|
||||
for k, v in src["mapping"].items():
|
||||
__data[v] = tmp.get(k, None)
|
||||
__next = time.time() + 60 * 60 * 12 # update once every 12h
|
||||
return
|
||||
except Exception as e:
|
||||
pass
|
||||
__next = time.time() + 60 * 30 # error - try again every 30m
|
||||
|
||||
|
||||
def __get(name, default=None):
|
||||
global __data
|
||||
if not __data or __expired():
|
||||
__load()
|
||||
return __data.get(name, default)
|
||||
|
||||
|
||||
def reset():
|
||||
"""Resets the location library, ensuring that a new query will be started
|
||||
"""
|
||||
global __next
|
||||
__next = 0
|
||||
|
||||
|
||||
def coordinates():
|
||||
"""Returns a latitude, longitude pair
|
||||
|
||||
:return: current latitude and longitude
|
||||
:rtype: pair of strings
|
||||
"""
|
||||
return __get("latitude"), __get("longitude")
|
||||
|
||||
|
||||
def country():
|
||||
"""Returns the current country name
|
||||
|
||||
:return: country name
|
||||
:rtype: string
|
||||
"""
|
||||
return __get("country")
|
||||
|
||||
|
||||
def public_ip():
|
||||
"""Returns the current public IP
|
||||
|
||||
:return: public IP
|
||||
:rtype: string
|
||||
"""
|
||||
return __get("public_ip")
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
58
bumblebee_status/util/popup.py
Normal file
58
bumblebee_status/util/popup.py
Normal file
|
@ -0,0 +1,58 @@
|
|||
"""Pop-up menus."""
|
||||
|
||||
import logging
|
||||
|
||||
try:
|
||||
import Tkinter as tk
|
||||
except ImportError:
|
||||
# python 3
|
||||
import tkinter as tk
|
||||
|
||||
import functools
|
||||
|
||||
|
||||
class menu(object):
|
||||
def __init__(self, parent=None, leave=True):
|
||||
if not parent:
|
||||
self._root = tk.Tk()
|
||||
self._root.withdraw()
|
||||
self._menu = tk.Menu(self._root, tearoff=0)
|
||||
self._menu.bind("<FocusOut>", self._on_focus_out)
|
||||
else:
|
||||
self._root = parent.root()
|
||||
self._root.withdraw()
|
||||
self._menu = tk.Menu(self._root, tearoff=0)
|
||||
self._menu.bind("<FocusOut>", self._on_focus_out)
|
||||
if leave:
|
||||
self._menu.bind("<Leave>", self._on_focus_out)
|
||||
|
||||
def root(self):
|
||||
return self._root
|
||||
|
||||
def menu(self):
|
||||
return self._menu
|
||||
|
||||
def _on_focus_out(self, event=None):
|
||||
self._root.destroy()
|
||||
|
||||
def _on_click(self, callback):
|
||||
self._root.destroy()
|
||||
callback()
|
||||
|
||||
def add_cascade(self, menuitem, submenu):
|
||||
self._menu.add_cascade(label=menuitem, menu=submenu.menu())
|
||||
|
||||
def add_menuitem(self, menuitem, callback):
|
||||
self._menu.add_command(
|
||||
label=menuitem, command=functools.partial(self._on_click, callback)
|
||||
)
|
||||
|
||||
def show(self, event, offset_x=0, offset_y=0):
|
||||
try:
|
||||
self._menu.tk_popup(event["x"] + offset_x, event["y"] + offset_y)
|
||||
finally:
|
||||
self._menu.grab_release()
|
||||
self._root.mainloop()
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
44
bumblebee_status/util/store.py
Normal file
44
bumblebee_status/util/store.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
"""Store interface
|
||||
|
||||
Allows arbitrary classes to offer a simple get/set
|
||||
store interface by deriving from the Store class in
|
||||
this module
|
||||
"""
|
||||
|
||||
|
||||
class Store(object):
|
||||
"""Interface for storing and retrieving simple values"""
|
||||
|
||||
def __init__(self):
|
||||
super(Store, self).__init__()
|
||||
self._data = {}
|
||||
|
||||
def set(self, key, value):
|
||||
"""Sets key to value, overwriting any existing data for that key
|
||||
|
||||
:param key: the name of the parameter to set
|
||||
:param value: the value to be set
|
||||
"""
|
||||
self._data[key] = {"value": value, "used": False}
|
||||
|
||||
def unused_keys(self):
|
||||
"""Returns a list of unused keys
|
||||
|
||||
:return: a list of keys that are set, but never used
|
||||
:rtype: list of strings
|
||||
"""
|
||||
return [key for key, value in self._data.items() if value["used"] == False]
|
||||
|
||||
def get(self, key, default=None):
|
||||
"""Returns the current value for the specified key, or a default value,
|
||||
if the key is not set
|
||||
|
||||
:param key: the name of the parameter to retrieve
|
||||
:param default: the default value to return, defaults to None
|
||||
"""
|
||||
if key in self._data:
|
||||
self._data[key]["used"] = True
|
||||
return self._data.get(key, {"value": default})["value"]
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
Loading…
Add table
Add a link
Reference in a new issue