diff --git a/src/azure/cli/_output.py b/src/azure/cli/_output.py new file mode 100644 index 00000000000..53d84bc87c6 --- /dev/null +++ b/src/azure/cli/_output.py @@ -0,0 +1,115 @@ +from __future__ import print_function, unicode_literals + +import sys +import json + +try: + # Python 3 + from io import StringIO +except ImportError: + # Python 2 + from StringIO import StringIO + +class OutputFormatException(Exception): + pass + +def format_json(obj): + input_dict = obj.__dict__ if hasattr(obj, '__dict__') else obj + return json.dumps(input_dict, indent=2, sort_keys=True, separators=(',', ': ')) + +def format_table(obj): + obj_list = obj if isinstance(obj, list) else [obj] + to = TableOutput() + try: + for item in obj_list: + for item_key in sorted(item): + to.cell(item_key, item[item_key]) + to.end_row() + return to.dump() + except TypeError: + return '' + +def format_text(obj): + obj_list = obj if isinstance(obj, list) else [obj] + to = TextOutput() + try: + for item in obj_list: + for item_key in sorted(item): + to.add(item_key, item[item_key]) + return to.dump() + except TypeError: + return '' + +class OutputProducer(object): + + def __init__(self, formatter=format_json, file=sys.stdout): + self.formatter = formatter + self.file = file + + def out(self, obj): + print(self.formatter(obj), file=self.file) + +class TableOutput(object): + def __init__(self): + self._rows = [{}] + self._columns = {} + self._column_order = [] + + def dump(self): + if len(self._rows) == 1: + return + + with StringIO() as io: + cols = [(c, self._columns[c]) for c in self._column_order] + io.write(' | '.join(c.center(w) for c, w in cols)) + io.write('\n') + io.write('-|-'.join('-' * w for c, w in cols)) + io.write('\n') + for r in self._rows[:-1]: + io.write(' | '.join(r[c].ljust(w) for c, w in cols)) + io.write('\n') + return io.getvalue() + + @property + def any_rows(self): + return len(self._rows) > 1 + + def cell(self, name, value): + n = str(name) + v = str(value) + max_width = self._columns.get(n) + if max_width is None: + self._column_order.append(n) + max_width = len(n) + self._rows[-1][n] = v + self._columns[n] = max(max_width, len(v)) + + def end_row(self): + self._rows.append({}) + +class TextOutput(object): + + def __init__(self): + self.identifiers = {} + + def add(self, identifier, value): + if identifier in self.identifiers: + self.identifiers[identifier].append(value) + else: + self.identifiers[identifier] = [value] + + def dump(self): + with StringIO() as io: + for id in sorted(self.identifiers): + io.write(id.upper()) + io.write('\t') + for col in self.identifiers[id]: + if isinstance(col, str): + io.write(col) + else: + # TODO: Handle complex objects + io.write("null") + io.write('\t') + io.write('\n') + return io.getvalue() + diff --git a/src/azure/cli/_profile.py b/src/azure/cli/_profile.py index 1f663ba581b..f72e70ecaa5 100644 --- a/src/azure/cli/_profile.py +++ b/src/azure/cli/_profile.py @@ -12,7 +12,7 @@ def normalize_properties(user, subscriptions): consolidated = [] for s in subscriptions: consolidated.append({ - 'id': s.id.split('/')[-1], + 'id': s.id.rpartition('/')[2], 'name': s.display_name, 'state': s.state, 'user': user, diff --git a/src/azure/cli/_util.py b/src/azure/cli/_util.py index 83f30161e5d..6449b631737 100644 --- a/src/azure/cli/_util.py +++ b/src/azure/cli/_util.py @@ -1,38 +1,3 @@ -import types -class TableOutput(object): - def __enter__(self): - self._rows = [{}] - self._columns = {} - self._column_order = [] - return self - - def __exit__(self, ex_type, ex_value, ex_tb): - if ex_type: - return - if len(self._rows) == 1: - return - - cols = [(c, self._columns[c]) for c in self._column_order] - print(' | '.join(c.center(w) for c, w in cols)) - print('-|-'.join('-' * w for c, w in cols)) - for r in self._rows[:-1]: - print(' | '.join(r[c].ljust(w) for c, w in cols)) - print() - - @property - def any_rows(self): - return len(self._rows) > 1 - - def cell(self, name, value): - n = str(name) - v = str(value) - max_width = self._columns.get(n) - if max_width is None: - self._column_order.append(n) - max_width = len(n) - self._rows[-1][n] = v - self._columns[n] = max(max_width, len(v)) - - def end_row(self): - self._rows.append({}) +def normalize_newlines(str_to_normalize): + return str_to_normalize.replace('\r\n', '\n') diff --git a/src/azure/cli/commands/account.py b/src/azure/cli/commands/account.py index 2eb554fed68..4df6168ecf4 100644 --- a/src/azure/cli/commands/account.py +++ b/src/azure/cli/commands/account.py @@ -1,5 +1,4 @@ from .._profile import Profile -from .._util import TableOutput from ..commands import command, description, option @command('account list') @@ -8,14 +7,7 @@ def list_subscriptions(args, unexpected): profile = Profile() subscriptions = profile.load_subscriptions() - with TableOutput() as to: - for subscription in subscriptions: - to.cell('Name', subscription['name']) - to.cell('Active', bool(subscription['active'])) - to.cell('User', subscription['user']) - to.cell('Subscription Id', subscription['id']) - to.cell('State', subscription['state']) - to.end_row() + return subscriptions @command('account set') @description(_('Set the current subscription')) diff --git a/src/azure/cli/commands/login.py b/src/azure/cli/commands/login.py index 447096e5437..92f7eadd690 100644 --- a/src/azure/cli/commands/login.py +++ b/src/azure/cli/commands/login.py @@ -2,8 +2,9 @@ from azure.mgmt.resource.subscriptions import SubscriptionClient, \ SubscriptionClientConfiguration +from msrest import Serializer + from .._profile import Profile -from .._util import TableOutput from ..commands import command, description, option CLIENT_ID = '04b07795-8ddb-461a-bbee-02f9e1bf7b46' @@ -27,18 +28,11 @@ def login(args, unexpected): if not subscriptions: raise RuntimeError(_('No subscriptions found for this account.')) - #keep useful properties and not json serializable + serializable = Serializer().serialize_data(subscriptions, "[Subscription]") + #keep useful properties and not json serializable profile = Profile() consolidated = Profile.normalize_properties(username, subscriptions) profile.set_subscriptions(consolidated, credentials.token['access_token']) - with TableOutput() as to: - for subscription in consolidated: - to.cell('Name', subscription['name']) - to.cell('Active', bool(subscription['active'])) - to.cell('User', subscription['user']) - to.cell('Subscription Id', subscription['id']) - to.cell('State', subscription['state']) - to.end_row() - + return serializable diff --git a/src/azure/cli/commands/storage.py b/src/azure/cli/commands/storage.py index f235a26ca3b..74699c83e17 100644 --- a/src/azure/cli/commands/storage.py +++ b/src/azure/cli/commands/storage.py @@ -1,6 +1,6 @@ +from msrest import Serializer + from ..main import SESSION -from .._logging import logging -from .._util import TableOutput from ..commands import command, description, option from .._profile import Profile @@ -24,15 +24,8 @@ def list_accounts(args, unexpected): else: accounts = smc.storage_accounts.list() - with TableOutput() as to: - for acc in accounts: - assert isinstance(acc, StorageAccount) - to.cell('Name', acc.name) - to.cell('Type', acc.account_type) - to.cell('Location', acc.location) - to.end_row() - if not to.any_rows: - print('No storage accounts defined') + serializable = Serializer().serialize_data(accounts, "[StorageAccount]") + return serializable @command('storage account check') @option('--account-name ') diff --git a/src/azure/cli/main.py b/src/azure/cli/main.py index 3598cb5538b..08aaeee6ea7 100644 --- a/src/azure/cli/main.py +++ b/src/azure/cli/main.py @@ -4,6 +4,7 @@ from ._locale import install as locale_install from ._logging import configure_logging, logger from ._session import Session +from ._output import OutputProducer # CONFIG provides external configuration options CONFIG = Session() @@ -37,7 +38,11 @@ def main(args): commands.add_to_parser(parser) try: - parser.execute(args) + result = parser.execute(args) + # Commands can return a dictionary/list of results + # If they do, we print the results. + if result: + OutputProducer().out(result) except RuntimeError as ex: logger.error(ex.args[0]) return ex.args[1] if len(ex.args) >= 2 else -1 diff --git a/src/azure/cli/tests/test_output.py b/src/azure/cli/tests/test_output.py new file mode 100644 index 00000000000..14ab85cb96c --- /dev/null +++ b/src/azure/cli/tests/test_output.py @@ -0,0 +1,57 @@ +from __future__ import print_function + +import unittest + +try: + # Python 3 + from io import StringIO +except ImportError: + # Python 2 + from StringIO import StringIO + +from azure.cli._output import OutputProducer, OutputFormatException, format_json, format_table, format_text +import azure.cli._util as util + +class TestOutput(unittest.TestCase): + + @classmethod + def setUpClass(cls): + pass + + @classmethod + def tearDownClass(cls): + pass + + def setUp(self): + self.io = StringIO() + + def tearDown(self): + self.io.close() + + def test_out_json_valid(self): + """ + The JSON output when the input is a dict should be the dict serialized to JSON + """ + output_producer = OutputProducer(formatter=format_json, file=self.io) + output_producer.out({'active': True, 'id': '0b1f6472'}) + self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( +"""{ + "active": true, + "id": "0b1f6472" +} +""")) + + def test_out_table_valid(self): + """ + """ + output_producer = OutputProducer(formatter=format_table, file=self.io) + output_producer.out({'active': True, 'id': '0b1f6472'}) + self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( +"""active | id +-------|--------- +True | 0b1f6472 + +""")) + +if __name__ == '__main__': + unittest.main()