Merge remote-tracking branch 'upstream/master'

This commit is contained in:
tyjak 2015-03-16 21:43:45 +01:00
commit ab25a1b6eb
18 changed files with 235 additions and 46 deletions

View File

@ -122,3 +122,23 @@ Also change your i3wm config to the following:
position top position top
workspace_buttons yes workspace_buttons yes
} }
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:
::
# Use the default keyring to retrieve credentials. To determine which backend is the default on your system, run
# python -c 'import keyring; print(keyring.get_keyring())'
status.register('github')
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".

View File

@ -14,6 +14,9 @@ tools for this which make this even easier:
periodically. periodically.
- Settings (already built into above classes) allow you to easily - Settings (already built into above classes) allow you to easily
specify user-modifiable attributes of your class for configuration. specify user-modifiable attributes of your class for configuration.
- For modules that require credentials, it is recommended to add a
keyring_backend setting to allow users to specify their own backends
for retrieving sensitive credentials.
Required settings and default values are also handled. Required settings and default values are also handled.

View File

@ -32,13 +32,24 @@ class Clock(IntervalModule):
on_downscroll = ["scroll_format", -1] on_downscroll = ["scroll_format", -1]
def init(self): def init(self):
lang, enc = os.environ.get('LANG', None).split('.', 1) env_lang = os.environ.get('LC_TIME', None)
if lang != locale.getlocale(locale.LC_TIME)[0]: if env_lang is None:
env_lang = os.environ.get('LANG', None)
if env_lang is not None:
if env_lang.find('.') != -1:
lang = tuple(env_lang.split('.', 1))
else:
lang = (env_lang, None)
else:
lang = (None, None)
if lang != locale.getlocale(locale.LC_TIME):
# affects datetime.time.strftime() in whole program # affects datetime.time.strftime() in whole program
locale.setlocale(locale.LC_TIME, (lang, enc)) locale.setlocale(locale.LC_TIME, lang)
if self.format is None: if self.format is None:
if lang == 'en_US': if lang[0] == 'en_US':
# MDY format - United States of America # MDY format - United States of America
self.format = ["%a %b %-d %X"] self.format = ["%a %b %-d %X"]
else: else:

View File

