Skip to content

Commit 0a3e0c8

Browse files
author
Clark Perkins
committed
Make history/autocomplete work in shell
1 parent feb2dcb commit 0a3e0c8

File tree

6 files changed

+182
-26
lines changed

6 files changed

+182
-26
lines changed

setup.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@
1515
# limitations under the License.
1616
#
1717

18-
from __future__ import unicode_literals
19-
2018
import os
2119
import sys
2220

@@ -46,12 +44,13 @@ def test_python_version():
4644
with open('README.md') as f:
4745
LONG_DESCRIPTION = f.read()
4846

49-
CFG_DIR = os.path.expanduser("~/.stackdio-cli")
47+
CFG_DIR = os.path.join(os.path.expanduser('~'), '.stackdio-cli')
5048

5149
requirements = [
5250
'Jinja2==2.7.3',
5351
'PyYAML==3.11',
5452
'click>=6.0,<7.0',
53+
'cmd2>=0.6,<0.7',
5554
'keyring==3.7',
5655
'readline',
5756
'requests>=2.4.0,<2.6.0',
@@ -64,7 +63,7 @@ def test_python_version():
6463
'pylint<=1.2.0',
6564
]
6665

67-
if __name__ == "__main__":
66+
if __name__ == '__main__':
6867
test_python_version()
6968

7069
setup(
@@ -83,8 +82,8 @@ def test_python_version():
8382
[
8483
'bootstrap.yaml',
8584
]),
86-
(os.path.join(CFG_DIR, "blueprints"),
87-
["blueprints/%s" % f for f in os.listdir("blueprints")]),
85+
(os.path.join(CFG_DIR, 'blueprints'),
86+
['blueprints/%s' % f for f in os.listdir('blueprints')]),
8887
],
8988
zip_safe=False,
9089
install_requires=requirements,
@@ -113,7 +112,6 @@ def test_python_version():
113112
'Programming Language :: Python :: 3.2',
114113
'Programming Language :: Python :: 3.3',
115114
'Programming Language :: Python :: 3.4',
116-
'Programming Language :: Python :: 3.5',
117115
'Topic :: System :: Clustering',
118116
'Topic :: System :: Distributed Computing',
119117
]

stackdio/cli/__init__.py

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,48 +14,55 @@
1414
from stackdio.cli import mixins
1515
from stackdio.cli.shell import get_shell
1616
from stackdio.client import StackdIO
17+
from stackdio.client.config import StackdioConfig
18+
from stackdio.client.exceptions import MissingConfigException
1719
from stackdio.client.version import __version__
1820

1921

2022
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
2123

22-
CFG_DIR = os.path.expanduser("~/.stackdio-cli/")
23-
CFG_FILE = os.path.join(CFG_DIR, "config.json")
24-
KEYRING_SERVICE = "stackdio_cli"
2524

25+
KEYRING_SERVICE = 'stackdio_cli'
2626

27-
def get_client():
28-
if not os.path.isfile(CFG_FILE):
29-
click.echo('It looks like you haven\'t used this CLI before. Please run '
30-
'`stackdio-cli configure`'.format(sys.argv[0]))
31-
sys.exit(1)
3227

33-
config = json.load(open(CFG_FILE, 'r'))
34-
config['blueprint_dir'] = os.path.expanduser(config.get('blueprint_dir', ''))
28+
def load_config(fail_on_misconfigure, section='stackdio'):
29+
try:
30+
return StackdioConfig(section)
31+
except MissingConfigException:
32+
if fail_on_misconfigure:
33+
click.echo('It looks like you haven\'t used this CLI before. Please run '
34+
'`stackdio-cli configure`'.format(sys.argv[0]))
35+
sys.exit(1)
36+
else:
37+
return None
38+
3539

40+
def get_client(config):
3641
return StackdIO(
37-
base_url=config["url"],
42+
base_url=config['url'],
3843
auth=(
39-
config["username"],
40-
keyring.get_password(KEYRING_SERVICE, config.get("username") or "")
44+
config['username'],
45+
keyring.get_password(KEYRING_SERVICE, config.get('username') or '')
4146
),
4247
verify=config.get('verify', True)
4348
)
4449

4550

4651
class StackdioObj(object):
4752

48-
def __init__(self, ctx):
53+
def __init__(self, ctx, fail_on_misconfigure):
4954
super(StackdioObj, self).__init__()
55+
self.config = load_config(fail_on_misconfigure)
5056
self.shell = get_shell(ctx)
51-
self.client = get_client()
57+
if self.config:
58+
self.client = get_client(self.config)
5259

5360

5461
@click.group(context_settings=CONTEXT_SETTINGS, invoke_without_command=True)
5562
@click.version_option(__version__, '-v', '--version')
5663
@click.pass_context
5764
def stackdio(ctx):
58-
ctx.obj = StackdioObj(ctx)
65+
ctx.obj = StackdioObj(ctx, ctx.invoked_subcommand != 'configure')
5966

6067
if ctx.invoked_subcommand is None:
6168
ctx.obj.shell.cmdloop()
@@ -81,7 +88,11 @@ def formulas():
8188

8289
@stackdio.command()
8390
def configure():
84-
pass
91+
config = StackdioConfig(create=True)
92+
93+
config.prompt_for_config()
94+
95+
config.save()
8596

8697

8798
@stackdio.command('server-version')

stackdio/cli/shell.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,12 @@
1616
#
1717

1818
import os
19-
from cmd import Cmd
19+
from cmd2 import Cmd
2020

2121
import click
2222

2323
from stackdio.client.version import __version__
2424

25-
2625
try:
2726
import readline
2827
except ImportError:

stackdio/client/compat.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# Copyright 2014, Digital Reasoning
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
try:
19+
# Python 2
20+
from ConfigParser import ConfigParser, NoOptionError
21+
except ImportError:
22+
# Python 3
23+
from configparser import ConfigParser, NoOptionError

stackdio/client/config.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# Copyright 2014, Digital Reasoning
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
import os
19+
20+
import click
21+
import requests
22+
from requests.exceptions import ConnectionError, MissingSchema
23+
24+
from stackdio.client.compat import ConfigParser, NoOptionError
25+
from stackdio.client.exceptions import MissingConfigException
26+
27+
28+
CFG_DIR = os.path.join(os.path.expanduser('~'), '.stackdio')
29+
CFG_FILE = os.path.join(CFG_DIR, 'client.cfg')
30+
31+
32+
class StackdioConfig(object):
33+
34+
def __init__(self, section='stackdio', config_file=CFG_FILE, create=False):
35+
super(StackdioConfig, self).__init__()
36+
37+
self._cfg_file = config_file
38+
39+
if not create and not os.path.isfile(config_file):
40+
raise MissingConfigException('{0} does not exist'.format(config_file))
41+
42+
self._config = ConfigParser()
43+
44+
if create:
45+
self._config.add_section(section)
46+
else:
47+
self._config.read(config_file)
48+
49+
# Make the blueprint dir usable
50+
new_blueprint_dir = os.path.expanduser(self.get('blueprint_dir'))
51+
self._config.set(section, 'blueprint_dir', new_blueprint_dir)
52+
53+
self.section = section
54+
55+
def save(self):
56+
with open(self._cfg_file, 'w') as f:
57+
self._config.write(f)
58+
59+
def __getitem__(self, item):
60+
try:
61+
return self._config.get(self.section, item)
62+
except NoOptionError:
63+
raise KeyError(item)
64+
65+
def __setitem__(self, key, value):
66+
self._config.set(self.section, key, value)
67+
68+
def get(self, item, default=None):
69+
try:
70+
return self[item]
71+
except KeyError:
72+
return default
73+
74+
def items(self):
75+
return self._config.items(self.section)
76+
77+
def prompt_for_config(self):
78+
self.get_url()
79+
80+
def _test_url(self, url):
81+
try:
82+
r = requests.get(url, verify=self.get('verify', True))
83+
return (200 <= r.status_code < 300) or r.status_code == 403
84+
except ConnectionError:
85+
return False
86+
except MissingSchema:
87+
print("You might have forgotten http:// or https://")
88+
return False
89+
90+
def get_url(self):
91+
92+
if self.get('url') is not None:
93+
val = click.prompt('Keep existing url', default='y', prompt_suffix=' (y|n)? ')
94+
if val not in ('N', 'n'):
95+
return
96+
97+
val = click.prompt('Does your stackd.io server have a self-signed SSL certificate',
98+
default='n', prompt_suffix=' (y|n)? ')
99+
100+
if val in ('Y', 'y'):
101+
self['verify'] = False
102+
else:
103+
self['verify'] = True
104+
105+
self['url'] = None
106+
107+
while self['url'] is None:
108+
url = click.prompt('What is the URL of your stackd.io server', prompt_suffix='? ')
109+
if url.endswith('api'):
110+
url += '/'
111+
elif url.endswith('api/'):
112+
pass
113+
elif url.endswith('/'):
114+
url += 'api/'
115+
else:
116+
url += '/api/'
117+
if self._test_url(url):
118+
self['url'] = url
119+
else:
120+
click.echo('There was an error while attempting to contact that server. '
121+
'Try again.')

stackdio/client/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
#
1717

1818

19+
class MissingConfigException(Exception):
20+
pass
21+
22+
1923
class StackException(Exception):
2024
pass
2125

0 commit comments

Comments
 (0)