Add basic CLI

This commit is contained in:
Ben Firshman 2013-12-11 14:25:32 +00:00
parent 523fb99d79
commit 3b654ad349
10 changed files with 266 additions and 1 deletions

0
plum/cli/__init__.py Normal file
View File

29
plum/cli/command.py Normal file
View File

@ -0,0 +1,29 @@
from docker import Client
import logging
import os
import yaml
from ..service_collection import ServiceCollection
from .docopt_command import DocoptCommand
from .formatter import Formatter
from .utils import cached_property, mkdir
log = logging.getLogger(__name__)
class Command(DocoptCommand):
@cached_property
def client(self):
if os.environ.get('DOCKER_URL'):
return Client(os.environ['DOCKER_URL'])
else:
return Client()
@cached_property
def service_collection(self):
config = yaml.load(open('plum.yml'))
return ServiceCollection.from_config(self.client, config)
@cached_property
def formatter(self):
return Formatter()

View File

@ -0,0 +1,46 @@
import sys
from inspect import getdoc
from docopt import docopt, DocoptExit
def docopt_full_help(docstring, *args, **kwargs):
try:
return docopt(docstring, *args, **kwargs)
except DocoptExit:
raise SystemExit(docstring)
class DocoptCommand(object):
def sys_dispatch(self):
self.dispatch(sys.argv[1:], None)
def dispatch(self, argv, global_options):
self.perform_command(*self.parse(argv, global_options))
def perform_command(self, options, command, handler, command_options):
handler(command_options)
def parse(self, argv, global_options):
options = docopt_full_help(getdoc(self), argv, options_first=True)
command = options['COMMAND']
if not hasattr(self, command):
raise NoSuchCommand(command, self)
handler = getattr(self, command)
docstring = getdoc(handler)
if docstring is None:
raise NoSuchCommand(command, self)
command_options = docopt_full_help(docstring, options['ARGS'], options_first=True)
return (options, command, handler, command_options)
class NoSuchCommand(Exception):
def __init__(self, command, supercommand):
super(NoSuchCommand, self).__init__("No such command: %s" % command)
self.command = command
self.supercommand = supercommand

6
plum/cli/errors.py Normal file
View File

@ -0,0 +1,6 @@
from textwrap import dedent
class UserError(Exception):
def __init__(self, msg):
self.msg = dedent(msg).strip()

15
plum/cli/formatter.py Normal file
View File

@ -0,0 +1,15 @@
import texttable
import os
class Formatter(object):
def table(self, headers, rows):
height, width = os.popen('stty size', 'r').read().split()
table = texttable.Texttable(max_width=width)
table.set_cols_dtype(['t' for h in headers])
table.add_rows([headers] + rows)
table.set_deco(table.HEADER)
table.set_chars(['-', '|', '+', '-'])
return table.draw()

83
plum/cli/main.py Normal file
View File

@ -0,0 +1,83 @@
import datetime
import logging
import sys
import os
import re
from docopt import docopt
from inspect import getdoc
from .. import __version__
from ..service_collection import ServiceCollection
from .command import Command
from .errors import UserError
from .docopt_command import NoSuchCommand
log = logging.getLogger(__name__)
def main():
try:
command = TopLevelCommand()
command.sys_dispatch()
except KeyboardInterrupt:
log.error("\nAborting.")
exit(1)
except UserError, e:
log.error(e.msg)
exit(1)
except NoSuchCommand, e:
log.error("No such command: %s", e.command)
log.error("")
log.error("\n".join(parse_doc_section("commands:", getdoc(e.supercommand))))
exit(1)
# stolen from docopt master
def parse_doc_section(name, source):
pattern = re.compile('^([^\n]*' + name + '[^\n]*\n?(?:[ \t].*?(?:\n|$))*)',
re.IGNORECASE | re.MULTILINE)
return [s.strip() for s in pattern.findall(source)]
class TopLevelCommand(Command):
""".
Usage:
plum [options] [COMMAND] [ARGS...]
plum -h|--help
Options:
--verbose Show more output
--version Print version and exit
Commands:
ps List services and containers
"""
def ps(self, options):
"""
List services and containers.
Usage: ps
"""
for service in self.service_collection:
for container in service.containers:
print container['Names'][0]
def start(self, options):
"""
Start all services
Usage: start
"""
self.service_collection.start()
def stop(self, options):
"""
Stop all services
Usage: stop
"""
self.service_collection.stop()

76
plum/cli/utils.py Normal file
View File

@ -0,0 +1,76 @@
import datetime
import os
def cached_property(f):
"""
returns a cached property that is calculated by function f
http://code.activestate.com/recipes/576563-cached-property/
"""
def get(self):
try:
return self._property_cache[f]
except AttributeError:
self._property_cache = {}
x = self._property_cache[f] = f(self)
return x
except KeyError:
x = self._property_cache[f] = f(self)
return x
return property(get)
def yesno(prompt, default=None):
"""
Prompt the user for a yes or no.
Can optionally specify a default value, which will only be
used if they enter a blank line.
Unrecognised input (anything other than "y", "n", "yes",
"no" or "") will return None.
"""
answer = raw_input(prompt).strip().lower()
if answer == "y" or answer == "yes":
return True
elif answer == "n" or answer == "no":
return False
elif answer == "":
return default
else:
return None
# http://stackoverflow.com/a/5164027
def prettydate(d):
diff = datetime.datetime.utcnow() - d
s = diff.seconds
if diff.days > 7 or diff.days < 0:
return d.strftime('%d %b %y')
elif diff.days == 1:
return '1 day ago'
elif diff.days > 1:
return '{0} days ago'.format(diff.days)
elif s <= 1:
return 'just now'
elif s < 60:
return '{0} seconds ago'.format(s)
elif s < 120:
return '1 minute ago'
elif s < 3600:
return '{0} minutes ago'.format(s/60)
elif s < 7200:
return '1 hour ago'
else:
return '{0} hours ago'.format(s/3600)
def mkdir(path, permissions=0700):
if not os.path.exists(path):
os.mkdir(path)
os.chmod(path, permissions)
return path

View File

@ -29,6 +29,14 @@ class ServiceCollection(list):
collection.append(Service(client=client, links=links, **service_dict))
return collection
@classmethod
def from_config(cls, client, config):
dicts = []
for name, service in config.items():
service['name'] = name
dicts.append(service)
return cls.from_dicts(client, dicts)
def get(self, name):
for service in self:
if service.name == name:

View File

@ -1 +1,3 @@
git+git://github.com/dotcloud/docker-py.git@4fde1a242e1853cbf83e5a36371d8b4a49501c52
docopt==0.6.1
PyYAML==3.10

View File

@ -36,6 +36,6 @@ setup(
dependency_links=[],
entry_points="""
[console_scripts]
plum=plum:main
plum=plum.cli.main:main
""",
)