diff --git a/util/algorithm.py b/util/algorithm.py index faa40d9..92b0380 100644 --- a/util/algorithm.py +++ b/util/algorithm.py @@ -1,9 +1,15 @@ import copy -# algorithm copied from -# http://blog.impressiver.com/post/31434674390/deep-merge-multiple-python-dicts -# nicely done :) + 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) diff --git a/util/cli.py b/util/cli.py index 5ea29f4..fd54093 100644 --- a/util/cli.py +++ b/util/cli.py @@ -5,6 +5,19 @@ 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: diff --git a/util/format.py b/util/format.py index d5dc15a..d0eea10 100644 --- a/util/format.py +++ b/util/format.py @@ -2,6 +2,14 @@ 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): @@ -11,6 +19,17 @@ def asbool(val): 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) @@ -20,6 +39,13 @@ def asint(val, minimum=None, maximum=None): 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): @@ -30,11 +56,30 @@ def aslist(val): __UNITS = {"metric": "C", "kelvin": "K", "imperial": "F", "default": "C"} -def astemperature(value, unit="metric"): +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(value), __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. 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) @@ -46,6 +91,13 @@ __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) @@ -62,6 +114,15 @@ def seconds(duration): 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" diff --git a/util/location.py b/util/location.py index caaee40..f8cb441 100644 --- a/util/location.py +++ b/util/location.py @@ -1,3 +1,13 @@ +"""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 @@ -5,7 +15,6 @@ import urllib.request __document = None __data = {} __next = 0 - __sources = [ { "url": "http://free.ipwhois.io/json/", @@ -58,19 +67,36 @@ def __get(name, default=None): 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")