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
.. moduleauthor:: Fabio Manganiello <blacklight86@gmail.com>
.. moduleauthor:: Fabio Manganiello <fabio@manganiello.tech>
.. 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)

View File

@ -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]:

View File

@ -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)