From c846c61493b4f3eb21a8e6f4cc9bfc877beeb74b Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Sat, 15 Jul 2023 01:37:49 +0200 Subject: [PATCH] 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. --- platypush/__init__.py | 54 ++++++------ platypush/config/__init__.py | 152 +++++++++++++++++++--------------- platypush/message/__init__.py | 22 ++--- 3 files changed, 122 insertions(+), 106 deletions(-) diff --git a/platypush/__init__.py b/platypush/__init__.py index 12824f270..445e1e749 100644 --- a/platypush/__init__.py +++ b/platypush/__init__.py @@ -1,7 +1,7 @@ """ Platypush -.. moduleauthor:: Fabio Manganiello +.. moduleauthor:: Fabio Manganiello .. license: MIT """ @@ -11,6 +11,7 @@ import os import sys from typing import Optional +from .bus import Bus from .bus.redis import RedisBus from .config import Config from .context import register_backends, register_plugins @@ -33,20 +34,9 @@ log = logging.getLogger('platypush') class 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_redis_queue = 'platypush/bus' - pidfile = None - # backend_name => backend_obj map backends = None @@ -80,25 +70,16 @@ class Daemon: verbose -- Enable debug/verbose logging, overriding the stored configuration (default: False). """ + self.pidfile = pidfile if pidfile: - self.pidfile = pidfile - with open(self.pidfile, 'w') as f: + with open(pidfile, 'w') as f: f.write(str(os.getpid())) + self.bus: Optional[Bus] = None self.redis_queue = redis_queue or self._default_redis_queue self.config_file = config_file + self._verbose = verbose 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_stderr = no_capture_stderr @@ -108,6 +89,23 @@ class Daemon: self.processed_requests = 0 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 def build_from_cmdline(cls, args): """ @@ -122,7 +120,7 @@ class Daemon: dest='config', required=False, default=None, - help=cls.config_file.__doc__, + help='Custom location for the configuration file', ) parser.add_argument( '--version', @@ -207,7 +205,7 @@ class Daemon: try: msg.execute(n_tries=self.n_tries) except PermissionError: - log.info('Dropped unauthorized request: {}'.format(msg)) + log.info('Dropped unauthorized request: %s', msg) self.processed_requests += 1 if ( @@ -255,7 +253,7 @@ class Daemon: sys.stderr = Logger(log.warning) 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 self.backends = register_backends(bus=self.bus, global_scope=True) diff --git a/platypush/config/__init__.py b/platypush/config/__init__.py index 3741ee21b..bff1d3d66 100644 --- a/platypush/config/__init__.py +++ b/platypush/config/__init__.py @@ -58,72 +58,51 @@ class Config: ) _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 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) + + :param cfgfile: Config file path (default: retrieve the first available + 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: cfgfile = self._get_default_cfgfile() if cfgfile is None: cfgfile = self._create_default_config() - 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 + self._cfgfile = os.path.abspath(os.path.expanduser(cfgfile)) + def _init_logging(self): logging_config = { 'level': logging.INFO, 'stream': sys.stdout, @@ -152,28 +131,65 @@ class 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: self._config['device_id'] = socket.gethostname() + def _init_environment(self): if 'environment' in self._config: for k, v in self._config['environment'].items(): os.environ[k] = str(v) - self.backends = {} - self.plugins = self._core_plugins - self.event_hooks = {} - self.procedures = {} - self.constants = {} - self.cronjobs = {} - self.dashboards = {} - self._plugin_manifests = {} - self._backend_manifests = {} + def _init_dirs(self): + 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) - self._init_manifests() - self._init_constants() - self._load_scripts() - self._init_components() - self._init_dashboards(self._config['dashboards_dir']) + if 'scripts_dir' not in self._config: + self._config['scripts_dir'] = os.path.join( + os.path.dirname(self._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(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 def _core_plugins(self) -> Dict[str, dict]: diff --git a/platypush/message/__init__.py b/platypush/message/__init__.py index e08a75a5d..67c4efe69 100644 --- a/platypush/message/__init__.py +++ b/platypush/message/__init__.py @@ -11,7 +11,7 @@ import time from typing import Union from uuid import UUID -logger = logging.getLogger('platypush') +_logger = logging.getLogger('platypush') class JSONAble(ABC): @@ -88,31 +88,33 @@ class Message: try: return super().default(obj) except Exception as e: - logger.warning( + _logger.warning( 'Could not serialize object type %s: %s: %s', type(obj), e, obj ) def __init__(self, *_, timestamp=None, logging_level=logging.INFO, **__): self.timestamp = timestamp or time.time() self.logging_level = logging_level + self._logger = _logger + self._default_log_prefix = '' def log(self, prefix=''): if self.logging_level is None: return # Skip logging - log_func = logger.info + log_func = self._logger.info if self.logging_level == logging.DEBUG: - log_func = logger.debug + log_func = self._logger.debug elif self.logging_level == logging.WARNING: - log_func = logger.warning + log_func = self._logger.warning elif self.logging_level == logging.ERROR: - log_func = logger.error + log_func = self._logger.error elif self.logging_level == logging.FATAL: - log_func = logger.fatal + log_func = self._logger.fatal if not prefix: - prefix = f'Received {self.__class__.__name__}: ' - log_func(f'{prefix}{self}') + prefix = self._default_log_prefix + log_func('%s%s', prefix, self) def __str__(self): """ @@ -154,7 +156,7 @@ class Message: try: msg = json.loads(msg.strip()) except (ValueError, TypeError): - logger.warning('Invalid JSON message: %s', msg) + _logger.warning('Invalid JSON message: %s', msg) assert isinstance(msg, dict)