Skip to content

Commit

Permalink
Breaking change: Add 'list' command to show known databases, rearrang…
Browse files Browse the repository at this point in the history
…e command line options, give better error on unknown db (#18)

* Add list option

* Rearrange command line arguments
  • Loading branch information
vinceatbluelabs authored Feb 29, 2020
1 parent b8f9ef1 commit 5275292
Show file tree
Hide file tree
Showing 14 changed files with 169 additions and 66 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ credentials.
Example:

```sh
$ db-facts redshift
$ db-facts sh redshift
export CONNECTION_TYPE
CONNECTION_TYPE=direct
export LASTPASS_SHARE_NAME_SUFFIX
Expand Down
5 changes: 4 additions & 1 deletion db_facts/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@ class UserErrorException(Exception):


def fail_on_invalid_db_name(db_name: DBName) -> NoReturn:
raise UserErrorException('-'.join(db_name) + ' is not a valid DB name.')
raise UserErrorException('-'.join(db_name) + ' is not a valid DB name. '
'To list valid databases, run "db-facts list" and to configure a '
'new database, please see '
'https://github.com/bluelabsio/db-facts/blob/master/CONFIGURATION.md')
29 changes: 29 additions & 0 deletions db_facts/list_db_names.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from .config import load_config


def format_db_description(db_name, db_description):
if db_description is not None:
return f"{db_name} ({db_description})"
else:
return db_name


def list_db_names() -> None:
dbcli_config = load_config()

dbs = dbcli_config.get('dbs', {})
db_descriptions = {
db_name: db_config.get('description', None)
for db_name, db_config
in dbs.items()
}

output = [
format_db_description(db_name, db_description)
for db_name, db_description
in db_descriptions.items()
]
sorted_output = sorted(list(output))
print("Available db_names:")
print("* ", end='')
print("\n* ".join(sorted_output))
76 changes: 52 additions & 24 deletions db_facts/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import sys
from .db_info import db
from .exports import print_exports
from .list_db_names import list_db_names
from .config_yaml import config_yaml
from typing import List
from .errors import UserErrorException
Expand All @@ -13,40 +14,67 @@ class Runner():
def __init__(self) -> None:
pass

def dump_json(self, args: argparse.Namespace) -> None:
db_name_str: str = args.dbname[0]
db_name = db_name_str.split('-')

db_info = db(db_name)

print(json.dumps(db_info, sort_keys=True, indent=4))

def dump_config(self, args: argparse.Namespace):
db_name_str: str = args.dbname[0]
db_name = db_name_str.split('-')

db_info = db(db_name)
print(config_yaml(db_name_str, db_info), end='')

def dump_sh(self, args: argparse.Namespace):
db_name_str: str = args.dbname[0]
db_name = db_name_str.split('-')

db_info = db(db_name)
print_exports(db_info)

def run(self, argv: List[str]) -> int:
try:
desc = 'Pull information about databases from user-friendly names'
parser = argparse.ArgumentParser(prog='db-facts', description=desc)
parser.add_argument('--json', dest='json', action='store_const',
const=True, default=False,
help=('Report output in JSON format '
'(default: env vars)'))
parser.add_argument('--config', dest='config', action='store_const',
const=True, default=False,
help=('Report output in db-facts config format '
'(default: env vars)'))
parser.add_argument('dbname', nargs=1,
help=('Friendly name of database '
'(e.g., "redshift", "dnc", '
'"cms-impl-dbadmin")'))
subparsers = parser.add_subparsers()

args = parser.parse_args(argv[1:])
list_parser = subparsers.add_parser('list', help='List available dbnames')
list_parser.set_defaults(func=lambda args: list_db_names())

json_parser = subparsers.add_parser('json', help='Report output in JSON format')
json_parser.add_argument('dbname', nargs=1,
help=('Friendly name of database '
'(e.g., "redshift", "dmv", '
'"abc-dev-dbadmin")'))
json_parser.set_defaults(func=self.dump_json)

db_name_str: str = args.dbname[0]
db_name = db_name_str.split('-')
config_parser = subparsers.add_parser('config',
help='Report output in db-facts config format')
config_parser.add_argument('dbname', nargs=1,
help=('Friendly name of database '
'(e.g., "redshift", "dmv", '
'"abc-dev-dbadmin")'))
config_parser.set_defaults(func=self.dump_config)

db_info = db(db_name)
sh_parser = subparsers.add_parser('sh',
help=('Report output in Bourne shell envionment '
'variable format '))
sh_parser.add_argument('dbname', nargs=1,
help=('Friendly name of database '
'(e.g., "redshift", "dmv", '
'"abc-dev-dbadmin")'))
sh_parser.set_defaults(func=self.dump_sh)

if args.json and args.config:
print("Please specify only one of --json or --config", file=sys.stderr)
args = parser.parse_args(argv[1:])
if 'func' not in args:
parser.print_help(file=sys.stderr)
return 1
elif args.json:
print(json.dumps(db_info, sort_keys=True, indent=4))
elif args.config:
print(config_yaml(db_name_str, db_info), end='')
else:
print_exports(db_info)
args.func(args)

return 0
except UserErrorException as e:
print(str(e), file=sys.stderr)
Expand Down
2 changes: 1 addition & 1 deletion db_facts/util.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import subprocess


def backtick(cmd):
def backtick(cmd: str) -> str:
return subprocess.check_output(cmd).decode('utf-8').strip()
2 changes: 1 addition & 1 deletion metrics/coverage_high_water_mark
Original file line number Diff line number Diff line change
@@ -1 +1 @@
93.3100
97.9100
2 changes: 1 addition & 1 deletion metrics/mypy_high_water_mark
Original file line number Diff line number Diff line change
@@ -1 +1 @@
47.9200
72.6500
4 changes: 4 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
[coverage:report]
exclude_lines =
if TYPE_CHECKING:

[pep8]
max_line_length=100

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import sys


VERSION = '2.15.3'
VERSION = '3.0.0'


# From https://circleci.com/blog/continuously-deploying-python-packages-to-pypi-with-circleci/
Expand Down
54 changes: 21 additions & 33 deletions tests/test_runner_failure.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
from db_facts.errors import UserErrorException


def without_whitespace(s: str) -> str:
return s.replace(" ", "").replace("\n", "")


@patch('db_facts.runner.db')
@patch('sys.stderr', new_callable=StringIO)
@patch('sys.stdout', new_callable=StringIO)
Expand All @@ -16,21 +20,10 @@ def test_runner_no_arg(self,
mock_stderr,
mock_db):
runner = Runner()
with self.assertRaises(SystemExit):
runner.run(['/bin/db-facts'])
self.assertIn('the following arguments are required: dbname',
mock_stderr.getvalue())
self.assertEqual(mock_stdout.getvalue(), '')

def test_runner_too_many_args(self,
mock_stdout,
mock_stderr,
mock_db):
runner = Runner()
out = runner.run(['/bin/db-facts', '--config', '--json', 'foo'])
out = runner.run(['/bin/db-facts'])
self.assertEqual(out, 1)
self.assertIn('Please specify only one of --json or --config',
mock_stderr.getvalue())
self.assertIn(without_whitespace('Pull information about databases from'),
without_whitespace(mock_stderr.getvalue()))
self.assertEqual(mock_stdout.getvalue(), '')

def test_runner_exception(self,
Expand All @@ -40,7 +33,7 @@ def test_runner_exception(self,
runner = Runner()
mock_db.side_effect = UserErrorException('error message here')

self.assertEqual(1, runner.run(['/bin/db-facts', 'foo']))
self.assertEqual(1, runner.run(['/bin/db-facts', 'sh', 'foo']))
mock_db.assert_called_with(['foo'])
self.assertEqual(mock_stderr.getvalue(), 'error message here\n')
self.assertEqual(mock_stdout.getvalue(), '')
Expand All @@ -53,25 +46,20 @@ def test_runner_help(self,
with self.assertRaises(SystemExit):
runner.run(['/bin/db-facts', '--help'])
self.assertEqual(mock_stderr.getvalue(), '')
helpstr = ('usage: db-facts [-h] [--json] [--config] dbname\n'
'\n'
'Pull information about databases from '
'user-friendly names\n'
'\n'
'positional arguments:\n'
' dbname Friendly name of database '
'(e.g., "redshift", "dnc", "cms-impl-\n'
' dbadmin")\n'
'\n'
'optional arguments:\n'
' -h, --help show this help message and exit\n'
' --json Report output in JSON format '
'(default: env vars)\n'
' --config Report output in db-facts config '
'format (default: env vars)\n')
helpstr = """
usage: db-facts [-h] {list,json,config,sh} ...
Pull information about databases from user-friendly names
positional arguments:
{list,json,config,sh}
list List available dbnames
json Report output in JSON format
config Report output in db-facts config format
sh Report output in Bourne shell envionment variable format
def without_whitespace(s: str) -> str:
return s.replace(" ", "").replace("\n", "")
optional arguments:
-h, --help show this help message and exit"""

self.assertEqual(without_whitespace(mock_stdout.getvalue()),
without_whitespace(helpstr))
2 changes: 1 addition & 1 deletion tests/test_runner_success.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def test_runner_lpass(self,
'connection_type': 'connection_type',
}

self.assertEqual(0, runner.run(['/bin/db-facts', 'foo']))
self.assertEqual(0, runner.run(['/bin/db-facts', 'sh', 'foo']))
mock_db.assert_called_with(['foo'])
self.assertEqual(mock_stderr.getvalue(), '')
self.assertEqual(mock_stdout.getvalue(),
Expand Down
2 changes: 1 addition & 1 deletion tests/test_runner_success_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def test_runner_lpass_config(self,
'connection_type': 'connection_type',
}

self.assertEqual(0, runner.run(['/bin/db-facts', '--config', 'foo']))
self.assertEqual(0, runner.run(['/bin/db-facts', 'config', 'foo']))
mock_db.assert_called_with(['foo'])
self.assertEqual(mock_stderr.getvalue(), '')
parsed_yaml_value = {
Expand Down
2 changes: 1 addition & 1 deletion tests/test_runner_success_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def test_runner_lpass_json(self,
'connection_type': 'connection_type',
}

self.assertEqual(0, runner.run(['/bin/db-facts', '--json', 'foo']))
self.assertEqual(0, runner.run(['/bin/db-facts', 'json', 'foo']))
mock_db.assert_called_with(['foo'])
self.assertEqual(mock_stderr.getvalue(), '')
parsed_json_value = {
Expand Down
51 changes: 51 additions & 0 deletions tests/test_runner_success_list.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from db_facts.runner import Runner
import unittest
from io import StringIO
from unittest.mock import patch


@patch('sys.stderr', new_callable=StringIO)
@patch('sys.stdout', new_callable=StringIO)
class TestRunnerList(unittest.TestCase):

@patch('db_facts.list_db_names.load_config')
def test_runner_list(self,
mock_load_config,
mock_stdout,
mock_stderr):
runner = Runner()
mock_load_config.return_value = {
'dbs': {
'mydb1': {
'description': 'My favorite database',
'password': 'password',
'host': 'host',
'user': 'user',
'type': 'type',
'protocol': 'protocol',
'port': 123,
'database': 'dbname',
'lastpass_share_name_suffix': 'lastpass_share_name_suffix',
'connection_type': 'connection_type',
},
'mydb2': {
'password': 'password',
'host': 'host',
'user': 'user',
'type': 'type',
'protocol': 'protocol',
'port': 123,
'database': 'dbname',
'lastpass_share_name_suffix': 'lastpass_share_name_suffix',
'connection_type': 'connection_type',
}
}
}

self.assertEqual(0, runner.run(['/bin/db-facts', 'list']))
mock_load_config.assert_called_with()
self.assertEqual(mock_stderr.getvalue(), '')
self.assertEqual(mock_stdout.getvalue(),
("Available db_names:\n"
"* mydb1 (My favorite database)\n"
"* mydb2\n"))

0 comments on commit 5275292

Please sign in to comment.