mirror of
https://git.zavage.net/Zavage-Software/wikicrawl.git
synced 2024-11-25 01:19:19 -07:00
203 lines
6.5 KiB
Python
203 lines
6.5 KiB
Python
|
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/<user>/.config/<app>/config.ini
|
||
|
On Windows, this will be C:\\Users\\<user>\\AppData\\Local\\<app>\\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
|
||
|
|