Merge pull request #1 from sprockets/v1

V1
This commit is contained in:
Gavin M. Roy 2014-08-29 09:40:29 -04:00
commit 08239ee6b7
8 changed files with 342 additions and 17 deletions

2
.gitignore vendored
View file

@ -1,5 +1,5 @@
.DS_Store
.pyc
build
dist
*.pyc
*.egg-info

View file

@ -1,2 +1,2 @@
LICENSE
README.md
include LICENSE
include README.rst

View file

@ -1,5 +0,0 @@
Sprockets
=========
A loosely coupled framework built on top of Tornado. Take what you need to build
awesome applications.

45
README.rst Normal file
View file

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

View file

@ -1,3 +1,4 @@
argparse
importlib
logutils
unittest2

View file

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

20
sprockets/__init__.py Normal file
View file

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

265
sprockets/cli.py Normal file
View file

@ -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 <PID %(process)d:%(processName)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()