diff --git a/README.rst b/README.rst index 3d7e823..21f0784 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,3 @@ -.. Always edit README.tpl.rst. Do not change the module reference manually. - i3pystatus ========== @@ -9,7 +7,7 @@ i3pystatus .. image:: https://travis-ci.org/enkore/i3pystatus.svg?branch=master :target: https://travis-ci.org/enkore/i3pystatus -i3pystatus is a (hopefully growing) collection of python scripts for +i3pystatus is a growing collection of python scripts for status output compatible to i3status / i3bar of the i3 window manager. Installation @@ -110,16 +108,13 @@ Contributors Contribute ---------- -To contribute a module, make sure it uses one of the Module classes. Most modules -use IntervalModule, which just calls a function repeatedly in a specified interval. +To contribute a module, make sure it uses one of the ``Module`` classes. Most modules +use ``IntervalModule``, which just calls a function repeatedly in a specified interval. -The output attribute should be set to a dictionary which represents your modules output, +The ``output`` attribute should be set to a dictionary which represents your modules output, the protocol is documented `here `_. -To update this readme run ``python -m i3pystatus.mkdocs`` in the -repository root and you're done :) - Developer documentation is available in the source code and `here -`_. +`_. **Patches and pull requests are very welcome :-)** diff --git a/ci-build.sh b/ci-build.sh index 8d78c65..fa142a3 100755 --- a/ci-build.sh +++ b/ci-build.sh @@ -17,6 +17,7 @@ mkdir ${BUILD}/test-install ${BUILD}/test-install-bin PYTHONPATH=${BUILD}/test-install python3 setup.py --quiet install --install-lib ${BUILD}/test-install --install-scripts ${BUILD}/test-install-bin test -f ${BUILD}/test-install-bin/i3pystatus +test -f ${BUILD}/test-install-bin/i3pystatus-setting-util PYTHONPATH=${BUILD}/test-install py.test --junitxml ${BUILD}/testlog.xml tests diff --git a/docs/changelog.rst b/docs/changelog.rst index 5641eaf..e9bc004 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,9 +6,11 @@ master branch +++++++++++++ * Errors can now be logged to ``~/.i3pystatus-`` - - ``log_level`` setting + - See :ref:`logging` * Added new callback system + - See :ref:`callbacks` * Added credentials storage + - See :ref:`credentials` * Added deadbeef module * Added github module * Added whosonlocation module diff --git a/docs/conf.py b/docs/conf.py index 63c1547..38a49b2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # # i3pystatus documentation build configuration file, created by # sphinx-quickstart on Mon Oct 14 17:41:37 2013. @@ -32,7 +31,8 @@ MOCK_MODULES = [ "requests", "bs4", "dota2py", - "novaclient.v2" + "novaclient.v2", + "speedtest_cli" ] for mod_name in MOCK_MODULES: diff --git a/docs/configuration.rst b/docs/configuration.rst index 109df38..fb47b96 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -120,12 +120,20 @@ Also change your i3wm config to the following: workspace_buttons yes } -.. note:: - Don't name your config file ``i3pystatus.py`` +.. note:: Don't name your config file ``i3pystatus.py``, as it would + make ``i3pystatus`` un-importable and lead to errors. -Settings that require credentials can utilize the keyring module to keep sensitive information out of config files. -To take advantage of this feature, simply use the setting_util.py script to set the credentials for a module. Once this -is done you can add the module to your config without specifying the credentials, eg: +.. _credentials: + +Credentials +----------- + +Settings that require credentials can utilize the keyring module to +keep sensitive information out of config files. To take advantage of +this feature, simply use the ``i3pystatus-setting-util`` script +installed along i3pystatus to set the credentials for a module. Once +this is done you can add the module to your config without specifying +the credentials, e.g.: .. code:: python @@ -141,4 +149,152 @@ If you don't want to use the default you can set a specific keyring like so: from keyring.backends.file import PlaintextKeyring status.register('github', keyring_backend=PlaintextKeyring()) -i3pystatus will locate and set the credentials during the module loading process. Currently supported credentals are "password", "email" and "username". +i3pystatus will locate and set the credentials during the module +loading process. Currently supported credentials are "password", +"email" and "username". + +.. note:: Credential handling requires the PyPI package + ``keyring``. Many distributions have it pre-packaged available as + ``python-keyring``. + +Formatting +---------- + +All modules let you specifiy the exact output formatting using a +`format string `_, which +gives you a great deal of flexibility. + +If a module gives you a float, it probably has a ton of +uninteresting decimal places. Use ``{somefloat:.0f}`` to get the integer +value, ``{somefloat:0.2f}`` gives you two decimal places after the +decimal dot + +.. _formatp: + +formatp +~~~~~~~ + +Some modules use an extended format string syntax (the :py:mod:`.mpd` +module, for example). Given the format string below the output adapts +itself to the available data. + +:: + + [{artist}/{album}/]{title}{status} + +Only if both the artist and album is known they're displayed. If only one or none +of them is known the entire group between the brackets is excluded. + +"is known" is here defined as "value evaluating to True in Python", i.e. an empty +string or 0 (or 0.0) counts as "not known". + +Inside a group always all format specifiers must evaluate to true (logical and). + +You can nest groups. The inner group will only become part of the output if both +the outer group and the inner group are eligible for output. + +.. _TimeWrapper: + +TimeWrapper +~~~~~~~~~~~ + +Some modules that output times use :py:class:`.TimeWrapper` to format +these. TimeWrapper is a mere extension of the standard formatting +method. + +The time format that should be used is specified using the format specifier, i.e. +with some_time being 3951 seconds a format string like ``{some_time:%h:%m:%s}`` +would produce ``1:5:51``. + +* ``%h``, ``%m`` and ``%s`` are the hours, minutes and seconds without + leading zeros (i.e. 0 to 59 for minutes and seconds) +* ``%H``, ``%M`` and ``%S`` are padded with a leading zero to two digits, + i.e. 00 to 59 +* ``%l`` and ``%L`` produce hours non-padded and padded but only if hours + is not zero. If the hours are zero it produces an empty string. +* ``%%`` produces a literal % +* ``%E`` (only valid on beginning of the string) if the time is null, + don't format anything but rather produce an empty string. If the + time is non-null it is removed from the string. +* When the module in question also uses formatp, 0 seconds counts as + "not known". +* The formatted time is stripped, i.e. spaces on both ends of the + result are removed. + +.. _logging: + +Logging +------- + +Errors do happen and to ease debugging i3pystatus includes a logging +facility. By default i3pystatus will log exceptions raised by modules +to files in your home directory named +``.i3pystatus-``. Some modules might log additional +information. + +.. rubric:: Log level + +Every module has a ``log_level`` option which sets the *minimum* +severity required for an event to be logged. + +The numeric values of logging levels are given in the following +table. + ++--------------+---------------+ +| Level | Numeric value | ++==============+===============+ +| ``CRITICAL`` | 50 | ++--------------+---------------+ +| ``ERROR`` | 40 | ++--------------+---------------+ +| ``WARNING`` | 30 | ++--------------+---------------+ +| ``INFO`` | 20 | ++--------------+---------------+ +| ``DEBUG`` | 10 | ++--------------+---------------+ +| ``NOTSET`` | 0 | ++--------------+---------------+ + +Exceptions raised by modules are of severity ``ERROR`` by default. The +default ``log_level`` in i3pystatus (some modules might redefine the +default, see the reference of the module in question) is 30 +(``WARNING``). + +.. _callbacks: + +Callbacks +--------- + +Callbacks are used for click-events (merged into i3bar since i3 4.6, +mouse wheel events are merged since 4.8), that is, you click (or +scroll) on the output of a module in your i3bar and something +happens. What happens is defined by these settings for each module +individually: + +- ``on_leftclick`` +- ``on_rightclick`` +- ``on_upscroll`` +- ``on_downscroll`` + +The global default action for all settings is ``None`` (do nothing), +but many modules define other defaults, which are documented in the +module reference. + +The settings can be of different types, namely + +- a list referring to a method of the module, e.g. + + .. code:: python + + status.register("clock", + on_leftclick=["scroll_format", 1]) + + ``scroll_format`` is a method of the ``clock`` module, the ``1`` is + passed as a parameter and indicates the direction in this case. +- as a special case of the above: a string referring to a method, no + parameters are passed. +- a list where the first element is a callable and the following + elements are passed as arguments to the callable +- again a special case of the above: just a callable, no parameters +- a string which is run in a shell diff --git a/docs/formatting.rst b/docs/formatting.rst deleted file mode 100644 index 8de1777..0000000 --- a/docs/formatting.rst +++ /dev/null @@ -1,63 +0,0 @@ -Formatting -========== - -All modules let you specifiy the exact output formatting using a -`format string `_, which -gives you a great deal of flexibility. - -If a module gives you a float, it probably has a ton of -uninteresting decimal places. Use ``{somefloat:.0f}`` to get the integer -value, ``{somefloat:0.2f}`` gives you two decimal places after the -decimal dot - -.. _formatp: - -formatp -------- - -Some modules use an extended format string syntax (the :py:mod:`.mpd` -module, for example). Given the format string below the output adapts -itself to the available data. - -:: - - [{artist}/{album}/]{title}{status} - -Only if both the artist and album is known they're displayed. If only one or none -of them is known the entire group between the brackets is excluded. - -"is known" is here defined as "value evaluating to True in Python", i.e. an empty -string or 0 (or 0.0) counts as "not known". - -Inside a group always all format specifiers must evaluate to true (logical and). - -You can nest groups. The inner group will only become part of the output if both -the outer group and the inner group are eligible for output. - -.. _TimeWrapper: - -TimeWrapper ------------ - -Some modules that output times use :py:class:`.TimeWrapper` to format -these. TimeWrapper is a mere extension of the standard formatting -method. - -The time format that should be used is specified using the format specifier, i.e. -with some_time being 3951 seconds a format string like ``{some_time:%h:%m:%s}`` -would produce ``1:5:51``. - -* ``%h``, ``%m`` and ``%s`` are the hours, minutes and seconds without - leading zeros (i.e. 0 to 59 for minutes and seconds) -* ``%H``, ``%M`` and ``%S`` are padded with a leading zero to two digits, - i.e. 00 to 59 -* ``%l`` and ``%L`` produce hours non-padded and padded but only if hours - is not zero. If the hours are zero it produces an empty string. -* ``%%`` produces a literal % -* ``%E`` (only valid on beginning of the string) if the time is null, - don't format anything but rather produce an empty string. If the - time is non-null it is removed from the string. -* When the module in question also uses formatp, 0 seconds counts as - "not known". -* The formatted time is stripped, i.e. spaces on both ends of the - result are removed. diff --git a/docs/i3pystatus.rst b/docs/i3pystatus.rst index dacbbb7..21dd060 100644 --- a/docs/i3pystatus.rst +++ b/docs/i3pystatus.rst @@ -4,18 +4,20 @@ Module reference .. Don't list *every* module here, e.g. cpu-usage suffices, because the other variants are listed below that one. +.. rubric:: Module overview: + :System: `clock`_ - `disk`_ - `load`_ - `mem`_ - `cpu_usage`_ :Audio: `alsa`_ - `pulseaudio`_ :Hardware: `battery`_ - `backlight`_ - `temp`_ :Network: `network`_ :Music: `now_playing`_ - `mpd`_ -:Websites & stuff: `weather`_ - `bitcoin`_ - `reddit`_ - `parcel`_ +:Websites: `weather`_ - `bitcoin`_ - `reddit`_ - `parcel`_ :Other: `mail`_ - `pyload`_ - `text`_ - `updates`_ :Advanced: `file`_ - `regex`_ - `runwatch`_ - `shell`_ .. autogen:: i3pystatus Module - .. note:: List of all modules: + .. rubric:: Module list: .. _mailbackends: diff --git a/docs/index.rst b/docs/index.rst index 42867b4..421b47e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,7 +8,6 @@ Contents: :maxdepth: 4 configuration - formatting i3pystatus changelog module diff --git a/docs/module_docs.py b/docs/module_docs.py index 5aff0eb..e415fa9 100644 --- a/docs/module_docs.py +++ b/docs/module_docs.py @@ -12,7 +12,7 @@ import i3pystatus.core.modules from i3pystatus.core.imputil import ClassFinder from i3pystatus.core.color import ColorRangeModule -IGNORE_MODULES = ("__main__", "core") +IGNORE_MODULES = ("__main__", "core", "tools") def is_module(obj): @@ -132,7 +132,7 @@ def generate_automodules(path, name, basecls): contents = [] for mod in modules: - contents.append(" * :py:mod:`~{}`".format(mod[0])) + contents.append("* :py:mod:`~{}`".format(mod[0])) contents.append("") for mod in modules: diff --git a/i3pystatus/__init__.py b/i3pystatus/__init__.py index 6d578cb..1d022a8 100644 --- a/i3pystatus/__init__.py +++ b/i3pystatus/__init__.py @@ -8,13 +8,6 @@ from i3pystatus.core.util import formatp import logging import os -h = logging.FileHandler(".i3pystatus-" + str(os.getpid()), delay=True) - -logger = logging.getLogger("i3pystatus") -logger.addHandler(h) -logger.setLevel(logging.CRITICAL) - - __path__ = extend_path(__path__, __name__) __all__ = [ @@ -24,6 +17,12 @@ __all__ = [ "formatp", ] +logpath = os.path.join(os.path.expanduser("~"), ".i3pystatus-%s" % os.getpid()) +handler = logging.FileHandler(logpath, delay=True) +logger = logging.getLogger("i3pystatus") +logger.addHandler(handler) +logger.setLevel(logging.CRITICAL) + def main(): from i3pystatus.clock import Clock diff --git a/i3pystatus/battery.py b/i3pystatus/battery.py index c42636d..4c0f76f 100644 --- a/i3pystatus/battery.py +++ b/i3pystatus/battery.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - import os import re import configparser diff --git a/i3pystatus/bitcoin.py b/i3pystatus/bitcoin.py index e27568f..8c89c9f 100644 --- a/i3pystatus/bitcoin.py +++ b/i3pystatus/bitcoin.py @@ -40,8 +40,6 @@ class Bitcoin(IntervalModule): ("colorize", "Enable color change on price increase/decrease"), ("color_up", "Color for price increases"), ("color_down", "Color for price decreases"), - ("leftclick", "URL to visit or command to run on left click"), - ("rightclick", "URL to visit or command to run on right click"), ("interval", "Update interval."), ("symbol", "Symbol for bitcoin sign"), "status" @@ -54,16 +52,14 @@ class Bitcoin(IntervalModule): colorize = False color_up = "#00FF00" color_down = "#FF0000" - leftclick = "electrum" - rightclick = "https://bitcoinaverage.com/" interval = 600 status = { "price_up": "▲", "price_down": "▼", } - on_leftclick = "handle_leftclick" - on_rightclick = "handle_rightclick" + on_leftclick = "electrum" + on_rightclick = [user_open, "https://bitcoinaverage.com/"] _price_prev = 0 @@ -128,9 +124,3 @@ class Bitcoin(IntervalModule): "full_text": self.format.format(**fdict), "color": color, } - - def handle_leftclick(self): - user_open(self.leftclick) - - def handle_rightclick(self): - user_open(self.rightclick) diff --git a/i3pystatus/clock.py b/i3pystatus/clock.py index bb0780f..dc6532d 100644 --- a/i3pystatus/clock.py +++ b/i3pystatus/clock.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - import os import locale import time diff --git a/i3pystatus/core/desktop.py b/i3pystatus/core/desktop.py index 5deacef..6143283 100644 --- a/i3pystatus/core/desktop.py +++ b/i3pystatus/core/desktop.py @@ -30,6 +30,8 @@ class DesktopNotification(BaseDesktopNotification): try: + import gi + gi.require_version('Notify', '0.7') from gi.repository import Notify except ImportError: pass diff --git a/i3pystatus/core/modules.py b/i3pystatus/core/modules.py index 8f7e408..859c7b9 100644 --- a/i3pystatus/core/modules.py +++ b/i3pystatus/core/modules.py @@ -9,10 +9,10 @@ class Module(SettingsBase): position = 0 settings = ( - ('on_leftclick', "Callback called on left click (string)"), - ('on_rightclick', "Callback called on right click (string)"), - ('on_upscroll', "Callback called on scrolling up (string)"), - ('on_downscroll', "Callback called on scrolling down (string)"), + ('on_leftclick', "Callback called on left click (see :ref:`callbacks`)"), + ('on_rightclick', "Callback called on right click (see :ref:`callbacks`)"), + ('on_upscroll', "Callback called on scrolling up (see :ref:`callbacks`)"), + ('on_downscroll', "Callback called on scrolling down (see :ref:`callbacks`)"), ('hints', "Additional output blocks for module output (dict)"), ) diff --git a/i3pystatus/core/settings.py b/i3pystatus/core/settings.py index ecabdd0..2ed7f2f 100644 --- a/i3pystatus/core/settings.py +++ b/i3pystatus/core/settings.py @@ -23,12 +23,18 @@ class SettingsBaseMeta(type): name(setting) in seen or seen.add(name(setting)))] settings = tuple() - required = tuple() + required = set() # getmro returns base classes according to Method Resolution Order, # which always includes the class itself as the first element. for base in inspect.getmro(cls): settings += tuple(getattr(base, "settings", [])) - required += tuple(getattr(base, "required", [])) + required |= set(getattr(base, "required", [])) + # if a derived class defines a default for a setting it is not + # required anymore. + for base in inspect.getmro(cls): + for r in list(required): + if hasattr(base, r): + required.remove(r) return unique(settings), required @@ -48,7 +54,7 @@ class SettingsBase(metaclass=SettingsBaseMeta): __PROTECTED_SETTINGS = ["password", "email", "username"] settings = ( - ("log_level", "Set to true to log error to .i3pystatus- file"), + ("log_level", "Set to true to log error to .i3pystatus- file."), ) """settings should be tuple containing two types of elements: @@ -61,7 +67,7 @@ class SettingsBase(metaclass=SettingsBaseMeta): required = tuple() """required can list settings which are required""" - log_level = logging.NOTSET + log_level = logging.WARNING logger = None def __init__(self, *args, **kwargs): diff --git a/i3pystatus/core/threading.py b/i3pystatus/core/threading.py index 2465551..f6eee9b 100644 --- a/i3pystatus/core/threading.py +++ b/i3pystatus/core/threading.py @@ -67,23 +67,26 @@ class ExceptionWrapper(Wrapper): try: self.workload() except: - sys.stderr.write("Exception in {thread} at {time}\n".format( + message = "\n> Exception in {thread} at {time}, module {name}".format( thread=threading.current_thread().name, - time=time.strftime("%c") - )) - traceback.print_exc(file=sys.stderr) - sys.stderr.flush() - if hasattr(self.workload, "output"): - self.workload.output = { - "full_text": "{}: {}".format(self.workload.__class__.__name__, - self.format_error(str(sys.exc_info()[1]))), - "color": "#FF0000", - } + time=time.strftime("%c"), + name=self.workload.__class__.__name__ + ) + if hasattr(self.workload, "logger"): + self.workload.logger.error(message, exc_info=True) + self.workload.output = { + "full_text": "{}: {}".format(self.workload.__class__.__name__, + self.format_error(str(sys.exc_info()[1]))), + "color": "#FF0000", + } def format_error(self, exception_message): if hasattr(self.workload, 'max_error_len'): error_len = self.workload.max_error_len - return exception_message[:error_len] + '...' if len(exception_message) > error_len else exception_message + if len(exception_message) > error_len: + return exception_message[:error_len] + '…' + else: + return exception_message else: return exception_message diff --git a/i3pystatus/cpu_freq.py b/i3pystatus/cpu_freq.py index e78d9d5..7e283e6 100644 --- a/i3pystatus/cpu_freq.py +++ b/i3pystatus/cpu_freq.py @@ -1,4 +1,3 @@ -# coding=utf-8 from i3pystatus import IntervalModule diff --git a/i3pystatus/cpu_usage.py b/i3pystatus/cpu_usage.py index 55ffc1c..5df3dc7 100644 --- a/i3pystatus/cpu_usage.py +++ b/i3pystatus/cpu_usage.py @@ -1,5 +1,3 @@ -# -*- coding:utf-8 -*- - from collections import defaultdict from string import Formatter diff --git a/i3pystatus/cpu_usage_bar.py b/i3pystatus/cpu_usage_bar.py index f65aa80..2bf06e5 100644 --- a/i3pystatus/cpu_usage_bar.py +++ b/i3pystatus/cpu_usage_bar.py @@ -1,4 +1,3 @@ -# -*- coding:utf-8 -*- from i3pystatus.core.color import ColorRangeModule from i3pystatus.cpu_usage import CpuUsage from i3pystatus.core.util import make_bar, make_vertical_bar diff --git a/i3pystatus/dota2wins.py b/i3pystatus/dota2wins.py index 9237c24..fd7fc9e 100644 --- a/i3pystatus/dota2wins.py +++ b/i3pystatus/dota2wins.py @@ -96,7 +96,7 @@ class Dota2wins(IntervalModule): "screenname": screenname, "wins": wins, "losses": losses, - "win_percent": win_percent, + "win_percent": "%.2f" % win_percent, } self.output = { diff --git a/i3pystatus/mail/imap.py b/i3pystatus/mail/imap.py index 6f91937..1ac0c5c 100644 --- a/i3pystatus/mail/imap.py +++ b/i3pystatus/mail/imap.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - import sys import imaplib diff --git a/i3pystatus/mail/maildir.py b/i3pystatus/mail/maildir.py index 7673972..9c1e12a 100644 --- a/i3pystatus/mail/maildir.py +++ b/i3pystatus/mail/maildir.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - import os from i3pystatus.mail import Backend diff --git a/i3pystatus/mail/mbox.py b/i3pystatus/mail/mbox.py index 951d3f6..e782ffe 100644 --- a/i3pystatus/mail/mbox.py +++ b/i3pystatus/mail/mbox.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - import sys from i3pystatus.mail import Backend import subprocess diff --git a/i3pystatus/mail/notmuchmail.py b/i3pystatus/mail/notmuchmail.py index 8953761..dc8988e 100644 --- a/i3pystatus/mail/notmuchmail.py +++ b/i3pystatus/mail/notmuchmail.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - # note that this needs the notmuch python bindings. For more info see: # http://notmuchmail.org/howto/#index4h2 import notmuch diff --git a/i3pystatus/mail/thunderbird.py b/i3pystatus/mail/thunderbird.py index 3760706..61c7710 100644 --- a/i3pystatus/mail/thunderbird.py +++ b/i3pystatus/mail/thunderbird.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- -# # This plugin listens for dbus signals emitted by the # thunderbird-dbus-sender extension for TB: # https://github.com/janoliver/thunderbird-dbus-sender diff --git a/i3pystatus/net_speed.py b/i3pystatus/net_speed.py new file mode 100644 index 0000000..fce158d --- /dev/null +++ b/i3pystatus/net_speed.py @@ -0,0 +1,111 @@ +from i3pystatus import IntervalModule +import speedtest_cli +import requests +import time +import os +from urllib.parse import urlparse +import contextlib +import sys +from io import StringIO + + +class NetSpeed(IntervalModule): + """ + Attempts to provide an estimation of internet speeds. + Requires: speedtest_cli + """ + + settings = ( + ("url", "Target URL to download a file from. Uses speedtest_cli to " + "find the 'best' server if none is supplied."), + ("units", "Valid values are B, b, bytes, or bits"), + "format" + ) + color = "#FFFFFF" + interval = 300 + url = None + units = 'bits' + format = "{speed} ({hosting_provider})" + + def run(self): + + # since speedtest_cli likes to print crap, we need to squelch it + @contextlib.contextmanager + def nostdout(): + save_stdout = sys.stdout + sys.stdout = StringIO() + yield + sys.stdout = save_stdout + + if not self.url: + with nostdout(): + try: + config = speedtest_cli.getConfig() + servers = speedtest_cli.closestServers(config['client']) + best = speedtest_cli.getBestServer(servers) + # 1500x1500 is about 4.3MB, which seems like a reasonable place to + # start, i guess... + url = '%s/random1500x1500.jpg' % os.path.dirname(best['url']) + except KeyError: + url = None + + if not url: + cdict = { + "speed": 0, + "hosting_provider": 'null', + } + else: + with open('/dev/null', 'wb') as devnull: + start = time.time() + req = requests.get(url, stream=True) + devnull.write(req.content) + end = time.time() + total_length = int(req.headers.get('content-length')) + devnull.close() + + # chop off the float after the 4th decimal point + # note: not rounding, simply cutting + # note: dl_time is in seconds + dl_time = float(end - start) + + if self.units == 'bits' or self.units == 'b': + unit = 'bps' + kilo = 1000 + mega = 1000000 + giga = 1000000000 + factor = 8 + elif self.units == 'bytes' or self.units == 'B': + unit = 'Bps' + kilo = 8000 + mega = 8000000 + giga = 8000000000 + factor = 1 + + if total_length < kilo: + bps = float(total_length / dl_time) + + if total_length >= kilo and total_length < mega: + unit = "K" + unit + bps = float((total_length / 1024.0) / dl_time) + + if total_length >= mega and total_length < giga: + unit = "M" + unit + bps = float((total_length / (1024.0 * 1024.0)) / dl_time) + + if total_length >= giga: + unit = "G" + unit + bps = float((total_length / (1024.0 * 1024.0 * 1024.0)) / dl_time) + + bps = "%.2f" % (bps * factor) + speed = "%s %s" % (bps, unit) + hosting_provider = '.'.join(urlparse(url).hostname.split('.')[-2:]) + + cdict = { + "speed": speed, + "hosting_provider": hosting_provider, + } + + self.output = { + "full_text": self.format.format(**cdict), + "color": self.color + } diff --git a/i3pystatus/network.py b/i3pystatus/network.py index 87179a8..aa39099 100644 --- a/i3pystatus/network.py +++ b/i3pystatus/network.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import netifaces from i3pystatus import IntervalModule @@ -198,6 +197,20 @@ class NetworkTraffic(): def get_packets_received(self): return self.pnic.packets_recv - self.pnic_before.packets_recv + def get_rx_tot_Mbytes(self, interface): + try: + with open("/sys/class/net/{}/statistics/rx_bytes".format(interface)) as f: + return int(f.readline().split('\n')[0]) / (1024 * 1024) + except FileNotFoundError: + return False + + def get_tx_tot_Mbytes(self, interface): + try: + with open("/sys/class/net/{}/statistics/tx_bytes".format(interface)) as f: + return int(f.readline().split('\n')[0]) / (1024 * 1024) + except FileNotFoundError: + return False + def get_usage(self, interface): self.update_counters(interface) usage = dict(bytes_sent=0, bytes_recv=0, packets_sent=0, packets_recv=0) @@ -209,6 +222,8 @@ class NetworkTraffic(): usage["bytes_recv"] = self.get_bytes_received() usage["packets_sent"] = self.get_packets_sent() usage["packets_recv"] = self.get_packets_received() + usage["rx_tot_Mbytes"] = self.get_rx_tot_Mbytes(interface) + usage["tx_tot_Mbytes"] = self.get_tx_tot_Mbytes(interface) round_dict(usage, self.round_size) return usage @@ -249,6 +264,8 @@ class Network(IntervalModule, ColorRangeModule): * `{bytes_recv}` — bytes received per second (divided by divisor) * `{packets_sent}` — bytes sent per second (divided by divisor) * `{packets_recv}` — bytes received per second (divided by divisor) + * `{rx_tot_Mbytes}` — total Mbytes received + * `{tx_tot_Mbytes}` — total Mbytes sent """ settings = ( ("format_up", "format string"), @@ -313,7 +330,8 @@ class Network(IntervalModule, ColorRangeModule): # Don't require importing psutil unless using the functionality it offers. if any(s in self.format_up or s in self.format_down for s in - ['bytes_sent', 'bytes_recv', 'packets_sent', 'packets_recv', 'network_graph', 'kbs']): + ['bytes_sent', 'bytes_recv', 'packets_sent', 'packets_recv', 'network_graph', + 'rx_tot_Mbytes', 'tx_tot_Mbytes', 'kbs']): self.network_traffic = NetworkTraffic(self.unknown_up, self.divisor, self.round_size) else: self.network_traffic = None @@ -324,6 +342,7 @@ class Network(IntervalModule, ColorRangeModule): self.kbs_arr = [0.0] * self.graph_width def cycle_interface(self, increment=1): + """Cycle through available interfaces in `increment` steps. Sign indicates direction.""" interfaces = [i for i in netifaces.interfaces() if i not in self.ignore_interfaces] if self.interface in interfaces: next_index = (interfaces.index(self.interface) + increment) % len(interfaces) @@ -343,6 +362,7 @@ class Network(IntervalModule, ColorRangeModule): def run(self): format_values = dict(kbs="", network_graph="", bytes_sent="", bytes_recv="", packets_sent="", packets_recv="", + rx_tot_Mbytes="", tx_tot_Mbytes="", interface="", v4="", v4mask="", v4cidr="", v6="", v6mask="", v6cidr="", mac="", essid="", freq="", quality="", quality_bar="") if self.network_traffic: diff --git a/i3pystatus/pomodoro.py b/i3pystatus/pomodoro.py index 90c5825..7d6bad1 100644 --- a/i3pystatus/pomodoro.py +++ b/i3pystatus/pomodoro.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - import os import subprocess import locale diff --git a/i3pystatus/tools/__init__.py b/i3pystatus/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/i3pystatus/tools/setting_util.py b/i3pystatus/tools/setting_util.py new file mode 100755 index 0000000..60bed5d --- /dev/null +++ b/i3pystatus/tools/setting_util.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python +import glob +import inspect +import os +import getpass +import sys +import signal +import pkgutil +from collections import defaultdict, OrderedDict + +import keyring + +import i3pystatus +from i3pystatus import Module, SettingsBase +from i3pystatus.core import ClassFinder + + +def signal_handler(signal, frame): + sys.exit(0) + + +def get_int_in_range(prompt, _range): + while True: + try: + answer = input(prompt) + except EOFError: + print() + sys.exit(0) + try: + n = int(answer.strip()) + if n in _range: + return n + else: + print("Value out of range!") + except ValueError: + print("Invalid input!") + + +def enumerate_choices(choices): + lines = [] + for index, choice in enumerate(choices, start=1): + lines.append(" %d - %s\n" % (index, choice)) + return "".join(lines) + + +def get_modules(): + for importer, modname, ispkg in pkgutil.iter_modules(i3pystatus.__path__): + if modname not in ["core", "tools"]: + yield modname + + +def get_credential_modules(): + verbose = "-v" in sys.argv + + protected_settings = SettingsBase._SettingsBase__PROTECTED_SETTINGS + class_finder = ClassFinder(Module) + credential_modules = defaultdict(dict) + for module_name in get_modules(): + try: + module = class_finder.get_module(module_name) + except ImportError: + if verbose: + print("ImportError while importing", module_name) + continue + clazz = class_finder.get_class(module) + members = [m[0] for m in inspect.getmembers(clazz) if not m[0].startswith('_')] + if any([hasattr(clazz, setting) for setting in protected_settings]): + credential_modules[clazz.__name__]['credentials'] = list(set(protected_settings) & set(members)) + credential_modules[clazz.__name__]['key'] = "%s.%s" % (clazz.__module__, clazz.__name__) + elif hasattr(clazz, 'required'): + protected = [] + required = getattr(clazz, 'required') + for setting in protected_settings: + if setting in required: + protected.append(setting) + if protected: + credential_modules[clazz.__name__]['credentials'] = protected + credential_modules[clazz.__name__]['key'] = "%s.%s" % (clazz.__module__, clazz.__name__) + return credential_modules + + +def main(): + signal.signal(signal.SIGINT, signal_handler) + + print("""%s - part of i3pystatus +This allows you to edit keyring-protected settings of +i3pystatus modules, which are stored globally (independent +of your i3pystatus configuration) in your keyring. + +Options: + -l: list all stored settings (no values are printed) + -v: print informational messages +""" % os.path.basename(sys.argv[0])) + + credential_modules = get_credential_modules() + + if "-l" in sys.argv: + for name, module in credential_modules.items(): + print(name) + for credential in enumerate_choices(module["credentials"]): + if keyring.get_password("%s.%s" % (module["key"], credential), getpass.getuser()): + print(" - %s: set" % credential) + else: + print(" (none stored)") + return + + choices = list(credential_modules.keys()) + prompt = "Choose a module to edit:\n" + prompt += enumerate_choices(choices) + prompt += "> " + + index = get_int_in_range(prompt, range(1, len(choices) + 1)) + module_name = choices[index - 1] + module = credential_modules[module_name] + + prompt = "Choose setting of %s to edit:\n" % module_name + prompt += enumerate_choices(module["credentials"]) + prompt += "> " + + choices = module['credentials'] + index = get_int_in_range(prompt, range(1, len(choices) + 1)) + setting = choices[index - 1] + + answer = getpass.getpass("Enter value for %s:\n> " % setting) + answer2 = getpass.getpass("Re-enter value\n> ") + if answer == answer2: + key = "%s.%s" % (module['key'], setting) + keyring.set_password(key, getpass.getuser(), answer) + print("%s set!" % setting) + else: + print("Values don't match - nothing set.") + +if __name__ == "__main__": + main() diff --git a/setting_util.py b/setting_util.py deleted file mode 100755 index 97cf663..0000000 --- a/setting_util.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python -import glob -import inspect -import os -import keyring -import getpass -import sys -import signal -from i3pystatus import Module, SettingsBase -from i3pystatus.core import ClassFinder -from collections import defaultdict, OrderedDict - -def signal_handler(signal, frame): - sys.exit(0) -signal.signal(signal.SIGINT, signal_handler) - - -def get_int_in_range(prompt, _range): - while True: - answer = input(prompt) - try: - n = int(answer.strip()) - if n in _range: - return n - else: - print("Value out of range!") - except ValueError: - print("Invalid input!") - -modules = [os.path.basename(m.replace('.py', '')) - for m in glob.glob(os.path.join(os.path.dirname(__file__), "i3pystatus", "*.py")) - if not os.path.basename(m).startswith('_')] - -protected_settings = SettingsBase._SettingsBase__PROTECTED_SETTINGS -class_finder = ClassFinder(Module) -credential_modules = defaultdict(dict) -for module_name in modules: - try: - module = class_finder.get_module(module_name) - clazz = class_finder.get_class(module) - members = [m[0] for m in inspect.getmembers(clazz) if not m[0].startswith('_')] - if any([hasattr(clazz, setting) for setting in protected_settings]): - credential_modules[clazz.__name__]['credentials'] = list(set(protected_settings) & set(members)) - credential_modules[clazz.__name__]['key'] = "%s.%s" % (clazz.__module__, clazz.__name__) - elif hasattr(clazz, 'required'): - protected = [] - required = getattr(clazz, 'required') - for setting in protected_settings: - if setting in required: - protected.append(setting) - if protected: - credential_modules[clazz.__name__]['credentials'] = protected - credential_modules[clazz.__name__]['key'] = "%s.%s" % (clazz.__module__, clazz.__name__) - - except ImportError: - continue - -choices = [k for k in credential_modules.keys()] -for idx, module in enumerate(choices, start=1): - print("%s - %s" % (idx, module)) - -index = get_int_in_range("Choose module:\n> ", range(1, len(choices) + 1)) -module_name = choices[index - 1] -module = credential_modules[module_name] - -for idx, setting in enumerate(module['credentials'], start=1): - print("%s - %s" % (idx, setting)) - -choices = module['credentials'] -index = get_int_in_range("Choose setting for %s:\n> " % module_name, range(1, len(choices) + 1)) -setting = choices[index - 1] - -answer = getpass.getpass("Enter value for %s:\n> " % setting) -answer2 = getpass.getpass("Re-enter value\n> ") -if answer == answer2: - key = "%s.%s" % (module['key'], setting) - keyring.set_password(key, getpass.getuser(), answer) - print("%s set!" % setting) -else: - print("Values don't match - nothing set.") diff --git a/setting_util.py b/setting_util.py new file mode 120000 index 0000000..27d2457 --- /dev/null +++ b/setting_util.py @@ -0,0 +1 @@ +i3pystatus/tools/setting_util.py \ No newline at end of file diff --git a/setup.py b/setup.py index 8cf3168..7a5cac8 100755 --- a/setup.py +++ b/setup.py @@ -18,13 +18,15 @@ setup(name="i3pystatus", packages=[ "i3pystatus", "i3pystatus.core", + "i3pystatus.tools", "i3pystatus.mail", "i3pystatus.pulseaudio", "i3pystatus.updates", ], entry_points={ "console_scripts": [ - "i3pystatus = i3pystatus:main" + "i3pystatus = i3pystatus:main", + "i3pystatus-setting-util = i3pystatus.tools.setting_util:main" ] }, zip_safe=True, diff --git a/tests/test_cpu_freq.py b/tests/test_cpu_freq.py index a3a2e03..dee926c 100644 --- a/tests/test_cpu_freq.py +++ b/tests/test_cpu_freq.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 -# coding=utf-8 """ Basic tests for the cpu_freq module """