From 22526a3e7e57bc11b88ef3633ec4b5bf98b6130d Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Tue, 25 Jun 2024 15:23:26 -0700 Subject: [PATCH 01/16] Add detail to list output (cherry picked from commit 14c2dbbb5ed94766545bf759e6b4db445cc4cf92) --- tabcmd/commands/site/list_command.py | 68 ++++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/tabcmd/commands/site/list_command.py b/tabcmd/commands/site/list_command.py index 64e4fa48..68edc341 100644 --- a/tabcmd/commands/site/list_command.py +++ b/tabcmd/commands/site/list_command.py @@ -13,8 +13,9 @@ class ListCommand(Server): # strings to move to string files local_strings = { "tabcmd_content_listing": "===== Listing {0} content for user {1}...", - "tabcmd_listing_label_name": "\tNAME: {}", + "tabcmd_listing_label_name": "NAME: {}", "tabcmd_listing_label_id": "ID: {}", + "tabcmd_listing_label_owner": "OWNER: {}", "tabcmd_content_none": "No content found.", } @@ -27,7 +28,17 @@ def define_args(list_parser): args_group.add_argument( "content", choices=["projects", "workbooks", "datasources", "flows"], help="View content" ) - args_group.add_argument("-d", "--details", action="store_true", help="Show object details") + + format_group = list_parser.add_mutually_exclusive_group() + # TODO: should this be saved directly to csv? + format_group.add_argument("--machine", action="store_true", help="Format output as csv for machine reading") + + data_group = list_parser.add_argument_group(title="Attributes to include") + # data_group.add_argument("-i", "--id", action="store_true", help="Show item id") # default true + data_group.add_argument("-n", "--name", action="store_true", help="Show item name") # default true + data_group.add_argument("-o", "--owner", action="store_true", help="Show item owner") + data_group.add_argument("-d", "--details", action="store_true", help="Show children of the item") + data_group.add_argument("-a", "--address", action="store_true", help="Show web address of the item") @staticmethod def run_command(args): @@ -51,16 +62,53 @@ def run_command(args): if not items or len(items) == 0: logger.info(ListCommand.local_strings["tabcmd_content_none"]) + exit(0) + + + logger.info(ListCommand.show_header(args, content_type)) for item in items: - if args.details: - logger.info("\t{}".format(item)) - if content_type == "workbooks": - server.workbooks.populate_views(item) - for v in item.views: - logger.info(v) + if args.machine: + id = item.id + name = ", " + item.name if args.name else "" + owner =", " + item.owner_id if args.owner else "" + url = "" + if args.address and content_type in ["workbooks, datasources"]: + url = item.content_url + children = ListCommand.format_children_listing(args, server, content_type, item) if args.details else "" + else: - logger.info(ListCommand.local_strings["tabcmd_listing_label_id"].format(item.id)) - logger.info(ListCommand.local_strings["tabcmd_listing_label_name"].format(item.name)) + id = ListCommand.local_strings["tabcmd_listing_label_id"].format(item.id) + name = ", " + ListCommand.local_strings["tabcmd_listing_label_name"].format(item.name) if args.name else "" + owner = ", " + ListCommand.local_strings["tabcmd_listing_label_owner"].format(item.owner_id) if args.owner else "" + + url = "" + if args.address and content_type == "workbooks": + url = item.content_url + children = ListCommand.format_children_listing(args, server, content_type, item) if args.details else "" + + logger.info("{0}{1}{2}{3}".format(id, name, owner, url, children)) + # TODO: do we want this line if it is csv output? + logger.info("{} total {}".format(len(items), content_type)) except Exception as e: Errors.exit_with_error(logger, e) + + @staticmethod + def format_children_listing(args, server, content_type, item): + if args.details: + if content_type == "workbooks": + server.workbooks.populate_views(item) + child_items = item.views[:10] + children = ", VIEWS: [" + ", ".join(map(lambda x: x.name, child_items)) + "]" + return children + return "" + + @staticmethod + def show_header(args, content_type): + id = "ID" + name = ", NAME" if args.name else "" + owner = ", OWNER" if args.owner else "" + url = ", URL" if args.address and content_type in ["workbooks, datasources"] else "" + children = ", CHILDREN" if args.details and content_type == "workbooks" else "" + return "{0}{1}{2}{3}".format(id, name, owner, children) + From 32733c32dd3a9fe642d5c565bed0803d6e8a27dc Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Tue, 7 Jan 2025 18:17:07 -0800 Subject: [PATCH 02/16] add new args to tests --- tests/commands/test_listing_commands.py | 36 ++++++++++++++++++++----- tests/commands/test_publish_command.py | 7 +---- tests/commands/test_run_commands.py | 7 ++++- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/tests/commands/test_listing_commands.py b/tests/commands/test_listing_commands.py index 5d45590f..a7742053 100644 --- a/tests/commands/test_listing_commands.py +++ b/tests/commands/test_listing_commands.py @@ -17,26 +17,46 @@ getter = MagicMock() getter.get = MagicMock("get", return_value=([fake_item], 1)) -mock_args = argparse.Namespace() -mock_args.logging_level = "INFO" - - @mock.patch("tabcmd.commands.auth.session.Session.create_session") @mock.patch("tableauserverclient.Server") class ListingTests(unittest.TestCase): + + + @staticmethod + def _set_up_session(mock_session, mock_server): + mock_session.return_value = mock_server + assert mock_session is not None + mock_session.assert_not_called() + global mock_args + mock_args = argparse.Namespace(logging_level="DEBUG") + # set values for things that should always have a default + # should refactor so this can be automated + mock_args.continue_if_exists = False + mock_args.project_name = None + mock_args.parent_project_path = None + mock_args.parent_path = None + mock_args.timeout = None + mock_args.username = None + mock_args.name = True + mock_args.owner = None + mock_args.address = None + mock_args.get_extract_encryption_mode = False + mock_args.details = False + + def test_list_sites(self, mock_server, mock_session): + ListingTests._set_up_session(mock_session, mock_server) mock_server.sites = getter - mock_args.get_extract_encryption_mode = False - mock_session.return_value = mock_server out_value = ListSiteCommand.run_command(mock_args) def test_list_content(self, mock_server, mock_session): + ListingTests._set_up_session(mock_session, mock_server) mock_server.flows = getter mock_args.content = "flows" - mock_session.return_value = mock_server out_value = ListCommand.run_command(mock_args) def test_list_wb_details(self, mock_server, mock_session): + ListingTests._set_up_session(mock_session, mock_server) mock_server.workbooks = getter mock_args.content = "workbooks" mock_session.return_value = mock_server @@ -44,6 +64,7 @@ def test_list_wb_details(self, mock_server, mock_session): out_value = ListCommand.run_command(mock_args) def test_list_datasources(self, mock_server, mock_session): + ListingTests._set_up_session(mock_session, mock_server) mock_server.datasources = getter mock_args.content = "datasources" mock_session.return_value = mock_server @@ -51,6 +72,7 @@ def test_list_datasources(self, mock_server, mock_session): out_value = ListCommand.run_command(mock_args) def test_list_projects(self, mock_server, mock_session): + ListingTests._set_up_session(mock_session, mock_server) mock_server.projects = getter mock_args.content = "projects" mock_session.return_value = mock_server diff --git a/tests/commands/test_publish_command.py b/tests/commands/test_publish_command.py index 800c81fc..beed3606 100644 --- a/tests/commands/test_publish_command.py +++ b/tests/commands/test_publish_command.py @@ -3,12 +3,7 @@ from unittest.mock import * import tableauserverclient as TSC -from tabcmd.commands.auth import login_command -from tabcmd.commands.datasources_and_workbooks import delete_command, export_command, get_url_command, publish_command - - -from typing import List, NamedTuple, TextIO, Union -import io +from tabcmd.commands.datasources_and_workbooks import publish_command mock_args = argparse.Namespace() diff --git a/tests/commands/test_run_commands.py b/tests/commands/test_run_commands.py index c24e3e74..df628e6b 100644 --- a/tests/commands/test_run_commands.py +++ b/tests/commands/test_run_commands.py @@ -400,11 +400,16 @@ def test_create_user(self, mock_session, mock_server): mock_session.assert_called() def test_list_content(self, mock_session, mock_server): + RunCommandsTest._set_up_session(mock_session, mock_server) + mock_args.name = False + mock_args.owner = None + mock_args.address = None + mock_args.get_extract_encryption_mode = False + mock_args.details = False mock_args.content = "workbooks" list_command.ListCommand.run_command(mock_args) mock_args.content = "projects" list_command.ListCommand.run_command(mock_args) mock_args.content = "flows" list_command.ListCommand.run_command(mock_args) - # todo: details, filters From f5b941137468e1da13a794c11502201ff9bab27e Mon Sep 17 00:00:00 2001 From: Jac Date: Thu, 31 Jul 2025 02:37:17 -0700 Subject: [PATCH 03/16] Update list_command.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tabcmd/commands/site/list_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tabcmd/commands/site/list_command.py b/tabcmd/commands/site/list_command.py index 68edc341..4e603f62 100644 --- a/tabcmd/commands/site/list_command.py +++ b/tabcmd/commands/site/list_command.py @@ -72,7 +72,7 @@ def run_command(args): name = ", " + item.name if args.name else "" owner =", " + item.owner_id if args.owner else "" url = "" - if args.address and content_type in ["workbooks, datasources"]: + if args.address and content_type in ["workbooks", "datasources"]: url = item.content_url children = ListCommand.format_children_listing(args, server, content_type, item) if args.details else "" From 64414b0f299f11ff9b6a259686947c97d96b9999 Mon Sep 17 00:00:00 2001 From: Jac Date: Thu, 31 Jul 2025 02:37:37 -0700 Subject: [PATCH 04/16] Update list_command.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tabcmd/commands/site/list_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tabcmd/commands/site/list_command.py b/tabcmd/commands/site/list_command.py index 4e603f62..a7e8d81e 100644 --- a/tabcmd/commands/site/list_command.py +++ b/tabcmd/commands/site/list_command.py @@ -108,7 +108,7 @@ def show_header(args, content_type): id = "ID" name = ", NAME" if args.name else "" owner = ", OWNER" if args.owner else "" - url = ", URL" if args.address and content_type in ["workbooks, datasources"] else "" + url = ", URL" if args.address and content_type in ["workbooks", "datasources"] else "" children = ", CHILDREN" if args.details and content_type == "workbooks" else "" return "{0}{1}{2}{3}".format(id, name, owner, children) From 29027ce21b6b193c772bbedf3235cf5631c6cd73 Mon Sep 17 00:00:00 2001 From: Jac Date: Thu, 31 Jul 2025 02:37:55 -0700 Subject: [PATCH 05/16] Update list_command.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tabcmd/commands/site/list_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tabcmd/commands/site/list_command.py b/tabcmd/commands/site/list_command.py index a7e8d81e..855140df 100644 --- a/tabcmd/commands/site/list_command.py +++ b/tabcmd/commands/site/list_command.py @@ -86,7 +86,7 @@ def run_command(args): url = item.content_url children = ListCommand.format_children_listing(args, server, content_type, item) if args.details else "" - logger.info("{0}{1}{2}{3}".format(id, name, owner, url, children)) + logger.info("{0}{1}{2}{3}{4}".format(id, name, owner, url, children)) # TODO: do we want this line if it is csv output? logger.info("{} total {}".format(len(items), content_type)) From b89ea1a965967db6f025678d012db752c5f41296 Mon Sep 17 00:00:00 2001 From: Jac Date: Thu, 31 Jul 2025 02:38:33 -0700 Subject: [PATCH 06/16] Update list_command.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tabcmd/commands/site/list_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tabcmd/commands/site/list_command.py b/tabcmd/commands/site/list_command.py index 855140df..483014e9 100644 --- a/tabcmd/commands/site/list_command.py +++ b/tabcmd/commands/site/list_command.py @@ -70,7 +70,7 @@ def run_command(args): if args.machine: id = item.id name = ", " + item.name if args.name else "" - owner =", " + item.owner_id if args.owner else "" + owner = ", " + item.owner_id if args.owner else "" url = "" if args.address and content_type in ["workbooks", "datasources"]: url = item.content_url From f55e09d13ceeefbbc5066440d44fabef20e6e9f5 Mon Sep 17 00:00:00 2001 From: Brian Cantoni Date: Fri, 1 Aug 2025 10:26:48 -0700 Subject: [PATCH 07/16] Black reformat --- tabcmd/commands/site/list_command.py | 44 +++++++++++++++---------- tests/commands/test_listing_commands.py | 6 ++-- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/tabcmd/commands/site/list_command.py b/tabcmd/commands/site/list_command.py index 483014e9..04f2f51a 100644 --- a/tabcmd/commands/site/list_command.py +++ b/tabcmd/commands/site/list_command.py @@ -28,14 +28,14 @@ def define_args(list_parser): args_group.add_argument( "content", choices=["projects", "workbooks", "datasources", "flows"], help="View content" ) - + format_group = list_parser.add_mutually_exclusive_group() - # TODO: should this be saved directly to csv? + # TODO: should this be saved directly to csv? format_group.add_argument("--machine", action="store_true", help="Format output as csv for machine reading") - + data_group = list_parser.add_argument_group(title="Attributes to include") # data_group.add_argument("-i", "--id", action="store_true", help="Show item id") # default true - data_group.add_argument("-n", "--name", action="store_true", help="Show item name") # default true + data_group.add_argument("-n", "--name", action="store_true", help="Show item name") # default true data_group.add_argument("-o", "--owner", action="store_true", help="Show item owner") data_group.add_argument("-d", "--details", action="store_true", help="Show children of the item") data_group.add_argument("-a", "--address", action="store_true", help="Show web address of the item") @@ -64,7 +64,6 @@ def run_command(args): logger.info(ListCommand.local_strings["tabcmd_content_none"]) exit(0) - logger.info(ListCommand.show_header(args, content_type)) for item in items: if args.machine: @@ -73,22 +72,34 @@ def run_command(args): owner = ", " + item.owner_id if args.owner else "" url = "" if args.address and content_type in ["workbooks", "datasources"]: - url = item.content_url - children = ListCommand.format_children_listing(args, server, content_type, item) if args.details else "" - + url = item.content_url + children = ( + ListCommand.format_children_listing(args, server, content_type, item) if args.details else "" + ) + else: id = ListCommand.local_strings["tabcmd_listing_label_id"].format(item.id) - name = ", " + ListCommand.local_strings["tabcmd_listing_label_name"].format(item.name) if args.name else "" - owner = ", " + ListCommand.local_strings["tabcmd_listing_label_owner"].format(item.owner_id) if args.owner else "" - + name = ( + ", " + ListCommand.local_strings["tabcmd_listing_label_name"].format(item.name) + if args.name + else "" + ) + owner = ( + ", " + ListCommand.local_strings["tabcmd_listing_label_owner"].format(item.owner_id) + if args.owner + else "" + ) + url = "" if args.address and content_type == "workbooks": - url = item.content_url - children = ListCommand.format_children_listing(args, server, content_type, item) if args.details else "" - + url = item.content_url + children = ( + ListCommand.format_children_listing(args, server, content_type, item) if args.details else "" + ) + logger.info("{0}{1}{2}{3}{4}".format(id, name, owner, url, children)) - # TODO: do we want this line if it is csv output? + # TODO: do we want this line if it is csv output? logger.info("{} total {}".format(len(items), content_type)) except Exception as e: Errors.exit_with_error(logger, e) @@ -102,7 +113,7 @@ def format_children_listing(args, server, content_type, item): children = ", VIEWS: [" + ", ".join(map(lambda x: x.name, child_items)) + "]" return children return "" - + @staticmethod def show_header(args, content_type): id = "ID" @@ -111,4 +122,3 @@ def show_header(args, content_type): url = ", URL" if args.address and content_type in ["workbooks", "datasources"] else "" children = ", CHILDREN" if args.details and content_type == "workbooks" else "" return "{0}{1}{2}{3}".format(id, name, owner, children) - diff --git a/tests/commands/test_listing_commands.py b/tests/commands/test_listing_commands.py index a7742053..99c41c4e 100644 --- a/tests/commands/test_listing_commands.py +++ b/tests/commands/test_listing_commands.py @@ -17,11 +17,10 @@ getter = MagicMock() getter.get = MagicMock("get", return_value=([fake_item], 1)) + @mock.patch("tabcmd.commands.auth.session.Session.create_session") @mock.patch("tableauserverclient.Server") class ListingTests(unittest.TestCase): - - @staticmethod def _set_up_session(mock_session, mock_server): mock_session.return_value = mock_server @@ -42,8 +41,7 @@ def _set_up_session(mock_session, mock_server): mock_args.address = None mock_args.get_extract_encryption_mode = False mock_args.details = False - - + def test_list_sites(self, mock_server, mock_session): ListingTests._set_up_session(mock_session, mock_server) mock_server.sites = getter From a875cd53c0bbeb87f85f232bdbccfc666d785096 Mon Sep 17 00:00:00 2001 From: Brian Cantoni Date: Fri, 1 Aug 2025 10:42:21 -0700 Subject: [PATCH 08/16] fix: add missing mock methods and attributes for ListCommand tests - Add all() method to getter mock objects to support ListCommand functionality - Add machine attribute to test args to prevent AttributeError - Set up proper server object mocks (workbooks, projects, datasources, flows) - Fixes failing tests in test_run_commands.py and test_listing_commands.py (thanks to Cursor for the assist) --- tests/commands/test_listing_commands.py | 2 ++ tests/commands/test_run_commands.py | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/tests/commands/test_listing_commands.py b/tests/commands/test_listing_commands.py index 99c41c4e..eaa2bf6e 100644 --- a/tests/commands/test_listing_commands.py +++ b/tests/commands/test_listing_commands.py @@ -16,6 +16,7 @@ getter = MagicMock() getter.get = MagicMock("get", return_value=([fake_item], 1)) +getter.all = MagicMock("all", return_value=[fake_item]) @mock.patch("tabcmd.commands.auth.session.Session.create_session") @@ -39,6 +40,7 @@ def _set_up_session(mock_session, mock_server): mock_args.name = True mock_args.owner = None mock_args.address = None + mock_args.machine = False mock_args.get_extract_encryption_mode = False mock_args.details = False diff --git a/tests/commands/test_run_commands.py b/tests/commands/test_run_commands.py index 10706785..2359aab3 100644 --- a/tests/commands/test_run_commands.py +++ b/tests/commands/test_run_commands.py @@ -52,6 +52,7 @@ creator = MagicMock() getter = MagicMock() getter.get = MagicMock("get", return_value=([fake_item], fake_item_pagination)) +getter.all = MagicMock("all", return_value=[fake_item]) getter.publish = MagicMock("publish", return_value=fake_item) getter.create_extract = MagicMock("create_extract", return_value=fake_job) getter.decrypt_extract = MagicMock("decrypt_extract", return_value=fake_job) @@ -432,9 +433,14 @@ def test_create_user(self, mock_session, mock_server): def test_list_content(self, mock_session, mock_server): RunCommandsTest._set_up_session(mock_session, mock_server) + mock_server.workbooks = getter + mock_server.projects = getter + mock_server.datasources = getter + mock_server.flows = getter mock_args.name = False mock_args.owner = None mock_args.address = None + mock_args.machine = False mock_args.get_extract_encryption_mode = False mock_args.details = False mock_args.content = "workbooks" From 58c5e3735e460d2485b7ed8bb8e2322be8bf5d3f Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Tue, 5 Aug 2025 13:30:14 -0700 Subject: [PATCH 09/16] black --- tabcmd/commands/site/list_command.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/tabcmd/commands/site/list_command.py b/tabcmd/commands/site/list_command.py index d64944c1..6671dead 100644 --- a/tabcmd/commands/site/list_command.py +++ b/tabcmd/commands/site/list_command.py @@ -70,16 +70,8 @@ def run_command(args): else: id = _("tabcmd.listing.label.id").format(item.id) - name = ( - ", " + _("tabcmd.listing.label.name").format(item.name) - if args.name - else "" - ) - owner = ( - ", " + _("tabcmd.listing.label.owner").format(item.owner_id) - if args.owner - else "" - ) + name = ", " + _("tabcmd.listing.label.name").format(item.name) if args.name else "" + owner = ", " + _("tabcmd.listing.label.owner").format(item.owner_id) if args.owner else "" url = "" if args.address and content_type == "workbooks": From 52063ecf4b143b1273b9334d32c67380040967af Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Tue, 5 Aug 2025 14:39:34 -0700 Subject: [PATCH 10/16] Extract hardcoded strings from list command Added some unit tests for coverage --- tabcmd/commands/site/list_command.py | 28 +-- .../locales/en/tabcmd_messages_en.properties | 15 ++ tests/commands/test_listing_commands.py | 185 +++++++++++++++++- tests/commands/test_session.py | 30 +-- 4 files changed, 228 insertions(+), 30 deletions(-) diff --git a/tabcmd/commands/site/list_command.py b/tabcmd/commands/site/list_command.py index 6671dead..9c732111 100644 --- a/tabcmd/commands/site/list_command.py +++ b/tabcmd/commands/site/list_command.py @@ -22,14 +22,14 @@ def define_args(list_parser): format_group = list_parser.add_mutually_exclusive_group() # TODO: should this be saved directly to csv? - format_group.add_argument("--machine", action="store_true", help="Format output as csv for machine reading") + format_group.add_argument("--machine", action="store_true", help=_("tabcmd.listing.help.machine")) - data_group = list_parser.add_argument_group(title="Attributes to include") + data_group = list_parser.add_argument_group(title=_("tabcmd.listing.group.attributes")) # data_group.add_argument("-i", "--id", action="store_true", help="Show item id") # default true - data_group.add_argument("-n", "--name", action="store_true", help="Show item name") # default true - data_group.add_argument("-o", "--owner", action="store_true", help="Show item owner") - data_group.add_argument("-d", "--details", action="store_true", help="Show children of the item") - data_group.add_argument("-a", "--address", action="store_true", help="Show web address of the item") + data_group.add_argument("-n", "--name", action="store_true", help=_("tabcmd.listing.help.name")) # default true + data_group.add_argument("-o", "--owner", action="store_true", help=_("tabcmd.listing.help.owner")) + data_group.add_argument("-d", "--details", action="store_true", help=_("tabcmd.listing.help.details")) + data_group.add_argument("-a", "--address", action="store_true", help=_("tabcmd.listing.help.address")) @staticmethod def run_command(args): @@ -83,7 +83,7 @@ def run_command(args): logger.info("{0}{1}{2}{3}{4}".format(id, name, owner, url, children)) # TODO: do we want this line if it is csv output? - logger.info("{} total {}".format(len(items), content_type)) + logger.info(_("tabcmd.listing.summary").format(len(items), content_type)) except Exception as e: Errors.exit_with_error(logger, e) @@ -93,15 +93,15 @@ def format_children_listing(args, server, content_type, item): if content_type == "workbooks": server.workbooks.populate_views(item) child_items = item.views[:10] - children = ", VIEWS: [" + ", ".join(map(lambda x: x.name, child_items)) + "]" + children = ", " + _("tabcmd.listing.label.views") + ", ".join(map(lambda x: x.name, child_items)) return children return "" @staticmethod def show_header(args, content_type): - id = "ID" - name = ", NAME" if args.name else "" - owner = ", OWNER" if args.owner else "" - url = ", URL" if args.address and content_type in ["workbooks", "datasources"] else "" - children = ", CHILDREN" if args.details and content_type == "workbooks" else "" - return "{0}{1}{2}{3}".format(id, name, owner, children) + id = _("tabcmd.listing.header.id") + name = ", " + _("tabcmd.listing.header.name") if args.name else "" + owner = ", " + _("tabcmd.listing.header.owner") if args.owner else "" + url = ", " + _("tabcmd.listing.header.url") if args.address and content_type in ["workbooks", "datasources"] else "" + children = ", " + _("tabcmd.listing.header.children") if args.details and content_type == "workbooks" else "" + return "{0}{1}{2}{3}{4}".format(id, name, owner, url, children) diff --git a/tabcmd/locales/en/tabcmd_messages_en.properties b/tabcmd/locales/en/tabcmd_messages_en.properties index c91d8f49..2fe0f59e 100644 --- a/tabcmd/locales/en/tabcmd_messages_en.properties +++ b/tabcmd/locales/en/tabcmd_messages_en.properties @@ -377,3 +377,18 @@ tabcmdparser.help.description=Show message listing commands and global options, version.description=Print version information +tabcmd.listing.group.attributes=Attributes to include +tabcmd.listing.header.children=CHILDREN +tabcmd.listing.header.id=ID +tabcmd.listing.header.name=NAME +tabcmd.listing.header.owner=OWNER +tabcmd.listing.header.url=URL +tabcmd.listing.help.address=Show web address of the item +tabcmd.listing.help.details=Show children of the item +tabcmd.listing.help.machine=Format output as csv for machine reading +tabcmd.listing.help.name=Show item name +tabcmd.listing.help.owner=Show item owner +tabcmd.listing.label.owner=\tOWNER: {} +tabcmd.listing.label.views=VIEWS: +tabcmd.listing.summary={0} total {1} + diff --git a/tests/commands/test_listing_commands.py b/tests/commands/test_listing_commands.py index eaa2bf6e..6d5ae394 100644 --- a/tests/commands/test_listing_commands.py +++ b/tests/commands/test_listing_commands.py @@ -1,8 +1,11 @@ import argparse -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch +import io +import sys from tabcmd.commands.site.list_command import ListCommand from tabcmd.commands.site.list_sites_command import ListSiteCommand +from tabcmd.execution.localize import set_client_locale import unittest from unittest import mock @@ -13,6 +16,11 @@ fake_item.name = "fake-name" fake_item.id = "fake-id" fake_item.extract_encryption_mode = "ENFORCED" +fake_item.owner_id = "fake-owner" +fake_item.content_url = "fake-url" + +fake_view = mock.MagicMock() +fake_view.name = "fake-view" getter = MagicMock() getter.get = MagicMock("get", return_value=([fake_item], 1)) @@ -58,6 +66,8 @@ def test_list_content(self, mock_server, mock_session): def test_list_wb_details(self, mock_server, mock_session): ListingTests._set_up_session(mock_session, mock_server) mock_server.workbooks = getter + mock_server.workbooks.populate_views = MagicMock() + fake_item.views = [fake_view] mock_args.content = "workbooks" mock_session.return_value = mock_server mock_args.details = True @@ -78,3 +88,176 @@ def test_list_projects(self, mock_server, mock_session): mock_session.return_value = mock_server mock_args.details = True out_value = ListCommand.run_command(mock_args) + + + +class ListCommandFunctionalTests(unittest.TestCase): + """Test that ListCommand properly uses localized strings in different scenarios""" + + @patch('tabcmd.commands.site.list_command._') + @patch('tabcmd.commands.auth.session.Session') + @patch('tabcmd.execution.logger_config.log') + def test_show_header_with_all_options(self, mock_log, mock_session, mock_translate): + """Test header generation with all display options enabled""" + # Mock the translation function to return the actual English strings + def translate_side_effect(key): + translations = { + "tabcmd.listing.header.id": "ID", + "tabcmd.listing.header.name": "NAME", + "tabcmd.listing.header.owner": "OWNER", + "tabcmd.listing.header.url": "URL", + "tabcmd.listing.header.children": "CHILDREN" + } + return translations.get(key, key) + + mock_translate.side_effect = translate_side_effect + + mock_args = argparse.Namespace( + name=True, + owner=True, + address=True, + details=True + ) + + # Test workbooks (should include all headers) + header = ListCommand.show_header(mock_args, "workbooks") + self.assertIn("ID", header) + self.assertIn("NAME", header) + self.assertIn("OWNER", header) + self.assertIn("URL", header) + self.assertIn("CHILDREN", header) + + # Test datasources (should include URL but not CHILDREN) + header = ListCommand.show_header(mock_args, "datasources") + self.assertIn("ID", header) + self.assertIn("NAME", header) + self.assertIn("OWNER", header) + self.assertIn("URL", header) + self.assertNotIn("CHILDREN", header) + + # Test projects (should not include URL or CHILDREN) + header = ListCommand.show_header(mock_args, "projects") + self.assertIn("ID", header) + self.assertIn("NAME", header) + self.assertIn("OWNER", header) + self.assertNotIn("URL", header) + self.assertNotIn("CHILDREN", header) + + @patch('tabcmd.commands.site.list_command._') + @patch('tabcmd.commands.auth.session.Session') + @patch('tabcmd.execution.logger_config.log') + def test_show_header_minimal_options(self, mock_log, mock_session, mock_translate): + """Test header generation with minimal options""" + # Mock the translation function + mock_translate.return_value = "ID" + + mock_args = argparse.Namespace( + name=False, + owner=False, + address=False, + details=False + ) + + header = ListCommand.show_header(mock_args, "workbooks") + self.assertEqual(header, "ID") + + @patch('tabcmd.commands.site.list_command._') + @patch('tableauserverclient.Server') + def test_format_children_listing_workbooks(self, mock_server, mock_translate): + """Test children listing format for workbooks""" + # Mock the translation function + mock_translate.return_value = "VIEWS: [" + + mock_args = argparse.Namespace(details=True) + + # Mock workbook item with views - create proper mock objects with string names + view1 = MagicMock() + view1.name = "View1" + view2 = MagicMock() + view2.name = "View2" + + mock_item = MagicMock() + mock_item.views = [view1, view2] + + # Mock server populate_views method + mock_server.workbooks.populate_views = MagicMock() + + result = ListCommand.format_children_listing(mock_args, mock_server, "workbooks", mock_item) + + self.assertIn("VIEWS: ", result) + self.assertIn("View1", result) + self.assertIn("View2", result) + mock_server.workbooks.populate_views.assert_called_once_with(mock_item) + + @patch('tableauserverclient.Server') + def test_format_children_listing_non_workbooks(self, mock_server): + """Test children listing returns empty for non-workbook content types""" + mock_args = argparse.Namespace(details=True) + mock_item = MagicMock() + + result = ListCommand.format_children_listing(mock_args, mock_server, "datasources", mock_item) + self.assertEqual(result, "") + + result = ListCommand.format_children_listing(mock_args, mock_server, "projects", mock_item) + self.assertEqual(result, "") + + @patch('tableauserverclient.Server') + def test_format_children_listing_no_details(self, mock_server): + """Test children listing returns empty when details=False""" + mock_args = argparse.Namespace(details=False) + mock_item = MagicMock() + + result = ListCommand.format_children_listing(mock_args, mock_server, "workbooks", mock_item) + self.assertEqual(result, "") + + +class LocalizedStringKeysTests(unittest.TestCase): + """Test that ListCommand calls the correct localization keys""" + + @patch('tabcmd.commands.site.list_command._') + def test_show_header_datasources_with_url(self, mock_translate): + """Test that datasources headers include URL when address=True""" + mock_translate.return_value = "TRANSLATED" + + mock_args = argparse.Namespace( + name=True, + owner=True, + address=True, # With address=True, datasources should show URL + details=True + ) + + ListCommand.show_header(mock_args, "datasources") + + # Verify that URL key IS called for datasources (but not CHILDREN) + expected_calls = [ + mock.call("tabcmd.listing.header.id"), + mock.call("tabcmd.listing.header.name"), + mock.call("tabcmd.listing.header.owner"), + mock.call("tabcmd.listing.header.url"), # Should be called for datasources too + # Note: tabcmd.listing.header.children should NOT be called (only for workbooks) + ] + mock_translate.assert_has_calls(expected_calls, any_order=True) + + # Verify CHILDREN key was not called (only for workbooks) + all_calls = [call[0][0] for call in mock_translate.call_args_list] + self.assertNotIn("tabcmd.listing.header.children", all_calls) + + def test_show_header_structure_without_mocking(self): + """Test the basic structure of show_header without mocking translations""" + mock_args = argparse.Namespace( + name=False, + owner=False, + address=False, + details=False + ) + + # This should return just the ID header (even if it's the localization key) + header = ListCommand.show_header(mock_args, "workbooks") + + # The result should be a single string (not contain commas when no options are set) + self.assertNotIn(",", header) + self.assertTrue(isinstance(header, str)) + self.assertGreater(len(header), 0) + + + diff --git a/tests/commands/test_session.py b/tests/commands/test_session.py index 3267efe2..982666db 100644 --- a/tests/commands/test_session.py +++ b/tests/commands/test_session.py @@ -489,20 +489,6 @@ def test_timeout_as_integer_stored_char(self): assert result == 0 -class TimeoutIntegrationTest(unittest.TestCase): - def test_connection_times_out(self): - test_args = Namespace(**vars(args_to_mock)) - new_session = Session() - test_args.timeout = 2 - test_args.username = "u" - test_args.password = "p" - - test_args.server = "https://nothere.com" - with self.assertRaises(SystemExit): - new_session.create_session(test_args, None) - - # should test connection doesn't time out? - class ConnectionOptionsTest(unittest.TestCase): def test_user_agent(self): @@ -547,8 +533,22 @@ def test_timeout(self): connection = mock_session._open_connection_with_opts() assert connection._http_options["timeout"] == 10 - """ + +This is too slow for unit tests. +class TimeoutIntegrationTest(unittest.TestCase): + def test_connection_times_out(self): + test_args = Namespace(**vars(args_to_mock)) + new_session = Session() + test_args.timeout = 2 + test_args.username = "u" + test_args.password = "p" + + test_args.server = "https://nothere.com" + with self.assertRaises(SystemExit): + new_session.create_session(test_args, None) + + class CookieTests(unittest.TestCase): def test_no_file_if_no_cookie(self): From 3285752f0b4b164ec52f3b4d7ea887c1a784c8e3 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Tue, 5 Aug 2025 14:41:22 -0700 Subject: [PATCH 11/16] black --- tabcmd/commands/site/list_command.py | 6 +- tests/commands/test_listing_commands.py | 112 ++++++++++-------------- tests/commands/test_session.py | 2 +- 3 files changed, 51 insertions(+), 69 deletions(-) diff --git a/tabcmd/commands/site/list_command.py b/tabcmd/commands/site/list_command.py index 9c732111..8e4a1a1a 100644 --- a/tabcmd/commands/site/list_command.py +++ b/tabcmd/commands/site/list_command.py @@ -102,6 +102,10 @@ def show_header(args, content_type): id = _("tabcmd.listing.header.id") name = ", " + _("tabcmd.listing.header.name") if args.name else "" owner = ", " + _("tabcmd.listing.header.owner") if args.owner else "" - url = ", " + _("tabcmd.listing.header.url") if args.address and content_type in ["workbooks", "datasources"] else "" + url = ( + ", " + _("tabcmd.listing.header.url") + if args.address and content_type in ["workbooks", "datasources"] + else "" + ) children = ", " + _("tabcmd.listing.header.children") if args.details and content_type == "workbooks" else "" return "{0}{1}{2}{3}{4}".format(id, name, owner, url, children) diff --git a/tests/commands/test_listing_commands.py b/tests/commands/test_listing_commands.py index 6d5ae394..766e12fb 100644 --- a/tests/commands/test_listing_commands.py +++ b/tests/commands/test_listing_commands.py @@ -90,35 +90,29 @@ def test_list_projects(self, mock_server, mock_session): out_value = ListCommand.run_command(mock_args) - class ListCommandFunctionalTests(unittest.TestCase): """Test that ListCommand properly uses localized strings in different scenarios""" - - @patch('tabcmd.commands.site.list_command._') - @patch('tabcmd.commands.auth.session.Session') - @patch('tabcmd.execution.logger_config.log') + + @patch("tabcmd.commands.site.list_command._") + @patch("tabcmd.commands.auth.session.Session") + @patch("tabcmd.execution.logger_config.log") def test_show_header_with_all_options(self, mock_log, mock_session, mock_translate): """Test header generation with all display options enabled""" # Mock the translation function to return the actual English strings def translate_side_effect(key): translations = { "tabcmd.listing.header.id": "ID", - "tabcmd.listing.header.name": "NAME", + "tabcmd.listing.header.name": "NAME", "tabcmd.listing.header.owner": "OWNER", "tabcmd.listing.header.url": "URL", - "tabcmd.listing.header.children": "CHILDREN" + "tabcmd.listing.header.children": "CHILDREN", } return translations.get(key, key) - + mock_translate.side_effect = translate_side_effect - - mock_args = argparse.Namespace( - name=True, - owner=True, - address=True, - details=True - ) - + + mock_args = argparse.Namespace(name=True, owner=True, address=True, details=True) + # Test workbooks (should include all headers) header = ListCommand.show_header(mock_args, "workbooks") self.assertIn("ID", header) @@ -126,7 +120,7 @@ def translate_side_effect(key): self.assertIn("OWNER", header) self.assertIn("URL", header) self.assertIn("CHILDREN", header) - + # Test datasources (should include URL but not CHILDREN) header = ListCommand.show_header(mock_args, "datasources") self.assertIn("ID", header) @@ -134,7 +128,7 @@ def translate_side_effect(key): self.assertIn("OWNER", header) self.assertIn("URL", header) self.assertNotIn("CHILDREN", header) - + # Test projects (should not include URL or CHILDREN) header = ListCommand.show_header(mock_args, "projects") self.assertIn("ID", header) @@ -142,71 +136,66 @@ def translate_side_effect(key): self.assertIn("OWNER", header) self.assertNotIn("URL", header) self.assertNotIn("CHILDREN", header) - - @patch('tabcmd.commands.site.list_command._') - @patch('tabcmd.commands.auth.session.Session') - @patch('tabcmd.execution.logger_config.log') + + @patch("tabcmd.commands.site.list_command._") + @patch("tabcmd.commands.auth.session.Session") + @patch("tabcmd.execution.logger_config.log") def test_show_header_minimal_options(self, mock_log, mock_session, mock_translate): """Test header generation with minimal options""" # Mock the translation function mock_translate.return_value = "ID" - - mock_args = argparse.Namespace( - name=False, - owner=False, - address=False, - details=False - ) - + + mock_args = argparse.Namespace(name=False, owner=False, address=False, details=False) + header = ListCommand.show_header(mock_args, "workbooks") self.assertEqual(header, "ID") - - @patch('tabcmd.commands.site.list_command._') - @patch('tableauserverclient.Server') + + @patch("tabcmd.commands.site.list_command._") + @patch("tableauserverclient.Server") def test_format_children_listing_workbooks(self, mock_server, mock_translate): """Test children listing format for workbooks""" # Mock the translation function mock_translate.return_value = "VIEWS: [" - + mock_args = argparse.Namespace(details=True) - + # Mock workbook item with views - create proper mock objects with string names view1 = MagicMock() view1.name = "View1" - view2 = MagicMock() + view2 = MagicMock() view2.name = "View2" - + mock_item = MagicMock() mock_item.views = [view1, view2] - + # Mock server populate_views method mock_server.workbooks.populate_views = MagicMock() - + result = ListCommand.format_children_listing(mock_args, mock_server, "workbooks", mock_item) - + self.assertIn("VIEWS: ", result) self.assertIn("View1", result) self.assertIn("View2", result) mock_server.workbooks.populate_views.assert_called_once_with(mock_item) - - @patch('tableauserverclient.Server') + + @patch("tableauserverclient.Server") def test_format_children_listing_non_workbooks(self, mock_server): """Test children listing returns empty for non-workbook content types""" mock_args = argparse.Namespace(details=True) mock_item = MagicMock() - + result = ListCommand.format_children_listing(mock_args, mock_server, "datasources", mock_item) self.assertEqual(result, "") - + result = ListCommand.format_children_listing(mock_args, mock_server, "projects", mock_item) self.assertEqual(result, "") - - @patch('tableauserverclient.Server') + + @patch("tableauserverclient.Server") def test_format_children_listing_no_details(self, mock_server): """Test children listing returns empty when details=False""" mock_args = argparse.Namespace(details=False) mock_item = MagicMock() - + result = ListCommand.format_children_listing(mock_args, mock_server, "workbooks", mock_item) self.assertEqual(result, "") @@ -214,20 +203,17 @@ def test_format_children_listing_no_details(self, mock_server): class LocalizedStringKeysTests(unittest.TestCase): """Test that ListCommand calls the correct localization keys""" - @patch('tabcmd.commands.site.list_command._') + @patch("tabcmd.commands.site.list_command._") def test_show_header_datasources_with_url(self, mock_translate): """Test that datasources headers include URL when address=True""" mock_translate.return_value = "TRANSLATED" - + mock_args = argparse.Namespace( - name=True, - owner=True, - address=True, # With address=True, datasources should show URL - details=True + name=True, owner=True, address=True, details=True # With address=True, datasources should show URL ) - + ListCommand.show_header(mock_args, "datasources") - + # Verify that URL key IS called for datasources (but not CHILDREN) expected_calls = [ mock.call("tabcmd.listing.header.id"), @@ -237,27 +223,19 @@ def test_show_header_datasources_with_url(self, mock_translate): # Note: tabcmd.listing.header.children should NOT be called (only for workbooks) ] mock_translate.assert_has_calls(expected_calls, any_order=True) - + # Verify CHILDREN key was not called (only for workbooks) all_calls = [call[0][0] for call in mock_translate.call_args_list] self.assertNotIn("tabcmd.listing.header.children", all_calls) def test_show_header_structure_without_mocking(self): """Test the basic structure of show_header without mocking translations""" - mock_args = argparse.Namespace( - name=False, - owner=False, - address=False, - details=False - ) - + mock_args = argparse.Namespace(name=False, owner=False, address=False, details=False) + # This should return just the ID header (even if it's the localization key) header = ListCommand.show_header(mock_args, "workbooks") - + # The result should be a single string (not contain commas when no options are set) self.assertNotIn(",", header) self.assertTrue(isinstance(header, str)) self.assertGreater(len(header), 0) - - - diff --git a/tests/commands/test_session.py b/tests/commands/test_session.py index 982666db..401f16ae 100644 --- a/tests/commands/test_session.py +++ b/tests/commands/test_session.py @@ -489,7 +489,6 @@ def test_timeout_as_integer_stored_char(self): assert result == 0 - class ConnectionOptionsTest(unittest.TestCase): def test_user_agent(self): mock_session = Session() @@ -533,6 +532,7 @@ def test_timeout(self): connection = mock_session._open_connection_with_opts() assert connection._http_options["timeout"] == 10 + """ This is too slow for unit tests. From a3426e0fa57f47677f616561b6abbeb3e5a05c25 Mon Sep 17 00:00:00 2001 From: Jac Date: Thu, 7 Aug 2025 12:22:21 -0700 Subject: [PATCH 12/16] Update tabcmd/commands/site/list_command.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tabcmd/commands/site/list_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tabcmd/commands/site/list_command.py b/tabcmd/commands/site/list_command.py index 8e4a1a1a..ba2ce91c 100644 --- a/tabcmd/commands/site/list_command.py +++ b/tabcmd/commands/site/list_command.py @@ -75,7 +75,7 @@ def run_command(args): url = "" if args.address and content_type == "workbooks": - url = item.content_url + url = ", " + item.content_url children = ( ListCommand.format_children_listing(args, server, content_type, item) if args.details else "" ) From 42c819b512cbbf0bb2c017ecde9f2a8ee3f4897c Mon Sep 17 00:00:00 2001 From: Jac Date: Thu, 7 Aug 2025 12:22:38 -0700 Subject: [PATCH 13/16] Update tabcmd/commands/site/list_command.py this will need test updates too Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tabcmd/commands/site/list_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tabcmd/commands/site/list_command.py b/tabcmd/commands/site/list_command.py index ba2ce91c..a0198b07 100644 --- a/tabcmd/commands/site/list_command.py +++ b/tabcmd/commands/site/list_command.py @@ -74,7 +74,7 @@ def run_command(args): owner = ", " + _("tabcmd.listing.label.owner").format(item.owner_id) if args.owner else "" url = "" - if args.address and content_type == "workbooks": + if args.address and content_type in ["workbooks", "datasources"]: url = ", " + item.content_url children = ( ListCommand.format_children_listing(args, server, content_type, item) if args.details else "" From f3eb15dc2994804e8a8962fd12051e489c9c1965 Mon Sep 17 00:00:00 2001 From: Jac Date: Thu, 7 Aug 2025 12:22:46 -0700 Subject: [PATCH 14/16] Update tabcmd/commands/site/list_command.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tabcmd/commands/site/list_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tabcmd/commands/site/list_command.py b/tabcmd/commands/site/list_command.py index a0198b07..5f1db7ac 100644 --- a/tabcmd/commands/site/list_command.py +++ b/tabcmd/commands/site/list_command.py @@ -63,7 +63,7 @@ def run_command(args): owner = ", " + item.owner_id if args.owner else "" url = "" if args.address and content_type in ["workbooks", "datasources"]: - url = item.content_url + url = ", " + item.content_url children = ( ListCommand.format_children_listing(args, server, content_type, item) if args.details else "" ) From 75b6bbf28ab2c9a43110f64a8343d756b09cd1cd Mon Sep 17 00:00:00 2001 From: Jac Date: Thu, 7 Aug 2025 12:23:07 -0700 Subject: [PATCH 15/16] Update tabcmd/commands/site/list_command.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tabcmd/commands/site/list_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tabcmd/commands/site/list_command.py b/tabcmd/commands/site/list_command.py index 5f1db7ac..0aed8079 100644 --- a/tabcmd/commands/site/list_command.py +++ b/tabcmd/commands/site/list_command.py @@ -65,7 +65,7 @@ def run_command(args): if args.address and content_type in ["workbooks", "datasources"]: url = ", " + item.content_url children = ( - ListCommand.format_children_listing(args, server, content_type, item) if args.details else "" + ", " + ListCommand.format_children_listing(args, server, content_type, item) if args.details else "" ) else: From 100b96e637e2aa31664339fe982890b22fd2b3ee Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Thu, 7 Aug 2025 12:27:15 -0700 Subject: [PATCH 16/16] black --- tabcmd/commands/site/list_command.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tabcmd/commands/site/list_command.py b/tabcmd/commands/site/list_command.py index 0aed8079..ef07a940 100644 --- a/tabcmd/commands/site/list_command.py +++ b/tabcmd/commands/site/list_command.py @@ -65,7 +65,9 @@ def run_command(args): if args.address and content_type in ["workbooks", "datasources"]: url = ", " + item.content_url children = ( - ", " + ListCommand.format_children_listing(args, server, content_type, item) if args.details else "" + ", " + ListCommand.format_children_listing(args, server, content_type, item) + if args.details + else "" ) else: