[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:
tobi-wan-kenobi 2020-05-09 21:22:00 +02:00
parent 1d25be2059
commit 320827d577
146 changed files with 2509 additions and 2 deletions

View file

View 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

View 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

View 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

View 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

View 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

View 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

View 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