358 lines
12 KiB
Python
358 lines
12 KiB
Python
import datetime
|
|
import importlib
|
|
import inspect
|
|
import logging
|
|
import os
|
|
import pathlib
|
|
import pkgutil
|
|
import re
|
|
import socket
|
|
import sys
|
|
from typing import Optional
|
|
|
|
import yaml
|
|
|
|
from platypush.utils import get_hash, is_functional_procedure, is_functional_hook
|
|
|
|
""" 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)
|
|
- 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.path.expanduser('~'), '.config', 'platypush', 'config.yaml'),
|
|
os.path.join(os.sep, 'etc', 'platypush', 'config.yaml'),
|
|
]
|
|
|
|
_default_constants = {
|
|
'today': datetime.date.today,
|
|
'now': datetime.datetime.now,
|
|
}
|
|
|
|
_workdir_location = os.path.join(os.path.expanduser('~'), '.local', 'share', 'platypush')
|
|
_included_files = set()
|
|
|
|
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))
|
|
|
|
self._cfgfile = os.path.abspath(os.path.expanduser(cfgfile))
|
|
self._config = self._read_config_file(self._cfgfile)
|
|
|
|
if 'token' in self._config:
|
|
self._config['token'] = self._config['token']
|
|
self._config['token_hash'] = get_hash(self._config['token'])
|
|
|
|
if 'workdir' not in self._config:
|
|
self._config['workdir'] = self._workdir_location
|
|
os.makedirs(self._config['workdir'], exist_ok=True)
|
|
|
|
if 'scripts_dir' not in self._config:
|
|
self._config['scripts_dir'] = os.path.join(os.path.dirname(cfgfile), 'scripts')
|
|
os.makedirs(self._config['scripts_dir'], mode=0o755, exist_ok=True)
|
|
|
|
init_py = os.path.join(self._config['scripts_dir'], '__init__.py')
|
|
if not os.path.isfile(init_py):
|
|
with open(init_py, 'w') as f:
|
|
f.write('# Auto-generated __init__.py - do not remove\n')
|
|
|
|
# Include scripts_dir parent in sys.path so members can be imported in scripts
|
|
# through the `scripts` package
|
|
scripts_parent_dir = str(pathlib.Path(self._config['scripts_dir']).absolute().parent)
|
|
sys.path = [scripts_parent_dir] + sys.path
|
|
|
|
self._config['db'] = self._config.get('main.db', {
|
|
'engine': 'sqlite:///' + os.path.join(
|
|
os.path.expanduser('~'), '.local', 'share', 'platypush', 'main.db')
|
|
})
|
|
|
|
logging_config = {
|
|
'level': logging.INFO,
|
|
'stream': sys.stdout,
|
|
'format': '%(asctime)-15s|%(levelname)5s|%(name)s|%(message)s',
|
|
}
|
|
|
|
if 'logging' in self._config:
|
|
for (k, v) in self._config['logging'].items():
|
|
if k == 'filename':
|
|
logfile = os.path.expanduser(v)
|
|
logdir = os.path.dirname(logfile)
|
|
try:
|
|
os.makedirs(logdir, exist_ok=True)
|
|
except Exception as e:
|
|
print('Unable to create logs directory {}: {}'.format(
|
|
logdir, str(e)))
|
|
|
|
v = logfile
|
|
del logging_config['stream']
|
|
elif k == 'level':
|
|
try:
|
|
v = int(v)
|
|
except ValueError:
|
|
v = getattr(logging, v.upper())
|
|
|
|
logging_config[k] = v
|
|
|
|
self._config['logging'] = logging_config
|
|
|
|
if 'device_id' not in self._config:
|
|
self._config['device_id'] = socket.gethostname()
|
|
|
|
if 'environment' in self._config:
|
|
for k, v in self._config['environment'].items():
|
|
os.environ[k] = str(v)
|
|
|
|
self.backends = {}
|
|
self.plugins = {}
|
|
self.event_hooks = {}
|
|
self.procedures = {}
|
|
self.constants = {}
|
|
self.cronjobs = {}
|
|
|
|
self._init_constants()
|
|
self._load_scripts()
|
|
self._init_components()
|
|
|
|
@staticmethod
|
|
def _is_special_token(token):
|
|
return token == 'main.db' or \
|
|
token == 'token' or \
|
|
token == 'token_hash' or \
|
|
token == 'logging' or \
|
|
token == 'workdir' or \
|
|
token == 'device_id' or \
|
|
token == 'environment'
|
|
|
|
def _read_config_file(self, cfgfile):
|
|
cfgfile_dir = os.path.dirname(os.path.abspath(
|
|
os.path.expanduser(cfgfile)))
|
|
|
|
config = {}
|
|
|
|
with open(cfgfile, 'r') as fp:
|
|
file_config = yaml.safe_load(fp)
|
|
|
|
if not file_config:
|
|
return config
|
|
|
|
for section in file_config:
|
|
if section == 'include':
|
|
include_files = file_config[section] \
|
|
if isinstance(file_config[section], list) \
|
|
else [file_config[section]]
|
|
|
|
for include_file in include_files:
|
|
if not os.path.isabs(include_file):
|
|
include_file = os.path.join(cfgfile_dir, include_file)
|
|
self._included_files.add(include_file)
|
|
|
|
included_config = self._read_config_file(include_file)
|
|
for incl_section in included_config.keys():
|
|
config[incl_section] = included_config[incl_section]
|
|
elif section == 'scripts_dir':
|
|
assert isinstance(file_config[section], str)
|
|
config['scripts_dir'] = os.path.abspath(os.path.expanduser(file_config[section]))
|
|
elif 'disabled' not in file_config[section] \
|
|
or file_config[section]['disabled'] is False:
|
|
config[section] = file_config[section]
|
|
|
|
return config
|
|
|
|
def _load_module(self, modname: str, prefix: Optional[str] = None):
|
|
try:
|
|
module = importlib.import_module(modname)
|
|
except Exception as e:
|
|
print('Unhandled exception while importing module {}: {}'.format(modname, str(e)))
|
|
return
|
|
|
|
prefix = modname + '.' if prefix is None else prefix
|
|
self.procedures.update(**{
|
|
prefix + name: obj
|
|
for name, obj in inspect.getmembers(module)
|
|
if is_functional_procedure(obj)
|
|
})
|
|
|
|
self.event_hooks.update(**{
|
|
prefix + name: obj
|
|
for name, obj in inspect.getmembers(module)
|
|
if is_functional_hook(obj)
|
|
})
|
|
|
|
def _load_scripts(self):
|
|
scripts_dir = self._config['scripts_dir']
|
|
sys_path = sys.path.copy()
|
|
sys.path = [scripts_dir] + sys.path
|
|
scripts_modname = os.path.basename(scripts_dir)
|
|
self._load_module(scripts_modname, prefix='')
|
|
|
|
for _, modname, _ in pkgutil.walk_packages(path=[scripts_dir], onerror=lambda x: None):
|
|
self._load_module(modname)
|
|
|
|
sys.path = sys_path
|
|
|
|
def _init_components(self):
|
|
for key in self._config.keys():
|
|
if key.startswith('backend.'):
|
|
backend_name = '.'.join(key.split('.')[1:])
|
|
self.backends[backend_name] = self._config[key]
|
|
elif key.startswith('event.hook.'):
|
|
hook_name = '.'.join(key.split('.')[2:])
|
|
self.event_hooks[hook_name] = self._config[key]
|
|
elif key.startswith('cron.'):
|
|
cron_name = '.'.join(key.split('.')[1:])
|
|
self.cronjobs[cron_name] = self._config[key]
|
|
elif key.startswith('procedure.'):
|
|
tokens = key.split('.')
|
|
_async = True if len(tokens) > 2 and tokens[1] == 'async' else False
|
|
procedure_name = '.'.join(tokens[2:] if len(tokens) > 2 else tokens[1:])
|
|
args = []
|
|
m = re.match(r'^([^(]+)\(([^)]+)\)\s*', procedure_name)
|
|
|
|
if m:
|
|
procedure_name = m.group(1).strip()
|
|
args = [
|
|
arg.strip()
|
|
for arg in m.group(2).strip().split(',')
|
|
if arg.strip()
|
|
]
|
|
|
|
self.procedures[procedure_name] = {
|
|
'_async': _async,
|
|
'actions': self._config[key],
|
|
'args': args,
|
|
}
|
|
elif not self._is_special_token(key):
|
|
self.plugins[key] = self._config[key]
|
|
|
|
def _init_constants(self):
|
|
if 'constants' in self._config:
|
|
self.constants = self._config['constants']
|
|
|
|
for (key, value) in self._default_constants.items():
|
|
self.constants[key] = value
|
|
|
|
@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_event_hooks():
|
|
global _default_config_instance
|
|
if _default_config_instance is None:
|
|
_default_config_instance = Config()
|
|
return _default_config_instance.event_hooks
|
|
|
|
@staticmethod
|
|
def get_procedures():
|
|
global _default_config_instance
|
|
if _default_config_instance is None:
|
|
_default_config_instance = Config()
|
|
return _default_config_instance.procedures
|
|
|
|
@staticmethod
|
|
def get_constants():
|
|
global _default_config_instance
|
|
if _default_config_instance is None:
|
|
_default_config_instance = Config()
|
|
constants = {}
|
|
|
|
for name in _default_config_instance.constants.keys():
|
|
constants[name] = Config.get_constant(name)
|
|
return constants
|
|
|
|
@staticmethod
|
|
def get_constant(name):
|
|
global _default_config_instance
|
|
if _default_config_instance is None:
|
|
_default_config_instance = Config()
|
|
|
|
if name not in _default_config_instance.constants:
|
|
return None
|
|
value = _default_config_instance.constants[name]
|
|
return value() if callable(value) else value
|
|
|
|
@staticmethod
|
|
def get_cronjobs():
|
|
global _default_config_instance
|
|
if _default_config_instance is None:
|
|
_default_config_instance = Config()
|
|
return _default_config_instance.cronjobs
|
|
|
|
@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.get(key)
|
|
|
|
# vim:sw=4:ts=4:et:
|