From 4a04e51da747fa4433523919e75edf51ffb0af9e Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Mon, 18 Dec 2017 01:10:51 +0100 Subject: [PATCH] Major refactoring #2 --- platypush/__init__.py | 161 ++-------------------- platypush/backend/__init__.py | 8 +- platypush/backend/pushbullet/__init__.py | 1 + platypush/bus/__init__.py | 23 ++++ platypush/config/__init__.py | 145 +++++++++++++++++++ platypush/message/request/__init__.py | 2 +- platypush/plugins/__init__.py | 19 +-- platypush/plugins/light/hue/__init__.py | 32 +++-- platypush/plugins/music/mpd/__init__.py | 13 +- platypush/plugins/switch/wemo/__init__.py | 4 +- platypush/pusher/__init__.py | 22 ++- platypush/utils/__init__.py | 66 +++++++++ 12 files changed, 308 insertions(+), 188 deletions(-) create mode 100644 platypush/bus/__init__.py create mode 100644 platypush/config/__init__.py create mode 100644 platypush/utils/__init__.py diff --git a/platypush/__init__.py b/platypush/__init__.py index 2c2776e20..2b1b69b60 100644 --- a/platypush/__init__.py +++ b/platypush/__init__.py @@ -1,16 +1,13 @@ -import functools -import importlib -import os import logging -import socket import sys import traceback -import yaml -from queue import Queue from threading import Thread from getopt import getopt +from .bus import Bus +from .config import Config +from .utils import get_or_load_plugin, init_backends from .message.request import Request from .message.response import Response @@ -19,50 +16,13 @@ __version__ = '0.3.3' #-----------# -config = {} -modules = {} -wrkdir = os.path.dirname(os.path.realpath(__file__)) - - -def _init_plugin(plugin_name, reload=False): - global modules - global config - - if plugin_name in modules and not reload: - return modules[plugin_name] - - try: - module = importlib.import_module(__package__ + '.plugins.' + plugin_name) - except ModuleNotFoundError as e: - logging.warn('No such plugin: {}'.format(plugin_name)) - raise RuntimeError(e) - - # e.g. plugins.music.mpd main class: MusicMpdPlugin - cls_name = functools.reduce( - lambda a,b: a.title() + b.title(), - (plugin_name.title().split('.')) - ) + 'Plugin' - - plugin_conf = config[plugin_name] if plugin_name in config else {} - - try: - plugin = getattr(module, cls_name)(plugin_conf) - modules[plugin_name] = plugin - except AttributeError as e: - logging.warn('No such class in {}: {}'.format( - plugin_name, cls_name)) - raise RuntimeError(e) - - return plugin - - def _execute_request(request, retry=True): tokens = request.action.split('.') module_name = str.join('.', tokens[:-1]) method_name = tokens[-1:][0] try: - plugin = _init_plugin(module_name) + plugin = get_or_load_plugin(module_name) except RuntimeError as e: # Module/class not found logging.exception(e) return @@ -78,7 +38,7 @@ def _execute_request(request, retry=True): logging.exception(e) if retry: logging.info('Reloading plugin {} and retrying'.format(module_name)) - _init_plugin(module_name, reload=True) + get_or_load_plugin(module_name, reload=True) _execute_request(request, retry=False) finally: # Send the response on the backend that received the request @@ -95,131 +55,32 @@ def on_msg(msg): logging.info('Received response: {}'.format(msg)) -def parse_config_file(config_file=None): - global config - - if config_file: - locations = [config_file] - else: - locations = [ - # ./config.yaml - os.path.join(wrkdir, 'config.yaml'), - # ~/.config/platypush/config.yaml - os.path.join(os.environ['HOME'], '.config', 'platypush', 'config.yaml'), - # /etc/platypush/config.yaml - os.path.join(os.sep, 'etc', 'platypush', 'config.yaml'), - ] - - for loc in locations: - try: - with open(loc,'r') as f: - config = yaml.load(f) - except FileNotFoundError as e: - pass - - for section in config: - if 'disabled' in config[section] and config[section]['disabled']: - del config[section] - - if 'logging' not in config: - config['logging'] = logging.INFO - else: - config['logging'] = getattr(logging, config['logging'].upper()) - - if 'device_id' not in config: - config['device_id'] = socket.gethostname() - - return config - - -def init_backends(config, bus=None): - backends = {} - - for k in config.keys(): - if not k.startswith('backend.'): continue - - module = importlib.import_module(__package__ + '.' + k) - - # e.g. backend.pushbullet main class: PushbulletBackend - cls_name = functools.reduce( - lambda a,b: a.title() + b.title(), - (module.__name__.title().split('.')[2:]) - ) + 'Backend' - - # Ignore the pusher attribute here - if 'pusher' in config[k]: del config[k]['pusher'] - - try: - b = getattr(module, cls_name)(bus=bus, **config[k]) - name = '.'.join((k.split('.'))[1:]) - backends[name] = b - except AttributeError as e: - logging.warn('No such class in {}: {}'.format( - module.__name__, cls_name)) - raise RuntimeError(e) - - return backends - - -def get_default_pusher_backend(config): - backends = ['.'.join((k.split('.'))[1:]) - for k in config.keys() if k.startswith('backend.') - and 'pusher' in config[k] and config[k]['pusher'] is True] - - return backends[0] if backends else None - - -def get_logging_level(): - global config - return config['logging'] - - -def get_device_id(): - global config - return config['device_id'] if 'device_id' in config else None - - def main(): print('Starting platypush v.{}'.format(__version__)) - - debug = False config_file = None - plugins_dir = os.path.join(wrkdir, 'plugins') - sys.path.insert(0, plugins_dir) - optlist, args = getopt(sys.argv[1:], 'vh') for opt, arg in optlist: if opt == '-c': config_file = arg - if opt == '-v': - debug = True elif opt == '-h': print(''' -Usage: {} [-v] [-h] [-c ] - -v Enable debug mode +Usage: {} [-h] [-c ] -h Show this help -c Path to the configuration file (default: ./config.yaml) '''.format(sys.argv[0])) return - config = parse_config_file(config_file) - if debug: config['logging'] = logging.DEBUG + Config.init(config_file) + logging.basicConfig(level=Config.get('logging'), stream=sys.stdout) - logging.basicConfig(level=get_logging_level(), stream=sys.stdout) - logging.debug('Configuration dump: {}'.format(config)) - - bus = Queue() - backends = init_backends(config, bus) + bus = Bus(on_msg=on_msg) + backends = init_backends(bus) for backend in backends.values(): backend.start() - while True: - try: - on_msg(bus.get()) - except KeyboardInterrupt: - return + bus.loop_forever() if __name__ == '__main__': main() diff --git a/platypush/backend/__init__.py b/platypush/backend/__init__.py index 98faf8b79..8852f11d5 100644 --- a/platypush/backend/__init__.py +++ b/platypush/backend/__init__.py @@ -1,11 +1,11 @@ import importlib import logging import sys -import platypush from queue import Queue from threading import Thread +from platypush.config import Config from platypush.message import Message from platypush.message.request import Request from platypush.message.response import Response @@ -24,11 +24,11 @@ class Backend(Thread): # If no bus is specified, create an internal queue where # the received messages will be pushed self.bus = bus if bus else Queue() - self.device_id = platypush.get_device_id() + self.device_id = Config.get('device_id') self.msgtypes = {} Thread.__init__(self) - logging.basicConfig(stream=sys.stdout, level=platypush.get_logging_level() + logging.basicConfig(stream=sys.stdout, level=Config.get('logging') if 'logging' not in kwargs else getattr(logging, kwargs['logging'])) @@ -87,7 +87,7 @@ class Backend(Thread): logging.debug('Message received on the backend: {}'.format(msg)) msg.backend = self # Augment message - self.bus.put(msg) + self.bus.post(msg) def send_request(self, request): """ diff --git a/platypush/backend/pushbullet/__init__.py b/platypush/backend/pushbullet/__init__.py index 67464e554..cddf5a8d9 100644 --- a/platypush/backend/pushbullet/__init__.py +++ b/platypush/backend/pushbullet/__init__.py @@ -4,6 +4,7 @@ import requests import time import websocket +from platypush.config import Config from platypush.message import Message from .. import Backend diff --git a/platypush/bus/__init__.py b/platypush/bus/__init__.py new file mode 100644 index 000000000..1ee474586 --- /dev/null +++ b/platypush/bus/__init__.py @@ -0,0 +1,23 @@ +from queue import Queue + +class Bus(object): + """ Main local bus where the daemon will listen for new messages """ + + def __init__(self, on_msg): + self.bus = Queue() + self.on_msg = on_msg + + def post(self, msg): + """ Sends a message to the bus """ + self.bus.put(msg) + + def loop_forever(self): + """ Reads messages from the bus """ + while True: + try: + self.on_msg(self.bus.get()) + except KeyboardInterrupt: + return + +# vim:sw=4:ts=4:et: + diff --git a/platypush/config/__init__.py b/platypush/config/__init__.py new file mode 100644 index 000000000..9e7603add --- /dev/null +++ b/platypush/config/__init__.py @@ -0,0 +1,145 @@ +import logging +import os +import socket +import yaml + +""" Config singleton instance """ +_default_config_instance = None + +class Config(object): + """ + Configuration base class + Usage: + - Initialize config from one of the default paths: + Config.init() + - Initialize config from a custom path + Config.init(config_file_path) + - Set a value + Config.set('foo', 'bar') + - Get a value + Config.get('foo') + """ + + """ + Default config file locations: + - $HOME/.config/platypush/config.yaml + - /etc/platypush/config.yaml + """ + _cfgfile_locations = [ + os.path.join(os.environ['HOME'], '.config', 'platypush', 'config.yaml'), + os.path.join(os.sep, 'etc', 'platypush', 'config.yaml'), + ] + + def __init__(self, cfgfile=None): + """ + Constructor. Always use the class as a singleton (i.e. through + Config.init), you won't probably need to call the constructor directly + Params: + cfgfile -- Config file path (default: retrieve the first + available location in _cfgfile_locations) + """ + + if cfgfile is None: + cfgfile = self._get_default_cfgfile() + + if cfgfile is None: + raise RuntimeError('No config file specified and nothing found in {}' + .format(self._cfgfile_locations)) + + with open(cfgfile, 'r') as fp: + self._config = yaml.load(fp) + + for section in self._config: + if 'disabled' in self._config[section] \ + and self._config[section]['disabled']: + del self._config[section] + + if 'logging' not in self._config: + self._config['logging'] = logging.INFO + else: + self._config['logging'] = getattr(logging, self._config['logging'].upper()) + + if 'device_id' not in self._config: + self._config['device_id'] = socket.gethostname() + + self._init_backends() + self._init_plugins() + + def _init_backends(self): + self.backends = {} + for key in self._config.keys(): + if not key.startswith('backend.'): continue + backend_name = '.'.join(key.split('.')[1:]) + self.backends[backend_name] = self._config[key] + + def _init_plugins(self): + self.plugins = {} + for key in self._config.keys(): + if key.startswith('backend.'): continue + plugin_name = '.'.join(key.split('.')[1:]) + self.plugins[plugin_name] = self._config[key] + + @staticmethod + def get_backends(): + global _default_config_instance + if _default_config_instance is None: _default_config_instance = Config() + return _default_config_instance.backends + + @staticmethod + def get_plugins(): + global _default_config_instance + if _default_config_instance is None: _default_config_instance = Config() + return _default_config_instance.plugins + + @staticmethod + def get_default_pusher_backend(): + """ + Gets the default pusher backend from the config + """ + backends = [k for k in Config.get_backends().keys() + if 'pusher' in Config.get_backends()[k] + and Config.get_backends()[k]['pusher'] is True] + + return backends[0] if backends else None + + + @classmethod + def _get_default_cfgfile(cls): + for location in cls._cfgfile_locations: + if os.path.isfile(location): return location + + @staticmethod + def init(cfgfile=None): + """ + Initializes the config object singleton + Params: + cfgfile -- path to the config file - default: _cfgfile_locations + """ + global _default_config_instance + _default_config_instance = Config(cfgfile) + + @staticmethod + def get(key): + """ + Gets a config value + Params: + key -- Config key to get + """ + global _default_config_instance + if _default_config_instance is None: _default_config_instance = Config() + return _default_config_instance._config[key] + + @staticmethod + def set(key, value): + """ + Sets a config value + Params: + key -- Config key to set + value -- Value for key + """ + global _default_config_instance + if _default_config_instance is None: _default_config_instance = Config() + _default_config_instance._config[key] = key + +# vim:sw=4:ts=4:et: + diff --git a/platypush/message/request/__init__.py b/platypush/message/request/__init__.py index f88ba7eb7..6b03e368f 100644 --- a/platypush/message/request/__init__.py +++ b/platypush/message/request/__init__.py @@ -25,7 +25,7 @@ class Request(Message): args = { 'target' : msg['target'], 'action' : msg['action'], - 'args' : msg['args'], + 'args' : msg['args'] if 'args' in msg else {}, } if 'origin' in msg: args['origin'] = msg['origin'] diff --git a/platypush/plugins/__init__.py b/platypush/plugins/__init__.py index 3747a9cf1..f03079ebf 100644 --- a/platypush/plugins/__init__.py +++ b/platypush/plugins/__init__.py @@ -1,23 +1,16 @@ -import os import sys import logging -from platypush import get_logging_level +from platypush.config import Config from platypush.message.response import Response class Plugin(object): - def __init__(self, config): - self.config = config - logging.basicConfig(stream=sys.stdout, level=get_logging_level() - if 'logging' not in config - else getattr(logging, config.pop('logging'))) + """ Base plugin class """ - for cls in reversed(self.__class__.mro()): - if cls is not object: - try: - cls._init(self) - except AttributeError as e: - pass + def __init__(self, **kwargs): + logging.basicConfig(stream=sys.stdout, level=Config.get('logging') + if 'logging' not in kwargs + else getattr(logging, kwargs['logging'])) def run(self, method, *args, **kwargs): return getattr(self, method)(*args, **kwargs) diff --git a/platypush/plugins/light/hue/__init__.py b/platypush/plugins/light/hue/__init__.py index 000632d04..d43b6b618 100644 --- a/platypush/plugins/light/hue/__init__.py +++ b/platypush/plugins/light/hue/__init__.py @@ -7,12 +7,24 @@ from platypush.message.response import Response from .. import LightPlugin class LightHuePlugin(LightPlugin): + """ Philips Hue lights plugin """ + MAX_BRI = 255 MAX_SAT = 255 MAX_HUE = 65535 - def _init(self): - self.bridge_address = self.config['bridge'] + def __init__(self, bridge, lights=None, groups=None): + """ + Constructor + Params: + bridge -- Bridge address or hostname + lights -- Lights to be controlled (default: all) + groups -- Groups to be controlled (default: all) + """ + + super().__init__() + + self.bridge_address = bridge self.bridge = None logging.info('Initializing Hue lights plugin - bridge: "{}"'. format(self.bridge_address)) @@ -20,20 +32,18 @@ class LightHuePlugin(LightPlugin): self.connect() self.lights = []; self.groups = [] - if 'lights' in self.config: - self.lights = self.config['lights'] - elif 'groups' in self.config: - self.groups = self.config['groups'] - self._expand_groups(self.groups) + if lights: + self.lights = lights + elif groups: + self.groups = groups + self._expand_groups() else: self.lights = [l.name for l in self.bridge.lights] logging.info('Configured lights: "{}"'. format(self.lights)) - def _expand_groups(self, group_names): - groups = [g for g in self.bridge.groups - if g.name in group_names] - + def _expand_groups(self): + groups = [g for g in self.bridge.groups if g.name in self.groups] for g in groups: self.lights.extend([l.name for l in g.lights]) diff --git a/platypush/plugins/music/mpd/__init__.py b/platypush/plugins/music/mpd/__init__.py index 5d23a9fb1..7b6d8200f 100644 --- a/platypush/plugins/music/mpd/__init__.py +++ b/platypush/plugins/music/mpd/__init__.py @@ -5,9 +5,18 @@ from platypush.message.response import Response from .. import MusicPlugin class MusicMpdPlugin(MusicPlugin): - def _init(self): + def __init__(self, host, port): + """ + Constructor + Params: + host -- MPD host + port -- MPD port + """ + + self.host = host + self.port = port self.client = mpd.MPDClient(use_unicode=True) - self.client.connect(self.config['host'], self.config['port']) + self.client.connect(self.host, self.port) def _exec(self, method, *args, **kwargs): getattr(self.client, method)(*args, **kwargs) diff --git a/platypush/plugins/switch/wemo/__init__.py b/platypush/plugins/switch/wemo/__init__.py index 77df81698..a4109f95f 100644 --- a/platypush/plugins/switch/wemo/__init__.py +++ b/platypush/plugins/switch/wemo/__init__.py @@ -7,7 +7,9 @@ from platypush.message.response import Response from .. import SwitchPlugin class SwitchWemoPlugin(SwitchPlugin): - def _init(self, discovery_seconds=3): + def __init__(self, discovery_seconds=3): + super().__init__() + self.discovery_seconds=discovery_seconds self.env = Environment() self.env.start() diff --git a/platypush/pusher/__init__.py b/platypush/pusher/__init__.py index 69c84d50d..9e4f7ffce 100755 --- a/platypush/pusher/__init__.py +++ b/platypush/pusher/__init__.py @@ -2,7 +2,8 @@ import argparse import re import sys -from platypush import init_backends, get_default_pusher_backend, parse_config_file +from platypush.config import Config +from platypush.utils import init_backends from platypush.message.request import Request def print_usage(): @@ -15,15 +16,16 @@ def print_usage(): '''.format(sys.argv[0])) -def pusher(target, action, backend=None, **kwargs): - config = parse_config_file() +def pusher(target, action, backend=None, config=None, **kwargs): + Config.init(config) if target == 'localhost': backend = 'local' elif not backend: - backend = get_default_pusher_backend(config) + backend = Config.get_default_pusher_backend() - backends = init_backends(config) + # TODO Initialize a local bus and wait for the response + backends = init_backends() if backend not in backends: raise RuntimeError('No such backend configured: {}'.format(backend)) @@ -37,6 +39,11 @@ def pusher(target, action, backend=None, **kwargs): def main(): parser = argparse.ArgumentParser() + parser.add_argument('--config', '-c', dest='config', required=False, + help="Configuration file path (default: " + + "~/.config/platypush/config.yaml or " + + "/etc/platypush/config.yaml") + parser.add_argument('--target', '-t', dest='target', required=True, help="Destination of the command") @@ -59,11 +66,14 @@ def main(): payload[re.sub('^-+', '', args[i])] = args[i+1] pusher(target=opts.target, action=opts.action, - backend=opts.backend if 'backend' in opts else None, **payload) + backend=opts.backend if 'backend' in opts else None, + config=opts.config if 'config' in opts else None, + **payload) if __name__ == '__main__': main() + # vim:sw=4:ts=4:et: diff --git a/platypush/utils/__init__.py b/platypush/utils/__init__.py new file mode 100644 index 000000000..90cf5184f --- /dev/null +++ b/platypush/utils/__init__.py @@ -0,0 +1,66 @@ +import functools +import importlib +import logging + +from platypush.config import Config + +modules = {} + +def get_or_load_plugin(plugin_name, reload=False): + global modules + + if plugin_name in modules and not reload: + return modules[plugin_name] + + try: + module = importlib.import_module('platypush.plugins.' + plugin_name) + except ModuleNotFoundError as e: + logging.warn('No such plugin: {}'.format(plugin_name)) + raise RuntimeError(e) + + # e.g. plugins.music.mpd main class: MusicMpdPlugin + cls_name = functools.reduce( + lambda a,b: a.title() + b.title(), + (plugin_name.title().split('.')) + ) + 'Plugin' + + plugin_conf = Config.get_plugins()[plugin_name] \ + if plugin_name in Config.get_plugins() else {} + + try: + plugin = getattr(module, cls_name)(**plugin_conf) + modules[plugin_name] = plugin + except AttributeError as e: + logging.warn('No such class in {}: {}'.format( + plugin_name, cls_name)) + raise RuntimeError(e) + + return plugin + + +def init_backends(bus=None): + backends = {} + + for k in Config.get_backends().keys(): + module = importlib.import_module('platypush.backend.' + k) + cfg = Config.get_backends()[k] + + # e.g. backend.pushbullet main class: PushbulletBackend + cls_name = functools.reduce( + lambda a,b: a.title() + b.title(), + (module.__name__.title().split('.')[2:]) + ) + 'Backend' + + try: + b = getattr(module, cls_name)(bus=bus, **cfg) + backends[k] = b + except AttributeError as e: + logging.warn('No such class in {}: {}'.format( + module.__name__, cls_name)) + raise RuntimeError(e) + + return backends + + +# vim:sw=4:ts=4:et: +