commit
eafbb60ecf
@ -122,3 +122,23 @@ Also change your i3wm config to the following:
|
||||
position top
|
||||
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".
|
@ -14,6 +14,9 @@ tools for this which make this even easier:
|
||||
periodically.
|
||||
- Settings (already built into above classes) allow you to easily
|
||||
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.
|
||||
|
||||
|
@ -2,10 +2,10 @@ from i3pystatus.core.util import KeyConstraintDict
|
||||
from i3pystatus.core.exceptions import ConfigKeyError, ConfigMissingError
|
||||
import inspect
|
||||
import logging
|
||||
import getpass
|
||||
|
||||
|
||||
class SettingsBase:
|
||||
|
||||
"""
|
||||
Support class for providing a nice and flexible settings interface
|
||||
|
||||
@ -18,6 +18,8 @@ class SettingsBase:
|
||||
Settings are stored as attributes of self.
|
||||
"""
|
||||
|
||||
__PROTECTED_SETTINGS = ["password", "email", "username"]
|
||||
|
||||
settings = (
|
||||
("log_level", "Set to true to log error to .i3pystatus-<pid> file"),
|
||||
)
|
||||
@ -44,21 +46,25 @@ class SettingsBase:
|
||||
return kwargs
|
||||
|
||||
def merge_with_parents_settings():
|
||||
|
||||
settings = tuple()
|
||||
|
||||
# getmro returns base classes according to Method Resolution Order
|
||||
for cls in inspect.getmro(self.__class__):
|
||||
if hasattr(cls, "settings"):
|
||||
settings = settings + cls.settings
|
||||
return settings
|
||||
|
||||
self.__name__ = "{}.{}".format(
|
||||
self.__module__, self.__class__.__name__)
|
||||
|
||||
settings = merge_with_parents_settings()
|
||||
settings = self.flatten_settings(settings)
|
||||
|
||||
sm = KeyConstraintDict(settings, self.required)
|
||||
settings_source = get_argument_dict(args, kwargs)
|
||||
|
||||
protected = self.get_protected_settings(settings_source)
|
||||
settings_source.update(protected)
|
||||
|
||||
try:
|
||||
sm.update(settings_source)
|
||||
except KeyError as exc:
|
||||
@ -70,13 +76,48 @@ class SettingsBase:
|
||||
raise ConfigMissingError(
|
||||
type(self).__name__, missing=exc.keys) from exc
|
||||
|
||||
self.__name__ = "{}.{}".format(
|
||||
self.__module__, self.__class__.__name__)
|
||||
|
||||
self.logger = logging.getLogger(self.__name__)
|
||||
self.logger.setLevel(self.log_level)
|
||||
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):
|
||||
"""Convenience method which is called after all settings are set
|
||||
|
||||
|
@ -23,11 +23,13 @@ class Github(IntervalModule):
|
||||
password = ''
|
||||
format = '{unread}'
|
||||
interval = 600
|
||||
keyring_backend = None
|
||||
|
||||
on_leftclick = 'open_github'
|
||||
|
||||
settings = (
|
||||
('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'),
|
||||
("username", ""),
|
||||
("password", ""),
|
||||
|
@ -16,10 +16,12 @@ class IMAP(Backend):
|
||||
settings = (
|
||||
"host", "port",
|
||||
"username", "password",
|
||||
('keyring_backend', 'alternative keyring backend for retrieving credentials'),
|
||||
"ssl",
|
||||
"mailbox",
|
||||
)
|
||||
required = ("host", "username", "password")
|
||||
keyring_backend = None
|
||||
|
||||
port = 993
|
||||
ssl = True
|
||||
|
@ -20,10 +20,12 @@ class ModsDeChecker(IntervalModule):
|
||||
settings = (
|
||||
("format",
|
||||
"""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"""),
|
||||
"color", "username", "password"
|
||||
)
|
||||
required = ("username", "password")
|
||||
keyring_backend = None
|
||||
|
||||
color = "#7181fe"
|
||||
offset = 0
|
||||
|
@ -29,9 +29,11 @@ class pyLoad(IntervalModule):
|
||||
"format",
|
||||
"captcha_true", "captcha_false",
|
||||
"download_true", "download_false",
|
||||
"username", "password"
|
||||
"username", "password",
|
||||
('keyring_backend', 'alternative keyring backend for retrieving credentials'),
|
||||
)
|
||||
required = ("username", "password")
|
||||
keyring_backend = None
|
||||
|
||||
address = "http://127.0.0.1:8000"
|
||||
format = "{captcha} {progress_all:.1f}% {speed:.1f} kb/s"
|
||||
|
@ -35,6 +35,7 @@ class Reddit(IntervalModule):
|
||||
("format", "Format string used for output."),
|
||||
("username", "Reddit username."),
|
||||
("password", "Reddit password."),
|
||||
('keyring_backend', 'alternative keyring backend for retrieving credentials'),
|
||||
("subreddit", "Subreddit to monitor. Uses frontpage if unspecified."),
|
||||
("sort_by", "'hot', 'new', 'rising', 'controversial', or 'top'."),
|
||||
("color", "Standard color."),
|
||||
@ -48,6 +49,7 @@ class Reddit(IntervalModule):
|
||||
format = "[{submission_subreddit}] {submission_title} ({submission_domain})"
|
||||
username = ""
|
||||
password = ""
|
||||
keyring_backend = None
|
||||
subreddit = ""
|
||||
sort_by = "hot"
|
||||
color = "#FFFFFF"
|
||||
|
@ -5,7 +5,6 @@ from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
class WhosOnLocation():
|
||||
|
||||
email = None
|
||||
password = None
|
||||
session = None
|
||||
@ -68,9 +67,11 @@ class WOL(IntervalModule):
|
||||
password = None
|
||||
|
||||
settings = (
|
||||
('keyring_backend', 'alternative keyring backend for retrieving credentials'),
|
||||
'email',
|
||||
'password'
|
||||
)
|
||||
keyring_backend = None
|
||||
|
||||
color_on_site = '#00FF00'
|
||||
color_off_site = '#ff0000'
|
||||
|
80
setting_util.py
Executable file
80
setting_util.py
Executable 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.")
|
Loading…
Reference in New Issue
Block a user