@ -2,10 +2,10 @@ from i3pystatus.core.util import KeyConstraintDict
from i3pystatus.core.exceptions import ConfigKeyError, ConfigMissingError from i3pystatus.core.exceptions import ConfigKeyError, ConfigMissingError
import inspect import inspect
import logging import logging
import getpass
class SettingsBase: class SettingsBase:
""" """
Support class for providing a nice and flexible settings interface Support class for providing a nice and flexible settings interface
@ -18,6 +18,8 @@ class SettingsBase:
Settings are stored as attributes of self. Settings are stored as attributes of self.
""" """
__PROTECTED_SETTINGS = ["password", "email", "username"]
settings = ( settings = (
("log_level", "Set to true to log error to .i3pystatus-<pid> file"), ("log_level", "Set to true to log error to .i3pystatus-<pid> file"),
) )
@ -44,21 +46,25 @@ class SettingsBase:
return kwargs return kwargs
def merge_with_parents_settings(): def merge_with_parents_settings():
settings = tuple() settings = tuple()
# getmro returns base classes according to Method Resolution Order # getmro returns base classes according to Method Resolution Order
for cls in inspect.getmro(self.__class__): for cls in inspect.getmro(self.__class__):
if hasattr(cls, "settings"): if hasattr(cls, "settings"):
settings = settings + cls.settings settings = settings + cls.settings
return settings return settings
self.__name__ = "{}.{}".format(
self.__module__, self.__class__.__name__)
settings = merge_with_parents_settings() settings = merge_with_parents_settings()
settings = self.flatten_settings(settings) settings = self.flatten_settings(settings)
sm = KeyConstraintDict(settings, self.required) sm = KeyConstraintDict(settings, self.required)
settings_source = get_argument_dict(args, kwargs) settings_source = get_argument_dict(args, kwargs)
protected = self.get_protected_settings(settings_source)
settings_source.update(protected)
try: try:
sm.update(settings_source) sm.update(settings_source)
except KeyError as exc: except KeyError as exc:
@ -70,13 +76,48 @@ class SettingsBase:
raise ConfigMissingError( raise ConfigMissingError(
type(self).__name__, missing=exc.keys) from exc type(self).__name__, missing=exc.keys) from exc
self.__name__ = "{}.{}".format(
self.__module__, self.__class__.__name__)
self.logger = logging.getLogger(self.__name__) self.logger = logging.getLogger(self.__name__)
self.logger.setLevel(self.log_level) self.logger.setLevel(self.log_level)
self.init() self.init()
def get_protected_settings(self, settings_source):
"""
Attempt to retrieve protected settings from keyring if they are not already set.
"""
user_backend = settings_source.get('keyring_backend')
found_settings = dict()
for setting_name in self.__PROTECTED_SETTINGS:
# Nothing to do if the setting is already defined.
if settings_source.get(setting_name):
continue
setting = None
identifier = "%s.%s" % (self.__name__, setting_name)
if hasattr(self, 'required') and setting_name in getattr(self, 'required'):
setting = self.get_setting_from_keyring(identifier, user_backend)
elif hasattr(self, setting_name):
setting = self.get_setting_from_keyring(identifier, user_backend)
if setting:
found_settings.update({setting_name: setting})
return found_settings
def get_setting_from_keyring(self, setting_identifier, keyring_backend=None):
"""
Retrieves a protected setting from keyring
:param setting_identifier: must be in the format package.module.Class.setting
"""
# If a custom keyring backend has been defined, use it.
if keyring_backend:
return keyring_backend.get_password(setting_identifier, getpass.getuser())
# Otherwise try and use default keyring.
try:
import keyring
except ImportError:
pass
else:
return keyring.get_password(setting_identifier, getpass.getuser())
def init(self): def init(self):
"""Convenience method which is called after all settings are set """Convenience method which is called after all settings are set

View File

@ -383,7 +383,7 @@ def make_graph(values, lower_limit=0.0, upper_limit=100.0, style="blocks"):
extent = mx - mn extent = mx - mn
if style == 'blocks': if style == 'blocks':
bar = u'_▁▂▃▄▅▆▇█' bar = '_▁▂▃▄▅▆▇█'
bar_count = len(bar) - 1 bar_count = len(bar) - 1
if extent == 0: if extent == 0:
graph = '_' * len(values) graph = '_' * len(values)
@ -436,7 +436,7 @@ def make_vertical_bar(percentage, width=1):
:param width: How many characters wide the bar should be. :param width: How many characters wide the bar should be.
:returns: Bar as a String :returns: Bar as a String
""" """
bar = u' _▁▂▃▄▅▆▇█' bar = ' _▁▂▃▄▅▆▇█'
percentage //= 10 percentage //= 10
if percentage < 0: if percentage < 0:
output = bar[0] output = bar[0]

View File

