diff --git a/tabcmd/commands/constants.py b/tabcmd/commands/constants.py index 0eaa9b3c..ba4e6288 100644 --- a/tabcmd/commands/constants.py +++ b/tabcmd/commands/constants.py @@ -44,10 +44,11 @@ def log_stack(logger): start = 0 n_lines = 5 logger.trace(HEADER_FMT % (file, func)) - for frame in stack[start + 1 : n_lines]: + + for frame in stack[start + 2 : n_lines]: file, line, func = frame[1:4] logger.trace(STACK_FMT % (file, line, func)) - except BaseException as e: + except Exception as e: logger.info("Error printing stack trace:", e) @staticmethod @@ -64,10 +65,11 @@ def exit_with_error(logger, message=None, exception=None): # "session.session_expired_login")) # session.renew_session() if message: - logger.debug(message) + logger.debug("Error message: " + message) Errors.check_common_error_codes_and_explain(logger, exception) except Exception as exc: print("Error during log call from exception - {} {}".format(exc.__class__, message)) + print("Exiting...") sys.exit(1) @staticmethod diff --git a/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py b/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py index 2c2d421d..e59851ab 100644 --- a/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py +++ b/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py @@ -23,6 +23,7 @@ def get_view_by_content_url(logger, server, view_content_url) -> TSC.ViewItem: try: req_option = TSC.RequestOptions() req_option.filter.add(TSC.Filter("contentUrl", TSC.RequestOptions.Operator.Equals, view_content_url)) + logger.trace(req_option.get_query_params()) matching_views, paging = server.views.get(req_option) except TSC.ServerResponseError as e: Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response").format(e)) @@ -36,6 +37,7 @@ def get_wb_by_content_url(logger, server, workbook_content_url) -> TSC.WorkbookI try: req_option = TSC.RequestOptions() req_option.filter.add(TSC.Filter("contentUrl", TSC.RequestOptions.Operator.Equals, workbook_content_url)) + logger.trace(req_option.get_query_params()) matching_workbooks, paging = server.workbooks.get(req_option) except TSC.ServerResponseError as e: Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response").format("")) @@ -43,6 +45,20 @@ def get_wb_by_content_url(logger, server, workbook_content_url) -> TSC.WorkbookI Errors.exit_with_error(logger, message=_("dataalerts.failure.error.workbookNotFound")) return matching_workbooks[0] + @staticmethod + def get_ds_by_content_url(logger, server, datasource_content_url) -> TSC.DatasourceItem: + logger.debug(_("export.status").format(datasource_content_url)) + try: + req_option = TSC.RequestOptions() + req_option.filter.add(TSC.Filter("contentUrl", TSC.RequestOptions.Operator.Equals, datasource_content_url)) + logger.trace(req_option.get_query_params()) + matching_datasources, paging = server.datasources.get(req_option) + except TSC.ServerResponseError as e: + Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response").format("")) + if len(matching_datasources) < 1: + Errors.exit_with_error(logger, message=_("dataalerts.failure.error.datasourceNotFound")) + return matching_datasources[0] + @staticmethod def apply_values_from_url_params(request_options: TSC.PDFRequestOptions, url, logger) -> None: # should be able to replace this with request_options._append_view_filters(params) @@ -63,7 +79,7 @@ def apply_values_from_url_params(request_options: TSC.PDFRequestOptions, url, lo else: # it must be a filter DatasourcesAndWorkbooks.apply_filter_value(request_options, value, logger) - except BaseException as e: + except Exception as e: logger.warn("Error building filter params", e) # ExportCommand.log_stack(logger) # type: ignore @@ -86,7 +102,7 @@ def apply_option_value(request_options: TSC.PDFRequestOptions, value: str, logge logger.debug("Set max age to {} from {}".format(request_options.max_age, value)) elif ":size" == setting[0]: height, width = setting[1].split(",") - logger.warn("Height/weight parameters not yet implemented ({})".format(value)) + logger.warn("Height/width parameters not yet implemented ({})".format(value)) else: logger.debug("Parameter[s] not recognized: {}".format(value)) @@ -122,16 +138,3 @@ def save_to_file(logger, output, filename): with open(filename, "wb") as f: f.write(output) logger.info(_("export.success").format("", filename)) - - @staticmethod - def get_ds_by_content_url(logger, server, datasource_content_url) -> TSC.DatasourceItem: - logger.debug(_("export.status").format(datasource_content_url)) - try: - req_option = TSC.RequestOptions() - req_option.filter.add(TSC.Filter("contentUrl", TSC.RequestOptions.Operator.Equals, datasource_content_url)) - matching_datasources, paging = server.datasources.get(req_option) - except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response").format("")) - if len(matching_datasources) < 1: - Errors.exit_with_error(logger, message=_("dataalerts.failure.error.datasourceNotFound")) - return matching_datasources[0] diff --git a/tabcmd/commands/datasources_and_workbooks/get_url_command.py b/tabcmd/commands/datasources_and_workbooks/get_url_command.py index 1fb3139d..e4ec7faf 100644 --- a/tabcmd/commands/datasources_and_workbooks/get_url_command.py +++ b/tabcmd/commands/datasources_and_workbooks/get_url_command.py @@ -1,3 +1,5 @@ +import inspect + import tableauserverclient as TSC from tableauserverclient import ServerResponseError @@ -60,7 +62,7 @@ def run_command(args): GetUrl.get_content_as_file(file_type, content_type, logger, args, server, url) except ServerResponseError as e: Errors.exit_with_error(logger, e) - except BaseException as be: + except Exception as be: Errors.exit_with_error(logger, be, "Error during file download") ## this first set of methods is all parsing the url and file input from the user @@ -77,12 +79,14 @@ def evaluate_content_type(logger, url): Errors.exit_with_error(logger, message=_("get.errors.invalid_content_type").format(url)) @staticmethod - def explain_expected_url(logger, url): + def explain_expected_url(logger, url: str, command: str): view_example = "/views//[.ext]" wb_example = "/workbooks/[.ext]" ds_example = "/datasources/ "wb-name", datasource/ds-name -> ds-name + def get_resource_name(url: str, logger): # workbooks/wb-name" -> "wb-name", datasource/ds-name -> ds-name + url = url.lstrip("/") # strip opening / if present name_parts = url.split("/") if len(name_parts) != 2: - GetUrl.explain_expected_url(logger, url) + GetUrl.explain_expected_url(logger, url, "GetURl") resource_name_with_params = name_parts[::-1][0] # last part resource_name_with_ext = GetUrl.strip_query_params(resource_name_with_params) resource_name = GetUrl.get_name_without_possible_extension(resource_name_with_ext) @@ -136,7 +141,7 @@ def get_resource_name(url, logger): # workbooks/wb-name" -> "wb-name", datasour def get_view_url(url, logger): # "views/wb-name/view-name" -> wb-name/sheets/view-name name_parts = url.split("/") # ['views', 'wb-name', 'view-name'] if len(name_parts) != 3: - GetUrl.explain_expected_url(logger, url) + GetUrl.explain_expected_url(logger, url, "GetURl") workbook_name = name_parts[1] view_name = name_parts[::-1][0] view_name = GetUrl.strip_query_params(view_name) @@ -171,6 +176,7 @@ def get_content_as_file(file_type, content_type, logger, args, server, url): @staticmethod def generate_pdf(logger, server, args, view_url): + logger.trace("Entered method " + inspect.stack()[0].function) try: view_item: TSC.ViewItem = GetUrl.get_view_by_content_url(logger, server, view_url) logger.debug(_("content_type.view") + ": {}".format(view_item.name)) @@ -184,6 +190,7 @@ def generate_pdf(logger, server, args, view_url): @staticmethod def generate_png(logger, server, args, view_url): + logger.trace("Entered method " + inspect.stack()[0].function) try: view_item: TSC.ViewItem = GetUrl.get_view_by_content_url(logger, server, view_url) logger.debug(_("content_type.view") + ": {}".format(view_item.name)) @@ -197,6 +204,7 @@ def generate_png(logger, server, args, view_url): @staticmethod def generate_csv(logger, server, args, view_url): + logger.trace("Entered method " + inspect.stack()[0].function) try: view_item: TSC.ViewItem = GetUrl.get_view_by_content_url(logger, server, view_url) logger.debug(_("content_type.view") + ": {}".format(view_item.name)) @@ -212,6 +220,7 @@ def generate_csv(logger, server, args, view_url): @staticmethod def generate_twb(logger, server, args, file_extension, url): + logger.trace("Entered method " + inspect.stack()[0].function) workbook_name = GetUrl.get_resource_name(url, logger) try: target_workbook = GetUrl.get_wb_by_content_url(logger, server, workbook_name) @@ -225,7 +234,8 @@ def generate_twb(logger, server, args, file_extension, url): @staticmethod def generate_tds(logger, server, args, file_extension): - datasource_name = GetUrl.get_resource_name(logger, args.url) + logger.trace("Entered method " + inspect.stack()[0].function) + datasource_name = GetUrl.get_resource_name(args.url, logger) try: target_datasource = GetUrl.get_ds_by_content_url(logger, server, datasource_name) logger.debug(_("content_type.datasource") + ": {}".format(datasource_name)) diff --git a/tabcmd/commands/datasources_and_workbooks/publish_command.py b/tabcmd/commands/datasources_and_workbooks/publish_command.py index c529604b..93a6fe0b 100644 --- a/tabcmd/commands/datasources_and_workbooks/publish_command.py +++ b/tabcmd/commands/datasources_and_workbooks/publish_command.py @@ -51,15 +51,31 @@ def run_command(args): args.project_name = "default" args.parent_project_path = "" - publish_mode = PublishCommand.get_publish_mode(args) + publish_mode = PublishCommand.get_publish_mode(args) # --overwrite, --replace logger.info("Publishing as " + publish_mode) + if args.db_username: + creds = TSC.models.ConnectionCredentials(args.db_username, args.db_password, embed=args.save_db_password) + elif args.oauth_username: + creds = TSC.models.ConnectionCredentials(args.oauth_username, None, embed=False, oauth=args.save_oauth) + else: + logger.debug("No db-username or oauth-username found in command") + creds = None + source = PublishCommand.get_filename_extension_if_tableau_type(logger, args.filename) logger.info(_("publish.status").format(args.filename)) if source in ["twbx", "twb"]: new_workbook = TSC.WorkbookItem(project_id, name=args.name, show_tabs=args.tabbed) try: - new_workbook = server.workbooks.publish(new_workbook, args.filename, publish_mode) + new_workbook = server.workbooks.publish( + new_workbook, + args.filename, + publish_mode, + connection_credentials=creds, + as_job=False, + skip_connection_check=False, + ) + except IOError as ioe: Errors.exit_with_error(logger, ioe) logger.info(_("publish.success") + "\n{}".format(new_workbook.webpage_url)) @@ -68,7 +84,9 @@ def run_command(args): new_datasource = TSC.DatasourceItem(project_id, name=args.name) new_datasource.use_remote_query_agent = args.use_tableau_bridge try: - new_datasource = server.datasources.publish(new_datasource, args.filename, publish_mode) + new_datasource = server.datasources.publish( + new_datasource, args.filename, publish_mode, connection_credentials=creds + ) except IOError as ioe: Errors.exit_with_error(logger, exc) logger.info(_("publish.success") + "\n{}".format(new_datasource.webpage_url)) diff --git a/tabcmd/commands/server.py b/tabcmd/commands/server.py index bde8c624..f215e3a8 100644 --- a/tabcmd/commands/server.py +++ b/tabcmd/commands/server.py @@ -45,10 +45,10 @@ def find_user(logger, server, username): @staticmethod def get_items_by_name(logger, item_endpoint, item_name: str, container: Optional[TSC.ProjectItem] = None) -> List: # TODO: typing should reflect that this returns TSC.TableauItem and item_endpoint is of type TSC.QuerysetEndpoint[same] - item_type = type(item_endpoint).__name__ - item_log_name: str = "[" + item_type + "] " + item_name + item_log_name: str = "[{0}] {1}".format(type(item_endpoint).__name__, item_name) if container: - item_log_name = str(container) + "/" + item_log_name + container_name: str = "({0}) {1}".format(container.__class__, container.name) + item_log_name = "{0}/{1}".format(container_name, item_log_name) logger.debug(_("export.status").format(item_log_name)) req_option = TSC.RequestOptions() req_option.filter.add(TSC.Filter(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Operator.Equals, item_name)) diff --git a/tabcmd/commands/site/delete_site_command.py b/tabcmd/commands/site/delete_site_command.py index 2d7599da..854c4dd9 100644 --- a/tabcmd/commands/site/delete_site_command.py +++ b/tabcmd/commands/site/delete_site_command.py @@ -33,7 +33,7 @@ def run_command(args): server.sites.delete(target_site_id) except TSC.ServerResponseError as e: Errors.exit_with_error(logger, strings[1], e) - except BaseException as e: + except Exception as e: Errors.exit_with_error(logger, strings[4], e) logger.info(strings[0].format(args.site_name_to_delete)) diff --git a/tabcmd/execution/global_options.py b/tabcmd/execution/global_options.py index 0e66cd3c..61de456b 100644 --- a/tabcmd/execution/global_options.py +++ b/tabcmd/execution/global_options.py @@ -291,10 +291,15 @@ def set_publish_args(parser): parser.add_argument("-n", "--name", help="Name to publish the new datasource or workbook by.") set_overwrite_option(parser) - parser.add_argument( + + creds = parser.add_mutually_exclusive_group() + creds.add_argument("--oauth-username", help="The email address of a preconfigured OAuth connection") + creds.add_argument( "--db-username", help="Use this option to publish a database user name with the workbook, data source, or data extract.", ) + parser.add_argument("--save-oauth", action="store_true", help="Save embedded OAuth credentials in the datasource") + parser.add_argument( "--db-password", help="publish a database password with the workbook, data source, or extract", @@ -304,6 +309,7 @@ def set_publish_args(parser): action="store_true", help="Stores the provided database password on the server.", ) + parser.add_argument( "--tabbed", action="store_true", @@ -320,14 +326,11 @@ def set_publish_args(parser): action="store_true", help="Encrypt extracts in the workbook, datasource, or extract being published to the server", ) - parser.add_argument("--oauth-username", help="The email address of a preconfigured OAuth connection") - parser.add_argument("--save-oauth", action="store_true", help="Save embedded OAuth credentials in the datasource") - thumbnails = parser.add_mutually_exclusive_group() thumbnails.add_argument("--thumbnail-username", help="Not yet implemented") thumbnails.add_argument("--thumbnail-group", help="Not yet implemented") # not implemented in the REST API - parser.add_argument("--use-tableau-bridge", help="Refresh datasource through Tableau Bridge") + parser.add_argument("--use-tableau-bridge", action="store_true", help="Refresh datasource through Tableau Bridge") def set_overwrite_option(parser): diff --git a/tests/commands/test_run_commands.py b/tests/commands/test_run_commands.py index a7e03597..7a1afbe2 100644 --- a/tests/commands/test_run_commands.py +++ b/tests/commands/test_run_commands.py @@ -138,6 +138,8 @@ def test_publish(self, mock_session, mock_server): mock_args.parent_project_path = "projects" mock_args.name = "" mock_args.tabbed = True + mock_args.db_username = None + mock_args.oauth_username = None mock_server.projects = getter publish_command.PublishCommand.run_command(mock_args) mock_session.assert_called() diff --git a/tests/e2e/online_tests.py b/tests/e2e/online_tests.py index 236820f5..abd10b71 100644 --- a/tests/e2e/online_tests.py +++ b/tests/e2e/online_tests.py @@ -4,6 +4,7 @@ import time import unittest +from credentials import waremart_password, waremart_user from tests.e2e import setup_e2e debug_log = "--logging-level=DEBUG" @@ -70,15 +71,28 @@ def _publish_samples(self, project_name): arguments = [command, "--name", project_name] _test_command(arguments) - def _publish_wb(self, file, name): + def _publish_args(self, file, name, tabbed=None): command = "publish" arguments = [command, file, "--name", name, "--overwrite"] - return _test_command(arguments) - - def _publish_ds(self, file, name): - command = "publish" - arguments = [command, file, "--name", name, "--overwrite"] - return _test_command(arguments) + return arguments + + def _publish_creds_args( + self, arguments, db_user=None, db_pass=None, db_save=None, oauth_user=None, oauth_save=None + ): + if db_user: + arguments.append("--db-username") + arguments.append(db_user) + if db_pass: + arguments.append("--db-password") + arguments.append(db_pass) + if db_save: + arguments.append("--save-db-password") + if oauth_user: + arguments.append("--oauth-username") + arguments.append(oauth_user) + if oauth_save: + arguments.append("--save-oauth") + return arguments def _delete_wb(self, file): command = "delete" @@ -144,6 +158,7 @@ def _list(self, item_type: str): TWBX_WITHOUT_EXTRACT_SHEET = "Testsheet1" TDSX_WITH_EXTRACT_NAME = "WorldIndicators" TDSX_FILE_WITH_EXTRACT = "World Indicators.tdsx" + TWB_WITH_EMBEDDED_CONNECTION = "embedded_connection_waremart.twb" @pytest.mark.order(1) def test_login(self): @@ -250,7 +265,8 @@ def test_delete_projects(self): def test_wb_publish(self): name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME file = os.path.join("tests", "assets", OnlineCommandTest.TWBX_FILE_WITH_EXTRACT) - self._publish_wb(file, name_on_server) + arguments = self._publish_args(file, name_on_server) + _test_command(arguments) @pytest.mark.order(10) def test_wb_get(self): @@ -279,11 +295,20 @@ def test_wb_delete(self): name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME self._delete_wb(name_on_server) + @pytest.mark.order(11) + def test_wb_publish_embedded(self): + name_on_server = OnlineCommandTest.TWB_WITH_EMBEDDED_CONNECTION + file = os.path.join("tests", "assets", OnlineCommandTest.TWB_WITH_EMBEDDED_CONNECTION) + arguments = self._publish_args(file, name_on_server) + arguments = self._publish_creds_args(arguments, waremart_user, waremart_password, True) + _test_command(arguments) + @pytest.mark.order(12) def test_publish_ds(self): name_on_server = OnlineCommandTest.TDSX_WITH_EXTRACT_NAME file = os.path.join("tests", "assets", OnlineCommandTest.TDSX_FILE_WITH_EXTRACT) - self._publish_ds(file, name_on_server) + arguments = self._publish_args(file, name_on_server) + _test_command(arguments) @pytest.mark.order(13) def test__get_ds(self): @@ -300,7 +325,7 @@ def test_delete_extract(self): # fails because the extract has a bad data connection :/ name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME file = os.path.join("tests", "assets", OnlineCommandTest.TWBX_FILE_WITH_EXTRACT) - self._publish_wb(file, name_on_server) + self._publish_args(file, name_on_server) self._delete_extract(name_on_server) @pytest.mark.order(16) @@ -314,7 +339,8 @@ def test_refresh_extract(self): # must be a datasource owned by the test user name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME file = os.path.join("tests", "assets", OnlineCommandTest.TWBX_FILE_WITH_EXTRACT) - self._publish_wb(file, name_on_server) + arguments = self._publish_args(file, name_on_server) + _test_command(arguments) name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME self._refresh_extract(name_on_server) @@ -324,7 +350,9 @@ def test_refresh_extract(self): def test_export_wb_pdf(self): name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME file = os.path.join("tests", "assets", OnlineCommandTest.TWBX_FILE_WITH_EXTRACT) - self._publish_wb(file, name_on_server) + arguments = self._publish_args(file, name_on_server) + _test_command(arguments) + command = "export" friendly_name = name_on_server + "/" + OnlineCommandTest.TWBX_WITH_EXTRACT_SHEET arguments = [command, friendly_name, "--fullpdf", "-f", "exported_wb.pdf"] @@ -334,7 +362,8 @@ def test_export_wb_pdf(self): def test_export_view_pdf(self): name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME file = os.path.join("tests", "assets", OnlineCommandTest.TWBX_FILE_WITH_EXTRACT) - self._publish_wb(file, name_on_server) + arguments = self._publish_args(file, name_on_server) + _test_command(arguments) command = "export" friendly_name = name_on_server + "/" + OnlineCommandTest.TWBX_WITH_EXTRACT_SHEET + "?param1=3" arguments = [command, friendly_name, "--pdf", "-f", "exported_view.pdf"]