import appdirs import collections import functools import inspect import os import sys # Application scaffolding: from ._bootstrap import _bootstrap_logger from . import log from . import _util from . import cli from . import cfg DEFAULT_APP_NAME = 'python-app' DEFAULT_APP_AUTHOR = 'John Doe' # OPTIONAL: classes can sub-class from this? class Components: def inject_dependencies_based_on_names_in_args(self): pass def inject_dependency(self, name): pass def register_dependency(self, service, name): pass class ApplicationContext: """ Container for application-wide state; i.e. app configuration and loggers. """ def __init__(self, config, log): self.config = config self.log = log self.parsed_argv = None self.parsed_argv_unknown = None class ApplicationContainer: """ Generalized application functionality. Used for linking components and modules of the application together. Invokes runtime configuration reading from file, maintains the object instances for services, passes off to the cli to determine what to do, and then injects any necessary dependencies (e.g. database module) and kicks off the functionality requested in the cli. """ def __init__( self, configspec_filepath=None, config_filepath=None, *args, **kwargs ): # Instantiate root application context (container for globals) if configspec_filepath is None: configspec_filepath = self._get_configspec_filepath() self.appname = kwargs.get('appname') or DEFAULT_APP_NAME self.appauthor = kwargs.get('appauthor') or DEFAULT_APP_AUTHOR self._dependencies = {} config = cfg.Config(configspec_filepath) config.load_config_from_file(config_filepath) logger = log.LoggingLayer(self.appname, self.appauthor) # added here, is this okay to do twice? logger.configure_logging() self.ctx = ApplicationContext(config, logger) self['ctx'] = lambda: self.ctx self.cli = cli.CommandTree() # Command-line interface if callable(getattr(self, '_cli_options', None)): self._cli_options() if callable(getattr(self, '_services', None)): self._services() if callable(getattr(self, '_command_menu', None)): self._command_menu() def __delitem__(self, service_name): """ Deletes a service or dependency from the available dependencies. """ try: del self._dependencies[service_name] except KeyError as ex: pass def __getitem__(self, service_name): """ Returns a factory of a service or dependency. The factory is a function that is called to return an instance of the service object. app_container['netezza'] => returns the netezza service instance """ try: service_factory = self._dependencies[service_name] # Retrieve factory function return service_factory() # Call factory() to return instance of service except KeyError as ex: msg = 'failed to inject service: {}'.format(service_name) _bootstrap_logger.critical(msg) _util.eprint(msg) raise ServiceNotFound def __setitem__(self, service_name, value): """ Register a service or dependency factory to return a service. The factory function is called to return an instance of a service object. """ self._dependencies[service_name] = value def _construct_model(self, model_constructor, *args): """ Performs dependency resolution and instantiates an object of given type. This takes in the reference to a class constructor and a list of names of the dependencies that need passed into it, constructs that object and returns it. Models contain business logic and application functionality. Args: model_constructor: reference to object constructor. """ dependency_names = args dep_references = [] for dep_name in dependency_names: dep_references.append(self[dep_name]) return model_constructor(*dep_references) def _get_config_filepath(self, app_name, app_author, config_filename='config.ini'): """ Attempt to find config.ini in the user's config directory. On Linux, this will be /home//.config//config.ini On Windows, this will be C:\\Users\\\\AppData\\Local\\\\config.ini """ dirname = appdirs.user_config_dir(app_name, app_author) filepath = os.path.join(dirname, config_filename) _bootstrap_logger.info('default config filepath calculated to be: %s', filepath) return filepath def _get_configspec_filepath(self, configspec_filename='config.spec'): """ Attempt to find config.spec inside the installed package directory. """ return _util.get_root_asset(configspec_filename) def _inject_service_dependencies(self, constructor): """ Returns a function that, when called, constructs a new object for business/application logic with the listed dependencies. Args: constructor: service class to be created object. """ sig = inspect.signature(constructor.__init__) params = sig.parameters params = [params[paramname].name for paramname in params] # Convert Param() type => str cls_dependencies = params[1:] # Skip 'self' parameter on class methods. return functools.partial(self._construct_model, constructor, *cls_dependencies) def load_command(self): args, unk, success = self.cli.parse() if not success: return False self.ctx.parsed_argv = args self.ctx.parsed_argv_unknown = unk return True def invoke_command(self): rc = self.load_command() if not rc: return False try: self.cli.run_command() except NoCommandSpecified as ex: print('Failure: No command specified.') def interactive_shell(self): pass def invoke_from_cli(self): self.invoke_command() def usage(self): pass # Applications need a default usage class ServiceNotFound(Exception): """ Application framework error: unable to find and inject dependency. """ pass class NoCommandSpecified(Exception): pass