From 106f6a45ce1ce7901b04fe402ec0c6a131698ef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Armando=20Garc=C3=ADa=20Sancio?= Date: Sun, 18 Jan 2015 10:13:28 +0000 Subject: [PATCH] Initial dcos cli commit --- .gitignore | 4 ++ DESCRIPTION.rst | 0 README.rst | 68 ++++++++++++++++++++++++++++ data/data_file | 0 dcos/__init__.py | 69 ++++++++++++++++++++++++++++ dcos/config.py | 75 +++++++++++++++++++++++++++++++ dcos/subcommand.py | 31 +++++++++++++ setup.cfg | 5 +++ setup.py | 105 +++++++++++++++++++++++++++++++++++++++++++ tests/test_config.py | 49 ++++++++++++++++++++ tests/test_dcos.py | 17 +++++++ 11 files changed, 423 insertions(+) create mode 100644 .gitignore create mode 100644 DESCRIPTION.rst create mode 100644 README.rst create mode 100644 data/data_file create mode 100644 dcos/__init__.py create mode 100644 dcos/config.py create mode 100644 dcos/subcommand.py create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 tests/test_config.py create mode 100644 tests/test_dcos.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c26e208 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +dcos.egg-info/ +.ropeproject/ +env/ +*.pyc diff --git a/DESCRIPTION.rst b/DESCRIPTION.rst new file mode 100644 index 0000000..e69de29 diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..8c1852a --- /dev/null +++ b/README.rst @@ -0,0 +1,68 @@ +CLI-poc +======= +Proof-of-concept for a CLI with modular subcommands. + +Setup +----- + +#. Make sure you meet requirements for installing packages_ +#. Install the "wheel" project:: + + pip install wheel + +#. Clone git repo for the dcos cli:: + + git clone git@github.com:mesosphere/dcos-cli.git + +#. Change directory to the repo directory:: + + cd dcos-cli + +#. Create a virtualenv for the dcos cli project:: + + virtualenv --prompt='(dcos-cli) ' env + +Configure Development Environment +--------------------------------- + +#. Activate the virtualenv:: + + source env/bin/activate + +#. Install project in develop mode:: + + pip install -e . + +#. Export DCOS_PATH:: + + export DCOS_PATH=/env + +#. Export DCOS_CONFIG:: + + mkdir $DCOS_PATH/config + touch $DCOS_PATH/config.Dcos.toml + export DCOS_CONFIG=$DCOS_PATH/env/config/Dcos.toml + +Running POC +----------- + +#. List command help:: + + dcos --help + +#. Run subcommand:: + + dcos config --help + +Notes +----- +Submodule writing notes gathered so far: + +#. Because we are using python's pip to install packages it looks like we can't install packages + that share the same python's package of other installed packages because they will conflict once + deployed to virtualenv directory structure. + +#. Currently we require that subcommands implement an info command. For example dcos-subcommand + implements ``subcommand info``. + +.. _packages: https://packaging.python.org/en/latest/installing.html#installing-requirements diff --git a/data/data_file b/data/data_file new file mode 100644 index 0000000..e69de29 diff --git a/dcos/__init__.py b/dcos/__init__.py new file mode 100644 index 0000000..26472a7 --- /dev/null +++ b/dcos/__init__.py @@ -0,0 +1,69 @@ +""" +Usage: + dcos [--mesos=] [...] + dcos -h | --help + dcos --version + +Options: + -h, --help Show this screen + --version Show version + --mesos= URI for the Mesos master +""" + +import os +import subprocess +import docopt + + +def main(): + subcommands = _list_external_subcommands(os.environ['DCOS_PATH']) + subcommand_summaries = _external_command_documentation(subcommands) + + args = docopt.docopt( + _extend_usage_docopt(__doc__, subcommand_summaries), + version='dcos version 0.1.0', # TODO: grab from setuptool + options_first=True) + + argv = [args['']] + args[''] + + # TODO: We need to figure out a way to make subcommand's discoverable + if args[''] in subcommands: + command = 'dcos-{}'.format(args['']) + exit(subprocess.call([command] + argv)) + else: + exit( + '{!r} is not a dcos command. See "dcos --help".'.format( + args[''])) + + +def _list_external_subcommands(dcos_path): + # TODO: check that dir_path is directory? + prefix = 'dcos-' + + return [filename[len(prefix):] + + for dirpath, _, filenames + in os.walk(os.path.join(dcos_path, "bin")) + + for filename in filenames + + if filename.startswith(prefix) + and os.access(os.path.join(dirpath, filename), os.X_OK) + ] + + +def _external_command_documentation(commands): + def info(commnand): + return subprocess.check_output( + ['dcos-{}'.format(command), command, 'info']) + + return [(command, info(command)) for command in commands] + + +def _extend_usage_docopt(doc, command_summaries): + # TODO: make sure that we deal with long commands + doc += '\nThe dcos commands are:' + for command, summary in command_summaries: + doc += '\n\t{:15}\t{}'.format(command, summary.strip()) + + return doc diff --git a/dcos/config.py b/dcos/config.py new file mode 100644 index 0000000..26fa792 --- /dev/null +++ b/dcos/config.py @@ -0,0 +1,75 @@ +""" +Usage: + dcos config info + dcos config [] + dcos config --unset + dcos config --help + +Options: + -h, --help Show this screen + --unset Remove property from the config file +""" + +import os +import docopt +import toml + + +def main(): + config_path = os.environ['DCOS_CONFIG'] + args = docopt.docopt(__doc__) + + if args['config'] and args['info']: + print('Get and set DCOS command line options') + + elif args['config'] and args['--unset']: + config = _load_config_file(config_path) + _unset_property(config, args['']) + _save_config_file(config_path, config) + + elif args['config'] and args[''] is None: + config = _load_config_file(config_path) + print(_get_property(config, args[''])) + + elif args['config']: + config = _load_config_file(config_path) + _set_property(config, args[''], args['']) + _save_config_file(config_path, config) + + else: + print(args) + + +def _load_config_file(config_path): + with open(config_path) as config_file: + return toml.loads(config_file.read()) + + +def _save_config_file(config_path, config): + serial = toml.dumps(config) + with open(config_path, 'w') as config_file: + config_file.write(serial) + + +def _get_property(config, name): + for section in name.split('.'): + config = config[section] + + # TODO: Do we want to check that config is not a dictionary? + return config + + +def _set_property(config, name, value): + sections = name.split('.') + for section in sections[:-1]: + config = config[section] + + config[sections[-1]] = value + + +def _unset_property(config, name): + sections = name.split('.') + for section in sections[:-1]: + config = config[section] + + del config[sections[-1]] diff --git a/dcos/subcommand.py b/dcos/subcommand.py new file mode 100644 index 0000000..9b3c41a --- /dev/null +++ b/dcos/subcommand.py @@ -0,0 +1,31 @@ +""" +Usage: + dcos subcommand info + dcos subcommand install python + dcos subcommand -h | --help + +Options: + -h, --help Show this screen +""" + +import subprocess +import docopt + + +def main(): + args = docopt.docopt(__doc__) + + if args['subcommand'] and args['info']: + print('Manage DCOS external commands') + elif args['subcommand'] and args['install'] and args['python']: + print('Trying to install a python subcommand') + command = ['pip', 'install', args['']] + print('Running: {!r}'.format(command)) + # For now we are just going to call pip and see if it works + exit_status = subprocess.call(command) + + print( + 'Using pip returned the following exit status: {!r}'.format( + exit_status)) + else: + print(args) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..79bc678 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[bdist_wheel] +# This flag says that the code is written to work on both Python 2 and Python +# 3. If at all possible, it is good practice to do this. If you cannot, you +# will need to generate wheels for each Python version that you support. +universal=1 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..e70ad0d --- /dev/null +++ b/setup.py @@ -0,0 +1,105 @@ +from setuptools import setup, find_packages +from codecs import open +from os import path + +# TODO: what license should we use? + +here = path.abspath(path.dirname(__file__)) + +# Get the long description from the relevant file +with open(path.join(here, 'DESCRIPTION.rst'), encoding='utf-8') as f: + long_description = f.read() + +setup( + name='dcos', + + # Versions should comply with PEP440. For a discussion on single-sourcing + # the version across setup.py and the project code, see + # https://packaging.python.org/en/latest/single_source_version.html + version='0.1.0', + + description='Dcos cli poc project', + long_description=long_description, + + # The project's main homepage. + url='https://github.com/mesosphere/CLI-poc', + + # Author details + author='Mesosphere, Inc.', + author_email='team@mesosphere.io', + + # Choose your license + license='TODO', + + # See https://pypi.python.org/pypi?%3Aaction=list_classifiers + classifiers=[ + # How mature is this project? Common values are + # 3 - Alpha + # 4 - Beta + # 5 - Production/Stable + 'Development Status :: 3 - Alpha', + + # Indicate who your project is intended for + 'Intended Audience :: Developers', + 'Intended Audience :: Information Technology', + 'Topic :: Software Development :: User Interfaces', + + # Pick your license as you wish (should match "license" above) + 'License :: OSI Approved :: TODO License', + + # Specify the Python versions you support here. In particular, ensure + # that you indicate whether you support Python 2, Python 3 or both. + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + ], + + # What does your project relate to? + keywords='mesos apache marathon mesospehere command datacenter', + + # You can just specify the packages manually here if your project is + # simple. Or you can use find_packages(). + packages=find_packages(exclude=['contrib', 'docs', 'tests*']), + + # List run-time dependencies here. These will be installed by pip when your + # project is installed. For an analysis of "install_requires" vs pip's + # requirements files see: + # https://packaging.python.org/en/latest/requirements.html + install_requires=['docopt', 'toml'], + + # List additional groups of dependencies here (e.g. development + # dependencies). You can install these using the following syntax, for + # example: + # $ pip install -e .[dev,test] + extras_require={ + 'dev': ['check-manifest'], + 'test': ['coverage'], + }, + + # If there are data files included in your packages that need to be + # installed, specify them here. If using Python 2.6 or less, then these + # have to be included in MANIFEST.in as well. + package_data={ + 'sample': ['package_data.dat'], + }, + + # Although 'package_data' is the preferred approach, in some case you may + # need to place data files outside of your packages. + # In this case, 'data_file' will be installed into '/my_data' + data_files=[('my_data', ['data/data_file'])], + + # To provide executable scripts, use entry points in preference to the + # "scripts" keyword. Entry points provide cross-platform support and allow + # pip to create the appropriate form of executable for the target platform. + entry_points={ + 'console_scripts': [ + 'dcos=dcos:main', + 'dcos-subcommand=dcos.subcommand:main', + 'dcos-config=dcos.config:main', + ], + }, +) diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..f576adf --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,49 @@ +from dcos import config +import pytest + + +@pytest.fixture +def conf(): + return { + 'dcos': { + 'user': 'principal', + 'mesos_uri': 'zk://localhost/mesos' + }, + 'package': { + 'repo_uri': 'git://localhost/mesosphere/package-repo.git' + } + } + + +def test_unset_property(conf): + expect = { + 'dcos': { + 'user': 'principal', + 'mesos_uri': 'zk://localhost/mesos' + }, + 'package': {} + } + + config._unset_property(conf, 'package.repo_uri') + + assert conf == expect + + +def test_set_property(conf): + expect = { + 'dcos': { + 'user': 'group', + 'mesos_uri': 'zk://localhost/mesos' + }, + 'package': { + 'repo_uri': 'git://localhost/mesosphere/package-repo.git' + } + } + + config._set_property(conf, 'dcos.user', 'group') + + assert conf == expect + + +def test_get_property(conf): + config._get_property(conf, 'dcos.mesos_uri') == 'zk://localhost/mesos' diff --git a/tests/test_dcos.py b/tests/test_dcos.py new file mode 100644 index 0000000..e99c4cf --- /dev/null +++ b/tests/test_dcos.py @@ -0,0 +1,17 @@ +import dcos + + +def test_extend_usage_docopt(): + command_summaries = [ + ('first', 'first summary'), + ('second', ' second summary '), + ('third', 'third summary\n') + ] + + expected = """ +The dcos commands are: +\tfirst \tfirst summary +\tsecond \tsecond summary +\tthird \tthird summary""" + + assert dcos._extend_usage_docopt('', command_summaries) == expected