platypush/platypush/__init__.py
Fabio Manganiello 339e7b73a5 Major refactoring.
Solves, among the others, #2, #18 and #22
2017-12-17 16:15:44 +01:00

230 lines
6.2 KiB
Python

import functools
import importlib
import os
import logging
import json
import socket
import sys
import traceback
import websocket
import yaml
from queue import Queue
from threading import Thread
from getopt import getopt
from .message.request import Request
from .message.response import Response
__author__ = 'Fabio Manganiello <info@fabiomanganiello.com>'
__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)
except RuntimeError as e: # Module/class not found
logging.exception(e)
return
try:
response = plugin.run(method=method_name, **request.args)
if response and response.is_error():
logging.warn('Response processed with errors: {}'.format(response))
else:
logging.info('Processed response: {}'.format(response))
except Exception as e:
response = Response(output=None, errors=[str(e), traceback.format_exc()])
logging.exception(e)
if retry:
logging.info('Reloading plugin {} and retrying'.format(module_name))
_init_plugin(module_name, reload=True)
_execute_request(request, retry=False)
finally:
# Send the response on the backend that received the request
if request.backend and request.origin:
response.target = request.origin
request.backend.send_response(response)
def on_msg(msg):
if isinstance(msg, Request):
logging.info('Processing request: {}'.format(msg))
Thread(target=_execute_request, args=(msg,)).start()
elif isinstance(msg, Response):
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 <config_file>]
-v Enable debug mode
-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
logging.basicConfig(level=get_logging_level(), stream=sys.stdout)
logging.debug('Configuration dump: {}'.format(config))
bus = Queue()
backends = init_backends(config, bus)
for backend in backends.values():
backend.start()
while True:
try:
on_msg(bus.get())
except KeyboardInterrupt:
return
if __name__ == '__main__':
main()
# vim:sw=4:ts=4:et: