diff --git a/.gitignore b/.gitignore index 1d0ef72..ca42c40 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ .DS_Store -.pyc build dist +*.pyc *.egg-info diff --git a/MANIFEST.in b/MANIFEST.in index f0f644e..9d5d250 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,2 @@ -LICENSE -README.md +include LICENSE +include README.rst diff --git a/README.md b/README.md deleted file mode 100644 index e56be88..0000000 --- a/README.md +++ /dev/null @@ -1,5 +0,0 @@ -Sprockets -========= -A loosely coupled framework built on top of Tornado. Take what you need to build -awesome applications. - diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..9028d82 --- /dev/null +++ b/README.rst @@ -0,0 +1,45 @@ +Sprockets +========= +A loosely coupled framework built on top of Tornado. Take what you need to build +awesome applications. + +The core `sprockets` packages offers only the command line application for +invoking Sprockets controllers such as the HTTP and AMQP controllers. + +|Version| |Downloads| |Status| |Coverage| |License| + +CLI Usage +--------- + + usage: sprockets [-h] [-l] [-d] [-s] [-v] [--version] + {http,amqp} ... application + + positional arguments: + {http,amqp} Available sprockets application controllers + http HTTP Application Controller + amqp RabbitMQ Worker Controller + application The sprockets app to run + + optional arguments: + -h, --help show this help message and exit + -l, --list List installed sprockets apps + -s, --syslog Log to syslog + -v, --verbose Verbose logging output, use -vv for DEBUG level logging + --version show program's version number and exit + + + +.. |Version| image:: https://badge.fury.io/py/sprockets.svg? + :target: http://badge.fury.io/py/sprockets + +.. |Status| image:: https://travis-ci.org/sprockets/sprockets.svg?branch=master + :target: https://travis-ci.org/sprockets/sprockets + +.. |Coverage| image:: https://coveralls.io/repos/sprockets/sprockets/badge.png + :target: https://coveralls.io/r/sprockets/sprockets + +.. |Downloads| image:: https://pypip.in/d/sprockets/badge.svg? + :target: https://pypi.python.org/pypi/sprockets + +.. |License| image:: https://pypip.in/license/sprockets/badge.svg? + :target: https://sprockets.readthedocs.org diff --git a/requirements-2.6.txt b/requirements-2.6.txt index 09dae77..52981dd 100644 --- a/requirements-2.6.txt +++ b/requirements-2.6.txt @@ -1,3 +1,4 @@ argparse +importlib logutils unittest2 diff --git a/setup.py b/setup.py index 6898445..a7cce89 100644 --- a/setup.py +++ b/setup.py @@ -1,29 +1,28 @@ from setuptools import setup -import os -import platform +import sys -requirements = ['tornado'] +requirements = [] tests_require = ['coverage', 'coveralls', 'mock', 'nose'] # Requirements for Python 2.6 -(major, minor, rev) = platform.python_version_tuple() -if float('%s.%s' % (major, minor)) < 2.7: +version = sys.version_info +if (version.major, version.minor) < (2, 7): requirements.append('argparse') + requirements.append('importlib') requirements.append('logutils') tests_require.append('unittest2') - setup(name='sprockets', version='0.1.0', description=('A modular, loosely coupled micro-framework built on top ' 'of Tornado simplifying the creation of web applications ' 'and RabbitMQ workers'), - entry_points={'console_scripts': ['sprockets=sprockets:main']}, + entry_points={'console_scripts': ['sprockets=sprockets.cli:main']}, author='AWeber Communications', url='https://github.com/sprockets/sprockets', install_requires=requirements, license=open('LICENSE').read(), - package_data={'': ['LICENSE', 'README.md']}, + package_data={'': ['LICENSE', 'README.rst']}, packages=['sprockets'], classifiers=['Development Status :: 3 - Alpha', 'Environment :: No Input/Output (Daemon)', @@ -46,4 +45,4 @@ setup(name='sprockets', 'Topic :: Software Development :: Libraries :: Python Modules'], test_suite='nose.collector', tests_require=tests_require, - zip_safe=True) + zip_safe=False) diff --git a/sprockets/__init__.py b/sprockets/__init__.py new file mode 100644 index 0000000..9c57ee9 --- /dev/null +++ b/sprockets/__init__.py @@ -0,0 +1,20 @@ +""" +Sprockets +========= +A loosely coupled framework built on top of Tornado. Take what you need to +build awesome applications. + +""" +version_info = (0, 0, 0) +__version__ = '.'.join(str(v) for v in version_info) + +import logging + +# Ensure there is a NullHandler for logging +try: + from logging import NullHandler +except ImportError: + # Not available in Python 2.6 + class NullHandler(logging.Handler): + def emit(self, record): + pass diff --git a/sprockets/cli.py b/sprockets/cli.py new file mode 100644 index 0000000..f6e2818 --- /dev/null +++ b/sprockets/cli.py @@ -0,0 +1,265 @@ +""" +Sprockets CLI +============= +The sprockets CLI interface for running applications. Applications are meant +to be run by a controller that is managed by the sprockets CLI interface. + +The sprockets CLI interface loads controller applications that are registered +using setuptools entry points. + +Each controller is expected to expose at least a `main(application, args)` +method that would be invoked when starting the application. Additional, a +controller can implement a `add_cli_arguments(parser)` method that will be +invoked when setting up the command line parameters. This allows controllers +to inject configuration directives into the cli. + +Applications can be a python package or module and if they are registered +to a specific controller, can be referenced by an alias. + +""" +import argparse +import importlib +import logging +import string +import sys + +# import logutils for Python 2.6 or logging.config for later versions +sys_version = sys.version_info +if (sys_version.major, sys_version.minor) < (2, 7): + import logutils.dictconfig as logging_config +else: + from logging import config as logging_config + +import pkg_resources + +from sprockets import __version__ + +DESCRIPTION = 'Available sprockets application controllers' + +# Logging formatters +SYSLOG_FORMAT = ('%(levelname)s ' + '%(name)s.%(funcName)s(): %(message)s') + +VERBOSE_FORMAT = ('%(levelname) -10s %(asctime)s %(process)-6d ' + '%(processName) -20s %(name) -20s ' + '%(funcName) -20s L%(lineno)-6d: %(message)s') + +# Base logging configuration +LOGGING = {'disable_existing_loggers': True, + 'filters': {}, + 'formatters': {'syslog': {'format': SYSLOG_FORMAT}, + 'verbose': {'datefmt': '%Y-%m-%d %H:%M:%S', + 'format': VERBOSE_FORMAT}}, + 'handlers': {'console': {'class': 'logging.StreamHandler', + 'formatter': 'verbose'}, + 'syslog': {'class': 'logging.handlers.SysLogHandler', + 'formatter': 'syslog'}}, + 'incremental': False, + 'loggers': {'sprockets': {'handlers': ['console'], + 'level': logging.WARNING, + 'propagate': True}}, + 'root': {'handlers': [], + 'level': logging.CRITICAL, + 'propagate': True}, + 'version': 1} + +LOGGER = logging.getLogger(__name__) + + +class CLI(object): + """The core Sprockets CLI application providing argument parsing and + logic for starting a controller. + + The package or module for an application is passed into + + """ + + CONTROLLERS = 'sprockets.controller' + + def __init__(self): + self._controllers = self._get_controllers() + self._arg_parser = argparse.ArgumentParser() + self._add_cli_args() + self._args = self._arg_parser.parse_args() + + def run(self): + """Evaluate the command line arguments, performing the appropriate + actions so the application can be started. + + """ + # The list command prevents any other processing of args + if self._args.list: + self._print_installed_apps(self._args.controller) + sys.exit(0) + + # If app is not specified at this point, raise an error + if not self._args.application: + sys.stderr.write('\nerror: application not specified\n\n') + self._arg_parser.print_help() + sys.exit(-1) + + # If it's a registered app reference by name, get the module name + app_module = self._get_application_module(self._args.controller, + self._args.application) + + # Configure logging based upon the flags + self._configure_logging(app_module, + self._args.verbose, + self._args.syslog) + + # Try and run the controller + try: + self._controllers[self._args.controller].main(app_module, + self._args) + except TypeError as error: + sys.stderr.write('error: could not start the %s controller for %s' + ': %s\n\n' % (self._args.controller, + app_module, + str(error))) + sys.exit(-1) + + def _add_cli_args(self): + """Add the cli arguments to the argument parser.""" + + # Optional cli arguments + self._arg_parser.add_argument('-l', '--list', + action='store_true', + help='List installed sprockets apps') + + self._arg_parser.add_argument('-s', '--syslog', + action='store_true', + help='Log to syslog') + + self._arg_parser.add_argument('-v', '--verbose', + action='count', + help=('Verbose logging output, use -vv ' + 'for DEBUG level logging')) + + self._arg_parser.add_argument('--version', + action='version', + version='sprockets v%s ' % __version__) + + # Controller sub-parser + subparsers = self._arg_parser.add_subparsers(dest='controller', + help=DESCRIPTION) + + # Iterate through the controllers and add their cli arguments + for key in self._controllers: + help_text = self._get_controller_help(key) + sub_parser = subparsers.add_parser(key, help=help_text) + try: + self._controllers[key].add_cli_arguments(sub_parser) + except AttributeError: + LOGGER.debug('%s missing add_cli_arguments()', key) + + # The application argument + self._arg_parser.add_argument('application', + action="store", + help='The sprockets app to run') + + @staticmethod + def _configure_logging(application, verbosity=0, syslog=False): + """Configure logging for the application, setting the appropriate + verbosity and adding syslog if it's enabled. + + :param str application: The application module/package name + :param int verbosity: 1 == INFO, 2 == DEBUG + :param bool syslog: Enable the syslog handler + + """ + # Create a new copy of the logging config that will be modified + config = dict(LOGGING) + + # Increase the logging verbosity + if verbosity == 1: + config['loggers']['sprockets']['level'] = logging.INFO + elif verbosity == 2: + config['loggers']['sprockets']['level'] = logging.DEBUG + + # Add syslog if it's enabled + if syslog: + config['loggers']['sprockets']['handlers'].append('syslog') + + # Copy the sprockets logger to the application + config['loggers'][application] = dict(config['loggers']['sprockets']) + + # Configure logging + logging_config.dictConfig(config) + + def _get_application_module(self, controller, application): + """Return the module for an application. If it's a entry-point + registered application name, return the module name from the entry + points data. If not, the passed in application name is returned. + + :param str controller: The controller type + :param str application: The application name or module + :rtype: str + + """ + for pkg in self._get_applications(controller): + if pkg.name == application: + return pkg.module_name + return application + + @staticmethod + def _get_applications(controller): + """Return a list of application names for the given controller type + that have registered themselves as sprockets applications. + + :param str controller: The type of controller for the applications + :rtype: list + + """ + group_name = 'sprockets.%s.app' % controller + return pkg_resources.iter_entry_points(group=group_name) + + @staticmethod + def _get_argument_parser(): + """Return an instance of the argument parser. + + :return: argparse.ArgumentParser + + """ + return argparse.ArgumentParser() + + def _get_controllers(self): + """Iterate through the installed controller entry points and import + the module and assign the handle to the CLI._controllers dict. + + :return: dict + + """ + controllers = dict() + for pkg in pkg_resources.iter_entry_points(group=self.CONTROLLERS): + LOGGER.debug('Loading %s controller', pkg.name) + controllers[pkg.name] = importlib.import_module(pkg.module_name) + return controllers + + def _get_controller_help(self, controller): + """Return the value of the HELP attribute for a controller that should + describe the functionality of the controller. + + :rtype: str|None + + """ + if hasattr(self._controllers[controller], 'HELP'): + return self._controllers[controller].HELP + return None + + def _print_installed_apps(self, controller): + """Print out a list of installed sprockets applications + + :param str controller: The name of the controller to get apps for + """ + print('\nInstalled Sprockets %s Apps\n' % controller.upper()) + print("{0:<25} {1:>25}".format('Name', 'Module')) + print(string.ljust('', 51, '-')) + for app in self._get_applications(controller): + print('{0:<25} {1:>25}'.format(app.name, '(%s)' % app.module_name)) + print('') + + +def main(): + """Main application runner""" + cli = CLI() + cli.run()