Refactored `Config.__init__`.

The constructor of the `Config` class had grown too big. It's much more
manageable if split into multiple sub-constructor helpers.
This commit is contained in:
Fabio Manganiello 2023-07-15 01:37:49 +02:00
parent 0a3d6add83
commit c846c61493
Signed by: blacklight
GPG Key ID: D90FBA7F76362774
3 changed files with 122 additions and 106 deletions

View File

@ -1,7 +1,7 @@
""" """
Platypush Platypush
.. moduleauthor:: Fabio Manganiello <blacklight86@gmail.com> .. moduleauthor:: Fabio Manganiello <fabio@manganiello.tech>
.. license: MIT .. license: MIT
""" """
@ -11,6 +11,7 @@ import os
import sys import sys
from typing import Optional from typing import Optional
from .bus import Bus
from .bus.redis import RedisBus from .bus.redis import RedisBus
from .config import Config from .config import Config
from .context import register_backends, register_plugins from .context import register_backends, register_plugins
@ -33,20 +34,9 @@ log = logging.getLogger('platypush')
class Daemon: class Daemon:
"""Main class for the Platypush daemon""" """Main class for the Platypush daemon"""
# Configuration file (default: either ~/.config/platypush/config.yaml or
# /etc/platypush/config.yaml
config_file = None
# Application bus. It's an internal queue where:
# - backends will post the messages they receive
# - plugins will post the responses they process
bus = None
# Default bus queue name # Default bus queue name
_default_redis_queue = 'platypush/bus' _default_redis_queue = 'platypush/bus'
pidfile = None
# backend_name => backend_obj map # backend_name => backend_obj map
backends = None backends = None
@ -80,25 +70,16 @@ class Daemon:
verbose -- Enable debug/verbose logging, overriding the stored configuration (default: False). verbose -- Enable debug/verbose logging, overriding the stored configuration (default: False).
""" """
self.pidfile = pidfile
if pidfile: if pidfile:
self.pidfile = pidfile with open(pidfile, 'w') as f:
with open(self.pidfile, 'w') as f:
f.write(str(os.getpid())) f.write(str(os.getpid()))
self.bus: Optional[Bus] = None
self.redis_queue = redis_queue or self._default_redis_queue self.redis_queue = redis_queue or self._default_redis_queue
self.config_file = config_file self.config_file = config_file
self._verbose = verbose
Config.init(self.config_file) Config.init(self.config_file)
logging_conf = Config.get('logging') or {}
if verbose:
logging_conf['level'] = logging.DEBUG
logging.basicConfig(**logging_conf)
redis_conf = Config.get('backend.redis') or {}
self.bus = RedisBus(
redis_queue=self.redis_queue,
on_message=self.on_message(),
**redis_conf.get('redis_args', {})
)
self.no_capture_stdout = no_capture_stdout self.no_capture_stdout = no_capture_stdout
self.no_capture_stderr = no_capture_stderr self.no_capture_stderr = no_capture_stderr
@ -108,6 +89,23 @@ class Daemon:
self.processed_requests = 0 self.processed_requests = 0
self.cron_scheduler = None self.cron_scheduler = None
self._init_bus()
self._init_logging()
def _init_bus(self):
redis_conf = Config.get('backend.redis') or {}
self.bus = RedisBus(
redis_queue=self.redis_queue,
on_message=self.on_message(),
**redis_conf.get('redis_args', {})
)
def _init_logging(self):
logging_conf = Config.get('logging') or {}
if self._verbose:
logging_conf['level'] = logging.DEBUG
logging.basicConfig(**logging_conf)
@classmethod @classmethod
def build_from_cmdline(cls, args): def build_from_cmdline(cls, args):
""" """
@ -122,7 +120,7 @@ class Daemon:
dest='config', dest='config',
required=False, required=False,
default=None, default=None,
help=cls.config_file.__doc__, help='Custom location for the configuration file',
) )
parser.add_argument( parser.add_argument(
'--version', '--version',
@ -207,7 +205,7 @@ class Daemon:
try: try:
msg.execute(n_tries=self.n_tries) msg.execute(n_tries=self.n_tries)
except PermissionError: except PermissionError:
log.info('Dropped unauthorized request: {}'.format(msg)) log.info('Dropped unauthorized request: %s', msg)
self.processed_requests += 1 self.processed_requests += 1
if ( if (
@ -255,7 +253,7 @@ class Daemon:
sys.stderr = Logger(log.warning) sys.stderr = Logger(log.warning)
set_thread_name('platypush') set_thread_name('platypush')
log.info('---- Starting platypush v.{}'.format(__version__)) log.info('---- Starting platypush v.%s', __version__)
# Initialize the backends and link them to the bus # Initialize the backends and link them to the bus
self.backends = register_backends(bus=self.bus, global_scope=True) self.backends = register_backends(bus=self.bus, global_scope=True)

View File

@ -58,72 +58,51 @@ class Config:
) )
_included_files: Set[str] = set() _included_files: Set[str] = set()
def __init__(self, cfgfile=None): def __init__(self, cfgfile: Optional[str] = None):
""" """
Constructor. Always use the class as a singleton (i.e. through Constructor. Always use the class as a singleton (i.e. through
Config.init), you won't probably need to call the constructor directly Config.init), you won't probably need to call the constructor directly
Params:
cfgfile -- Config file path (default: retrieve the first :param cfgfile: Config file path (default: retrieve the first available
available location in _cfgfile_locations) location in _cfgfile_locations).
""" """
self.backends = {}
self.plugins = self._core_plugins
self.event_hooks = {}
self.procedures = {}
self.constants = {}
self.cronjobs = {}
self.dashboards = {}
self._plugin_manifests = {}
self._backend_manifests = {}
self._cfgfile = ''
self._init_cfgfile(cfgfile)
self._config = self._read_config_file(self._cfgfile)
self._init_secrets()
self._init_dirs()
self._init_db()
self._init_logging()
self._init_device_id()
self._init_environment()
self._init_manifests()
self._init_constants()
self._load_scripts()
self._init_components()
self._init_dashboards(self._config['dashboards_dir'])
def _init_cfgfile(self, cfgfile: Optional[str] = None):
if cfgfile is None: if cfgfile is None:
cfgfile = self._get_default_cfgfile() cfgfile = self._get_default_cfgfile()
if cfgfile is None: if cfgfile is None:
cfgfile = self._create_default_config() cfgfile = self._create_default_config()
cfgfile = self._cfgfile = os.path.abspath(os.path.expanduser(cfgfile)) 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_hash'] = get_hash(self._config['token'])
if 'workdir' not in self._config:
self._config['workdir'] = self._workdir_location
self._config['workdir'] = os.path.expanduser(self._config['workdir'])
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)
if 'dashboards_dir' not in self._config:
self._config['dashboards_dir'] = os.path.join(
os.path.dirname(cfgfile), 'dashboards'
)
os.makedirs(self._config['dashboards_dir'], mode=0o755, exist_ok=True)
# Create a default (empty) __init__.py in the scripts folder
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
# Initialize the default db connection string
db_engine = self._config.get('main.db', '')
if db_engine:
if isinstance(db_engine, str):
db_engine = {
'engine': db_engine,
}
else:
db_engine = {
'engine': 'sqlite:///'
+ os.path.join(quote(self._config['workdir']), 'main.db')
}
self._config['db'] = db_engine
def _init_logging(self):
logging_config = { logging_config = {
'level': logging.INFO, 'level': logging.INFO,
'stream': sys.stdout, 'stream': sys.stdout,
@ -152,28 +131,65 @@ class Config:
self._config['logging'] = logging_config self._config['logging'] = logging_config
def _init_db(self):
# Initialize the default db connection string
db_engine = self._config.get('main.db', '')
if db_engine:
if isinstance(db_engine, str):
db_engine = {
'engine': db_engine,
}
else:
db_engine = {
'engine': 'sqlite:///'
+ os.path.join(quote(self._config['workdir']), 'main.db')
}
self._config['db'] = db_engine
def _init_device_id(self):
if 'device_id' not in self._config: if 'device_id' not in self._config:
self._config['device_id'] = socket.gethostname() self._config['device_id'] = socket.gethostname()
def _init_environment(self):
if 'environment' in self._config: if 'environment' in self._config:
for k, v in self._config['environment'].items(): for k, v in self._config['environment'].items():
os.environ[k] = str(v) os.environ[k] = str(v)
self.backends = {} def _init_dirs(self):
self.plugins = self._core_plugins if 'workdir' not in self._config:
self.event_hooks = {} self._config['workdir'] = self._workdir_location
self.procedures = {} self._config['workdir'] = os.path.expanduser(self._config['workdir'])
self.constants = {} os.makedirs(self._config['workdir'], exist_ok=True)
self.cronjobs = {}
self.dashboards = {}
self._plugin_manifests = {}
self._backend_manifests = {}
self._init_manifests() if 'scripts_dir' not in self._config:
self._init_constants() self._config['scripts_dir'] = os.path.join(
self._load_scripts() os.path.dirname(self._cfgfile), 'scripts'
self._init_components() )
self._init_dashboards(self._config['dashboards_dir']) os.makedirs(self._config['scripts_dir'], mode=0o755, exist_ok=True)
if 'dashboards_dir' not in self._config:
self._config['dashboards_dir'] = os.path.join(
os.path.dirname(self._cfgfile), 'dashboards'
)
os.makedirs(self._config['dashboards_dir'], mode=0o755, exist_ok=True)
# Create a default (empty) __init__.py in the scripts folder
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
def _init_secrets(self):
if 'token' in self._config:
self._config['token_hash'] = get_hash(self._config['token'])
@property @property
def _core_plugins(self) -> Dict[str, dict]: def _core_plugins(self) -> Dict[str, dict]:

View File

@ -11,7 +11,7 @@ import time
from typing import Union from typing import Union
from uuid import UUID from uuid import UUID
logger = logging.getLogger('platypush') _logger = logging.getLogger('platypush')
class JSONAble(ABC): class JSONAble(ABC):
@ -88,31 +88,33 @@ class Message:
try: try:
return super().default(obj) return super().default(obj)
except Exception as e: except Exception as e:
logger.warning( _logger.warning(
'Could not serialize object type %s: %s: %s', type(obj), e, obj 'Could not serialize object type %s: %s: %s', type(obj), e, obj
) )
def __init__(self, *_, timestamp=None, logging_level=logging.INFO, **__): def __init__(self, *_, timestamp=None, logging_level=logging.INFO, **__):
self.timestamp = timestamp or time.time() self.timestamp = timestamp or time.time()
self.logging_level = logging_level self.logging_level = logging_level
self._logger = _logger
self._default_log_prefix = ''
def log(self, prefix=''): def log(self, prefix=''):
if self.logging_level is None: if self.logging_level is None:
return # Skip logging return # Skip logging
log_func = logger.info log_func = self._logger.info
if self.logging_level == logging.DEBUG: if self.logging_level == logging.DEBUG:
log_func = logger.debug log_func = self._logger.debug
elif self.logging_level == logging.WARNING: elif self.logging_level == logging.WARNING:
log_func = logger.warning log_func = self._logger.warning
elif self.logging_level == logging.ERROR: elif self.logging_level == logging.ERROR:
log_func = logger.error log_func = self._logger.error
elif self.logging_level == logging.FATAL: elif self.logging_level == logging.FATAL:
log_func = logger.fatal log_func = self._logger.fatal
if not prefix: if not prefix:
prefix = f'Received {self.__class__.__name__}: ' prefix = self._default_log_prefix
log_func(f'{prefix}{self}') log_func('%s%s', prefix, self)
def __str__(self): def __str__(self):
""" """
@ -154,7 +156,7 @@ class Message:
try: try:
msg = json.loads(msg.strip()) msg = json.loads(msg.strip())
except (ValueError, TypeError): except (ValueError, TypeError):
logger.warning('Invalid JSON message: %s', msg) _logger.warning('Invalid JSON message: %s', msg)
assert isinstance(msg, dict) assert isinstance(msg, dict)