commit
eafbb60ecf
@ -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".
|
@ -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.
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -23,11 +23,13 @@ class Github(IntervalModule):
|
|||||||
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", ""),
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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
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