@ -29,6 +29,7 @@ class CpuUsage(IntervalModule):
format = "{usage:02}%" format = "{usage:02}%"
format_all = "{core}:{usage:02}%" format_all = "{core}:{usage:02}%"
exclude_average = False exclude_average = False
interval = 1
settings = ( settings = (
("format", "format string."), ("format", "format string."),
("format_all", ("format string used for {usage_all} per core. " ("format_all", ("format string used for {usage_all} per core. "
@ -40,7 +41,6 @@ class CpuUsage(IntervalModule):
def init(self): def init(self):
self.prev_total = defaultdict(int) self.prev_total = defaultdict(int)
self.prev_busy = defaultdict(int) self.prev_busy = defaultdict(int)
self.interval = 1
self.formatter = Formatter() self.formatter = Formatter()
def get_cpu_timings(self): def get_cpu_timings(self):

View File

@ -38,6 +38,8 @@ class Disk(IntervalModule):
self.output = {} self.output = {}
return return
critical = available < self.critical_limit
cdict = { cdict = {
"total": (stat.f_bsize * stat.f_blocks) / self.divisor, "total": (stat.f_bsize * stat.f_blocks) / self.divisor,
"free": (stat.f_bsize * stat.f_bfree) / self.divisor, "free": (stat.f_bsize * stat.f_bfree) / self.divisor,
@ -51,6 +53,6 @@ class Disk(IntervalModule):
self.output = { self.output = {
"full_text": self.format.format(**cdict), "full_text": self.format.format(**cdict),
"color": self.color if available > self.critical_limit else self.critical_color, "color": self.critical_color if critical else self.color,
"urgent": available > self.critical_limit "urgent": critical
} }

View File

@ -16,18 +16,20 @@ class Github(IntervalModule):
* `{unread_count}` - number of unread notifications, empty if 0 * `{unread_count}` - number of unread notifications, empty if 0
""" """
unread_marker = u"" unread_marker = ""
unread = '' unread = ''
color = '#78EAF2' color = '#78EAF2'
username = '' username = ''
password = '' password = ''
format = '{unread}' format = '{unread}'
interval = 600 interval = 600
keyring_backend = None
on_leftclick = 'open_github' on_leftclick = 'open_github'
settings = ( settings = (
('format', 'format string'), ('format', 'format string'),
('keyring_backend', 'alternative keyring backend for retrieving credentials'),
('unread_marker', 'sets the string that the "unread" formatter shows when there are pending notifications'), ('unread_marker', 'sets the string that the "unread" formatter shows when there are pending notifications'),
("username", ""), ("username", ""),
("password", ""), ("password", ""),

View File

@ -16,10 +16,12 @@ class IMAP(Backend):
settings = ( settings = (
"host", "port", "host", "port",
"username", "password", "username", "password",
('keyring_backend', 'alternative keyring backend for retrieving credentials'),
"ssl", "ssl",
"mailbox", "mailbox",
) )
required = ("host", "username", "password") required = ("host", "username", "password")
keyring_backend = None
port = 993 port = 993
ssl = True ssl = True

View File

@ -20,10 +20,12 @@ class ModsDeChecker(IntervalModule):
settings = ( settings = (
("format", ("format",
"""Use {unread} as the formatter for number of unread posts"""), """Use {unread} as the formatter for number of unread posts"""),
('keyring_backend', 'alternative keyring backend for retrieving credentials'),
("offset", """subtract number of posts before output"""), ("offset", """subtract number of posts before output"""),
"color", "username", "password" "color", "username", "password"
) )
required = ("username", "password") required = ("username", "password")
keyring_backend = None
color = "#7181fe" color = "#7181fe"
offset = 0 offset = 0

View File

@ -1,5 +1,6 @@
import socket import socket
from os.path import basename from os.path import basename
from math import floor
from i3pystatus import IntervalModule, formatp from i3pystatus import IntervalModule, formatp
from i3pystatus.core.util import TimeWrapper from i3pystatus.core.util import TimeWrapper
@ -34,8 +35,10 @@ class MPD(IntervalModule):
("format", "formatp string"), ("format", "formatp string"),
("status", "Dictionary mapping pause, play and stop to output"), ("status", "Dictionary mapping pause, play and stop to output"),
("color", "The color of the text"), ("color", "The color of the text"),
("text_len", "Defines max length for title, album and artist, if truncated ellipsis are appended as indicator"), ("max_field_len", "Defines max length for in truncate_fields defined fields, if truncated, ellipsis are appended as indicator. It's applied *before* max_len. Value of 0 disables this."),
("truncate_fields", "fileds that will be truncated if exceeding text_len"), ("max_len", "Defines max length for the hole string, if exceeding fields specefied in truncate_fields are truncated equaly. If truncated, ellipsis are appended as indicator. It's applied *after* max_field_len. Value of 0 disables this."),
("truncate_fields", "fields that will be truncated if exceeding max_field_len or max_len."),
) )
host = "localhost" host = "localhost"
@ -48,7 +51,8 @@ class MPD(IntervalModule):
"stop": "", "stop": "",
} }
color = "#FFFFFF" color = "#FFFFFF"
text_len = 25 max_field_len = 25
max_len = 100
truncate_fields = ("title", "album", "artist") truncate_fields = ("title", "album", "artist")
on_leftclick = "switch_playpause" on_leftclick = "switch_playpause"
on_rightclick = "next_song" on_rightclick = "next_song"
@ -91,17 +95,30 @@ class MPD(IntervalModule):
} }
for key in self.truncate_fields:
if len(fdict[key]) > self.text_len:
fdict[key] = fdict[key][:self.text_len - 1] + ""
if not fdict["title"] and "filename" in fdict: if not fdict["title"] and "filename" in fdict:
fdict["filename"] = '.'.join( fdict["filename"] = '.'.join(
basename(currentsong["file"]).split('.')[:-1]) basename(currentsong["file"]).split('.')[:-1])
else: else:
fdict["filename"] = "" fdict["filename"] = ""
if self.max_field_len > 0:
for key in self.truncate_fields:
if len(fdict[key]) > self.max_field_len:
fdict[key] = fdict[key][:self.max_field_len - 1] + ""
full_text = formatp(self.format, **fdict).strip()
full_text_len = len(full_text)
if full_text_len > self.max_len and self.max_len > 0:
shrink = floor((self.max_len - full_text_len) /
len(self.truncate_fields)) - 1
for key in self.truncate_fields:
fdict[key] = fdict[key][:shrink] + ""
full_text = formatp(self.format, **fdict).strip()
self.output = { self.output = {
"full_text": formatp(self.format, **fdict).strip(), "full_text": full_text,
"color": self.color, "color": self.color,
} }

View File

@ -338,7 +338,6 @@ class Network(IntervalModule, ColorRangeModule):
format_values = dict(kbs="", network_graph="", bytes_sent="", bytes_recv="", packets_sent="", packets_recv="", format_values = dict(kbs="", network_graph="", bytes_sent="", bytes_recv="", packets_sent="", packets_recv="",
interface="", v4="", v4mask="", v4cidr="", v6="", v6mask="", v6cidr="", mac="", interface="", v4="", v4mask="", v4cidr="", v6="", v6mask="", v6cidr="", mac="",
essid="", freq="", quality="", quality_bar="") essid="", freq="", quality="", quality_bar="")
color = None
if self.network_traffic: if self.network_traffic:
network_usage = self.network_traffic.get_usage(self.interface) network_usage = self.network_traffic.get_usage(self.interface)
format_values.update(network_usage) format_values.update(network_usage)
@ -352,22 +351,22 @@ class Network(IntervalModule, ColorRangeModule):
format_values['network_graph'] = self.get_network_graph(kbs) format_values['network_graph'] = self.get_network_graph(kbs)
format_values['kbs'] = "{0:.1f}".format(round(kbs, 2)).rjust(6) format_values['kbs'] = "{0:.1f}".format(round(kbs, 2)).rjust(6)
color = self.get_gradient(kbs, self.colors, self.upper_limit) color = self.get_gradient(kbs, self.colors, self.upper_limit)
else:
color = None
if sysfs_interface_up(self.interface, self.unknown_up):
if not color:
color = self.color_up
format_str = self.format_up
else:
color = self.color_down
format_str = self.format_down
network_info = self.network_info.get_info(self.interface) network_info = self.network_info.get_info(self.interface)
format_values.update(network_info) format_values.update(network_info)
format_values['interface'] = self.interface format_values['interface'] = self.interface
if sysfs_interface_up(self.interface, self.unknown_up):
if not self.dynamic_color:
color = self.color_up
self.output = { self.output = {
"full_text": self.format_up.format(**format_values), "full_text": format_str.format(**format_values),
'color': color,
}
else:
color = self.color_down
self.output = {
"full_text": self.format_down.format(**format_values),
'color': color, 'color': color,
} }

View File

@ -80,6 +80,10 @@ class NowPlaying(IntervalModule):
def get_player(self): def get_player(self):
if self.player: if self.player:
player = "org.mpris.MediaPlayer2." + self.player player = "org.mpris.MediaPlayer2." + self.player
try:
return dbus.SessionBus().get_object(player, "/org/mpris/MediaPlayer2")
except dbus.exceptions.DBusException:
raise NoPlayerException()
else: else:
player = self.find_player() player = self.find_player()
return dbus.SessionBus().get_object(player, "/org/mpris/MediaPlayer2") return dbus.SessionBus().get_object(player, "/org/mpris/MediaPlayer2")

View File

@ -29,9 +29,11 @@ class pyLoad(IntervalModule):
"format", "format",
"captcha_true", "captcha_false", "captcha_true", "captcha_false",
"download_true", "download_false", "download_true", "download_false",
"username", "password" "username", "password",
('keyring_backend', 'alternative keyring backend for retrieving credentials'),
) )
required = ("username", "password") required = ("username", "password")
keyring_backend = None
address = "http://127.0.0.1:8000" address = "http://127.0.0.1:8000"
format = "{captcha} {progress_all:.1f}% {speed:.1f} kb/s" format = "{captcha} {progress_all:.1f}% {speed:.1f} kb/s"

View File

@ -35,6 +35,7 @@ class Reddit(IntervalModule):
("format", "Format string used for output."), ("format", "Format string used for output."),
("username", "Reddit username."), ("username", "Reddit username."),
("password", "Reddit password."), ("password", "Reddit password."),
('keyring_backend', 'alternative keyring backend for retrieving credentials'),
("subreddit", "Subreddit to monitor. Uses frontpage if unspecified."), ("subreddit", "Subreddit to monitor. Uses frontpage if unspecified."),
("sort_by", "'hot', 'new', 'rising', 'controversial', or 'top'."), ("sort_by", "'hot', 'new', 'rising', 'controversial', or 'top'."),
("color", "Standard color."), ("color", "Standard color."),
@ -48,6 +49,7 @@ class Reddit(IntervalModule):
format = "[{submission_subreddit}] {submission_title} ({submission_domain})" format = "[{submission_subreddit}] {submission_title} ({submission_domain})"
username = "" username = ""
password = "" password = ""
keyring_backend = None
subreddit = "" subreddit = ""
sort_by = "hot" sort_by = "hot"
color = "#FFFFFF" color = "#FFFFFF"

View File

@ -1,6 +1,3 @@
import re
import glob
from i3pystatus import IntervalModule from i3pystatus import IntervalModule
@ -16,10 +13,14 @@ class Temperature(IntervalModule):
"format string used for output. {temp} is the temperature in degrees celsius"), "format string used for output. {temp} is the temperature in degrees celsius"),
"color", "color",
"file", "file",
"alert_temp",
"alert_color",
) )
format = "{temp} °C" format = "{temp} °C"
color = "#FFFFFF" color = "#FFFFFF"
file = "/sys/class/thermal/thermal_zone0/temp" file = "/sys/class/thermal/thermal_zone0/temp"
alert_temp = 90
alert_color = "#FF0000"
def run(self): def run(self):
with open(self.file, "r") as f: with open(self.file, "r") as f:
@ -27,5 +28,5 @@ class Temperature(IntervalModule):
self.output = { self.output = {
"full_text": self.format.format(temp=temp), "full_text": self.format.format(temp=temp),
"color": self.color, "color": self.color if temp < self.alert_temp else self.alert_color,
} }

View File

@ -5,7 +5,6 @@ from bs4 import BeautifulSoup
class WhosOnLocation(): class WhosOnLocation():
email = None email = None
password = None password = None
session = None session = None
@ -68,9 +67,11 @@ class WOL(IntervalModule):
password = None password = None
settings = ( settings = (
('keyring_backend', 'alternative keyring backend for retrieving credentials'),
'email', 'email',
'password' 'password'
) )
keyring_backend = None
color_on_site = '#00FF00' color_on_site = '#00FF00'
color_off_site = '#ff0000' color_off_site = '#ff0000'

80
setting_util.py Executable file
View File

@ -0,0 +1,80 @@
#!/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.")