edbob/edbob/commands.py
2013-04-30 16:12:20 -07:00

564 lines
19 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# edbob -- Pythonic Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of edbob.
#
# edbob is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# edbob is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with edbob. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
``edbob.commands`` -- Console Commands
"""
from __future__ import absolute_import
import sys
import argparse
import subprocess
import logging
import platform
import edbob
from edbob.util import entry_point_map, requires_impl
class ArgumentParser(argparse.ArgumentParser):
"""
Customized version of ``argparse.ArgumentParser``, which overrides some of
the argument parsing logic. This is necessary for the application's
primary command (:class:`Command` class); but is not used with
:class:`Subcommand` derivatives.
"""
def parse_args(self, args=None, namespace=None):
args, argv = self.parse_known_args(args, namespace)
args.argv = argv
return args
class Command(edbob.Object):
"""
The primary command for the application.
You should subclass this within your own application if you wish to
leverage the command system provided by edbob.
"""
name = 'edbob'
version = edbob.__version__
description = "Pythonic Software Framework"
long_description = """
edbob is a Pythonic software framework.
Copyright (c) 2010-2012 Lance Edgar <lance@edbob.org>
This program comes with ABSOLUTELY NO WARRANTY. This is free software,
and you are welcome to redistribute it under certain conditions.
See the file COPYING.txt for more information.
"""
def __init__(self, **kwargs):
edbob.Object.__init__(self, **kwargs)
self.subcommands = entry_point_map('%s.commands' % self.name)
def __str__(self):
return str(self.name)
def iter_subcommands(self):
"""
Generator which yields associated :class:`Subcommand` classes, sorted
by :attr:`Subcommand.name`.
"""
for name in sorted(self.subcommands):
yield self.subcommands[name]
def print_help(self):
"""
Prints help text for the primary command, including a list of available
subcommands.
"""
print """%(description)s
Usage: %(name)s [options] <command> [command-options]
Options:
-c PATH, --config=PATH
Config path (may be specified more than once)
-n, --no-init Don't load config before executing command
-d, --debug Increase logging level to DEBUG
-P, --progress Show progress indicators (where relevant)
-v, --verbose Increase logging level to INFO
-V, --version Display program version and exit
Commands:""" % self
for cmd in self.iter_subcommands():
print " %-16s %s" % (cmd.name, cmd.description)
print """
Try '%(name)s help <command>' for more help.""" % self
def run(self, *args):
"""
Parses ``args`` and executes the appropriate subcommand action
accordingly (or displays help text).
"""
parser = ArgumentParser(
prog=self.name,
description=self.description,
add_help=False,
)
parser.add_argument('-c', '--config', action='append', dest='config_paths',
metavar='PATH')
parser.add_argument('-d', '--debug', action='store_true', dest='debug')
parser.add_argument('-n', '--no-init', action='store_true', default=False)
parser.add_argument('-P', '--progress', action='store_true', default=False)
parser.add_argument('-v', '--verbose', action='store_true', dest='verbose')
parser.add_argument('-V', '--version', action='version',
version="%%(prog)s %s" % self.version)
parser.add_argument('command', nargs='*')
# Parse args and determind subcommand.
args = parser.parse_args(list(args))
if not args or not args.command:
self.print_help()
return
# Show (sub)command help if so instructed, or unknown subcommand.
cmd = args.command.pop(0)
if cmd == 'help':
if len(args.command) != 1:
self.print_help()
return
cmd = args.command[0]
if cmd not in self.subcommands:
self.print_help()
return
cmd = self.subcommands[cmd](parent=self)
cmd.parser.print_help()
return
elif cmd not in self.subcommands:
self.print_help()
return
# Basic logging should be established before init()ing.
# Use root logger if setting logging flags.
log = logging.getLogger()
edbob.basic_logging()
if args.verbose:
log.setLevel(logging.INFO)
if args.debug:
log.setLevel(logging.DEBUG)
# Initialize everything...
if not args.no_init:
edbob.init(self.name, *(args.config_paths or []))
# Command line logging flags should override config.
if args.verbose:
log.setLevel(logging.INFO)
if args.debug:
log.setLevel(logging.DEBUG)
# And finally, do something of real value...
cmd = self.subcommands[cmd](parent=self)
cmd.show_progress = args.progress
cmd._run(*(args.command + args.argv))
class Subcommand(edbob.Object):
"""
Base class for application "subcommands." You'll want to derive from this
class as needed to implement most of your application's command logic.
"""
def __init__(self, **kwargs):
edbob.Object.__init__(self, **kwargs)
self.parser = argparse.ArgumentParser(
prog='%s %s' % (self.parent, self.name),
description=self.description,
)
self.add_parser_args(self.parser)
@property
@requires_impl(is_property=True)
def name(self):
"""
The name of the subcommand.
.. note::
The subcommand name should ideally be limited to 12 characters in
order to preserve formatting consistency when displaying help text.
"""
pass
@property
@requires_impl(is_property=True)
def description(self):
"""
The description for the subcommand.
"""
pass
def __repr__(self):
return "<Subcommand: %s>" % self.name
def add_parser_args(self, parser):
"""
If your subcommand accepts optional (or positional) arguments, you
should override this and add the possible arguments directly to
``parser`` (which is a :class:`argparse.ArgumentParser` instance) via
its ``add_argument()`` method.
"""
pass
def _run(self, *args):
args = self.parser.parse_args(list(args))
return self.run(args)
@requires_impl()
def run(self, args):
"""
Runs the subcommand. You must override this method within your
subclass. ``args`` will be a :class:`argparse.Namespace` object
containing all parsed arguments found on the original command line
executed by the user.
"""
pass
class DatabaseCommand(Subcommand):
"""
Provides tools for managing an ``edbob`` database; called as ``edbob db``.
This command requires additional arguments; see ``edbob help db`` for more
information.
"""
name = 'db'
description = "Tools for managing edbob databases"
def add_parser_args(self, parser):
parser.add_argument('-D', '--database', metavar='URL',
help="Database engine (default is edbob.db.engine)")
# parser.add_argument('command', choices=['upgrade', 'extensions', 'activate', 'deactivate'],
# help="Command to execute against database")
subparsers = parser.add_subparsers(title='subcommands')
extensions = subparsers.add_parser('extensions',
help="Display current extension status for the database")
extensions.set_defaults(func=self.extensions)
activate = subparsers.add_parser('activate',
help="Activate an extension within the database")
activate.add_argument('extension', help="Name of extension to activate")
activate.set_defaults(func=self.activate)
deactivate = subparsers.add_parser('deactivate',
help="Deactivate an extension within the database")
deactivate.add_argument('extension', help="Name of extension to deactivate")
deactivate.set_defaults(func=self.deactivate)
def activate(self, engine, args):
from edbob.db.extensions import (
available_extensions,
extension_active,
activate_extension,
)
if args.extension in available_extensions():
if not extension_active(args.extension, engine):
activate_extension(args.extension, engine)
print "Activated extension '%s' in database:" % args.extension
print ' %s' % engine.url
else:
print >> sys.stderr, "Extension already active: %s" % args.extension
else:
print >> sys.stderr, "Extension unknown: %s" % args.extension
def deactivate(self, engine, args):
from edbob.db.extensions import (
available_extensions,
extension_active,
deactivate_extension,
)
if args.extension in available_extensions():
if extension_active(args.extension, engine):
deactivate_extension(args.extension, engine)
print "Deactivated extension '%s' in database:" % args.extension
print ' %s' % engine.url
else:
print >> sys.stderr, "Extension already inactive: %s" % args.extension
else:
print >> sys.stderr, "Extension unknown: %s" % args.extension
def extensions(self, engine, args):
from edbob.db.extensions import (
available_extensions,
extension_active,
)
print "Extensions for database:"
print ' %s' % engine.url
print ''
print " Name Active?"
print "------------------------"
for name in sorted(available_extensions()):
print " %-16s %s" % (
name, 'Yes' if extension_active(name, engine) else 'No')
print ''
print "Use 'edbob db [de]activate <extension>' to change."
def run(self, args):
if args.database:
from sqlalchemy import create_engine
from sqlalchemy.exc import ArgumentError
try:
engine = create_engine(args.database)
except ArgumentError, err:
print err
return
else:
from edbob.db import engine
if not engine:
print >> sys.stderr, "Database not configured; please change that or specify -D URL"
return
args.func(engine, args)
# class ExtensionsCommand(RattailCommand):
# """
# Displays the currently-installed (available) extensions, and whether or not
# each has been activated within the configured database. Called as
# ``rattail extensions``.
# """
# short_description = "Displays available extensions and their statuses"
# def __call__(self, options, **args):
# from sqlalchemy.exc import OperationalError
# from rattail import engine
# from rattail.db.util import core_schema_installed
# from rattail.db.ext import get_available_extensions, extension_active
# try:
# engine.connect()
# except OperationalError, e:
# print >> sys.stderr, "Cannot connect to database:"
# print >> sys.stderr, engine.url
# print >> sys.stderr, e[0].strip()
# return
# if not core_schema_installed():
# print >> sys.stderr, "Database lacks core schema:"
# print >> sys.stderr, engine.url
# return
# extensions = get_available_extensions()
# print ''
# print ' %-25s %-10s' % ("Extension", "Active?")
# print '-' * 35
# for name in sorted(extensions):
# active = 'Y' if extension_active(name) else 'N'
# print ' %-28s %s' % (name, active)
# print ''
# print "Use 'rattail {activate|deactivate} EXTENSION' to change."
class FileMonitorCommand(Subcommand):
"""
Interacts with the file monitor service; called as ``edbob filemon``. This
command expects a subcommand; one of the following:
* ``edbob filemon start``
* ``edbob filemon stop``
On Windows platforms, the following additional subcommands are available:
* ``edbob filemon install``
* ``edbob filemon uninstall``
.. note::
The Windows Vista family of operating systems requires you to launch
``cmd.exe`` as an Administrator in order to have sufficient rights to
run the above commands.
.. todo::
Verify the previous statement... (Maybe test with/out UAC?)
See :doc:`howto.use_filemon` for more information.
"""
name = 'filemon'
description = "Manage the file monitor service"
appname = 'edbob'
def add_parser_args(self, parser):
subparsers = parser.add_subparsers(title='subcommands')
start = subparsers.add_parser('start', help="Start service")
start.set_defaults(subcommand='start')
stop = subparsers.add_parser('stop', help="Stop service")
stop.set_defaults(subcommand='stop')
if sys.platform == 'win32':
install = subparsers.add_parser('install', help="Install service")
install.set_defaults(subcommand='install')
install.add_argument('-a', '--auto-start', action='store_true',
help="Configure service to start automatically")
remove = subparsers.add_parser('remove', help="Uninstall (remove) service")
remove.set_defaults(subcommand='remove')
uninstall = subparsers.add_parser('uninstall', help="Uninstall (remove) service")
uninstall.set_defaults(subcommand='remove')
def get_win32_module(self):
from edbob.filemon import win32
return win32
def get_win32_service(self):
from edbob.filemon.win32 import FileMonitorService
return FileMonitorService
def get_win32_service_name(self):
service = self.get_win32_service()
return service._svc_name_
def run(self, args):
if sys.platform == 'linux2':
from edbob.filemon import linux as filemon
if args.subcommand == 'start':
filemon.start_daemon(self.appname)
elif args.subcommand == 'stop':
filemon.stop_daemon(self.appname)
elif sys.platform == 'win32':
from edbob import win32
filemon = self.get_win32_module()
# Execute typical service command.
options = []
if args.subcommand == 'install' and args.auto_start:
options = ['--startup', 'auto']
win32.execute_service_command(filemon, args.subcommand, *options)
# If installing auto-start service on Windows 7, we should update
# its startup type to be "Automatic (Delayed Start)".
if args.subcommand == 'install' and args.auto_start:
if platform.release() == '7':
name = self.get_win32_service_name()
win32.delayed_auto_start_service(name)
else:
print "Sorry, file monitor is not supported on platform %s." % sys.platform
class ShellCommand(Subcommand):
"""
Launches a Python shell (of your choice) with ``edbob`` pre-loaded; called
as ``edbob shell``.
You can define the shell within your config file (otherwise ``python`` is
assumed)::
.. highlight:: ini
[edbob]
shell.python = ipython
"""
name = 'shell'
description = "Launch Python shell with edbob pre-loaded"
def run(self, args):
code = ['import edbob']
if edbob.inited:
code.append("edbob.init('edbob', %s, shell=True)" %
edbob.config.paths_loaded)
code.append('from edbob import *')
code = '; '.join(code)
print "edbob v%s launching Python shell...\n" % edbob.__version__
python = 'python'
if edbob.inited:
python = edbob.config.get('edbob', 'shell.python', default=python)
proc = subprocess.Popen([python, '-i', '-c', code])
while True:
try:
proc.wait()
except KeyboardInterrupt:
pass
else:
break
class UuidCommand(Subcommand):
"""
Command for generating an UUID; called as ``edbob uuid``.
If the ``--gui`` option is specified, this command launches a small
wxPython application for generating UUIDs; otherwise a single UUID will be
printed to the console.
"""
name = 'uuid'
description = "Generate an universally-unique identifier"
def add_parser_args(self, parser):
parser.add_argument('--gui', action='store_true',
help="Display graphical interface")
def run(self, args):
if args.gui:
from edbob.wx.GenerateUuid import main
main()
else:
print edbob.get_uuid()
def main(*args):
"""
The primary entry point for the edbob command system.
.. note::
This entry point is really for ``edbob`` proper. Your application
should provide its own command entry point which leverages *your*
:class:`Command` subclass instead of using this entry point (which uses
:class:`Command` directly).
There's not much involved in doing so; see the source code for
implementation details.
"""
if args:
args = list(args)
else:
args = sys.argv[1:]
cmd = Command()
cmd.run(*args)