diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 2b46a3e6..39324cbf 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -57,7 +57,7 @@ jobs: python -m pip install --upgrade pip python -m pip install --upgrade build pip install . - pip install .[test] + pip install .[package] doit version python -m build diff --git a/.gitignore b/.gitignore index d586d56e..4f38905a 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,4 @@ site-packages # local tabcmd-dev workon +test.junit.xml diff --git a/README.md b/README.md index c22552aa..e1703650 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ To run tabcmd from your local copy, from a console window in the same directory > mypy tabcmd tests - packaging is done with pyinstaller. You can only build an executable for the platform you build on. +- To package a release, we first bump the version with `doit version` and build as 2.x.0 before packaging > pyinstaller tabcmd\tabcmd.py --clean --noconfirm produces dist/tabcmd.exe @@ -113,3 +114,4 @@ Copies of tabcmd that shipped with Tableau Server are referred to by the version At some point in the future, tabcmd will no longer be included with Tableau Server. *We have no intention of breaking Server install flows.* If you have specific suggestions or concerns on what that will look like, feel free to open an issue here or a thread on the Community Forums. + diff --git a/World Indicators.tdsx b/World Indicators.tdsx new file mode 100644 index 00000000..7c8e53db Binary files /dev/null and b/World Indicators.tdsx differ diff --git a/pyproject.toml b/pyproject.toml index 77c00308..72a1bf27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,7 @@ test = [ "pytest-runner", "requests-mock>=1.0,<2.0"] localize = ["doit", "ftfy"] -package = ["pyinstaller>=5.1"] +package = ["pyinstaller>=5.1", "doit"] [project.urls] repository = "https://github.com/tableau/tabcmd" [project.scripts] diff --git a/tabcmd.spec b/tabcmd.spec index a1e0421c..e28d4ca3 100644 --- a/tabcmd.spec +++ b/tabcmd.spec @@ -1,14 +1,26 @@ # -*- mode: python ; coding: utf-8 -*- - block_cipher = None +localized_strings = [ + ('tabcmd/locales/en/LC_MESSAGES/tabcmd.mo', 'tabcmd/locales/en/LC_MESSAGES'), + ('tabcmd/locales/de/LC_MESSAGES/tabcmd.mo', 'tabcmd/locales/de/LC_MESSAGES'), + ('tabcmd/locales/es/LC_MESSAGES/tabcmd.mo', 'tabcmd/locales/es/LC_MESSAGES'), + ('tabcmd/locales/fr/LC_MESSAGES/tabcmd.mo', 'tabcmd/locales/fr/LC_MESSAGES'), + ('tabcmd/locales/ga/LC_MESSAGES/tabcmd.mo', 'tabcmd/locales/ga/LC_MESSAGES'), + ('tabcmd/locales/it/LC_MESSAGES/tabcmd.mo', 'tabcmd/locales/it/LC_MESSAGES'), + ('tabcmd/locales/ja/LC_MESSAGES/tabcmd.mo', 'tabcmd/locales/ja/LC_MESSAGES'), + ('tabcmd/locales/ko/LC_MESSAGES/tabcmd.mo', 'tabcmd/locales/ko/LC_MESSAGES'), + ('tabcmd/locales/pt/LC_MESSAGES/tabcmd.mo', 'tabcmd/locales/pt/LC_MESSAGES'), + ('tabcmd/locales/sv/LC_MESSAGES/tabcmd.mo', 'tabcmd/locales/sv/LC_MESSAGES'), + ('tabcmd/locales/zh/LC_MESSAGES/tabcmd.mo', 'tabcmd/locales/zh/LC_MESSAGES'), + ] a = Analysis( ['tabcmd\\tabcmd.py'], pathex=[], binaries=[], - datas=[], + datas=localized_strings, hiddenimports=['tableauserverclient', 'requests.packages.urllib3', 'pkg_resources'], hookspath=[], hooksconfig={}, @@ -24,13 +36,17 @@ pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) exe = EXE( pyz, a.scripts, + a.binaries, + a.zipfiles, + a.datas, [], - exclude_binaries=True, name='tabcmd', debug=False, bootloader_ignore_signals=False, strip=False, upx=True, + upx_exclude=[], + runtime_tmpdir=None, console=True, disable_windowed_traceback=False, argv_emulation=False, @@ -38,13 +54,3 @@ exe = EXE( codesign_identity=None, entitlements_file=None, ) -coll = COLLECT( - exe, - a.binaries, - a.zipfiles, - a.datas, - strip=False, - upx=True, - upx_exclude=[], - name='tabcmd', -) diff --git a/tabcmd/__main__.py b/tabcmd/__main__.py index 29886963..97a969ee 100644 --- a/tabcmd/__main__.py +++ b/tabcmd/__main__.py @@ -3,8 +3,8 @@ try: from tabcmd.tabcmd import main except ImportError: - print("Tabcmd needs to be run as a module, it cannot be run as a script") - print("Try running python -m tabcmd") + print(sys.stderr, "Tabcmd needs to be run as a module, it cannot be run as a script") + print(sys.stderr, "Try running python -m tabcmd") sys.exit(1) if __name__ == "__main__": diff --git a/tabcmd/commands/auth/session.py b/tabcmd/commands/auth/session.py index 805ad387..d0f970a4 100644 --- a/tabcmd/commands/auth/session.py +++ b/tabcmd/commands/auth/session.py @@ -54,11 +54,16 @@ def _update_session_data(self, args): # user id and site id are never passed in as args # last_login_using and tableau_server are internal data # self.command = args.??? - self.username = args.username or self.username + self.username = args.username or self.username or "" + self.username = self.username.lower() + self.server_url = args.server or self.server_url or "http://localhost" + self.server_url = self.server_url.lower() + if args.server is not None: + self.site_name = None self.site_name = args.site_name or self.site_name or "" + self.site_name = self.site_name.lower() if self.site_name == "default": self.site_name = "" - self.server_url = args.server or self.server_url or "http://localhost" self.logging_level = args.logging_level or self.logging_level self.password_file = args.password_file self.token_name = args.token_name or self.token_name @@ -190,10 +195,9 @@ def _sign_in(self, tableau_auth): if not self.username: self.username = self.tableau_server.users.get_by_id(self.user_id).name self.logger.info(_("common.output.succeeded")) - except TSC.ServerResponseError as e: - Errors.exit_with_error(self.logger, _("publish.errors.unexpected_server_response"), e) + except Exception as e: + Errors.exit_with_error(self.logger, e) self.logger.debug("Signed into {0}{1} as {2}".format(self.server_url, self.site_name, self.username)) - return self.tableau_server def _get_saved_credentials(self): diff --git a/tabcmd/commands/constants.py b/tabcmd/commands/constants.py index 0eaa9b3c..e78daf36 100644 --- a/tabcmd/commands/constants.py +++ b/tabcmd/commands/constants.py @@ -1,6 +1,8 @@ import inspect import sys +from tableauserverclient import ServerResponseError + from tabcmd.execution.localize import _ @@ -30,6 +32,10 @@ def is_login_error(error): if hasattr(error, "code"): return error.code == Constants.login_error + @staticmethod + def is_server_response_error(error): + return isinstance(error, ServerResponseError) + # https://gist.github.com/FredLoney/5454553 @staticmethod def log_stack(logger): @@ -44,10 +50,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 @@ -57,19 +64,29 @@ def exit_with_error(logger, message=None, exception=None): if message and not exception: logger.error(message) if exception: - if Errors.is_expired_session(exception): - logger.error(_("session.errors.session_expired")) - # TODO: add session as an argument to this method - # and add the full command line as a field in Session? - # "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(sys.stderr, "Error during log call from exception - {}".format(exc)) + try: + logger.info("Exiting...") + except Exception: + print(sys.stderr, "Exiting...") sys.exit(1) @staticmethod - def check_common_error_codes_and_explain(logger, error): - logger.error(error) + def check_common_error_codes_and_explain(logger, exception): + if Errors.is_server_response_error(exception): + logger.error(_("publish.errors.unexpected_server_response").format(exception)) + if Errors.is_expired_session(exception): + logger.error(_("session.errors.session_expired")) + # TODO: add session as an argument to this method + # and add the full command line as a field in Session? + # "session.session_expired_login")) + # session.renew_session + return + if exception.code.startswith(Constants.source_not_found): + logger.error(_("publish.errors.server_resource_not_found"), exception) + else: + logger.error(exception) 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 1eadb3a5..1d345a12 100644 --- a/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py +++ b/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py @@ -23,9 +23,10 @@ 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)) + except Exception as e: + Errors.exit_with_error(logger, e) if len(matching_views) < 1: Errors.exit_with_error(logger, message=_("errors.xmlapi.not_found")) return matching_views[0] @@ -36,13 +37,28 @@ 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("")) + except Exception as e: + Errors.exit_with_error(logger, e) if len(matching_workbooks) < 1: 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 Exception as e: + Errors.exit_with_error(logger, e) + 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)) diff --git a/tabcmd/commands/datasources_and_workbooks/delete_command.py b/tabcmd/commands/datasources_and_workbooks/delete_command.py index 8e68cb41..e28f5f45 100644 --- a/tabcmd/commands/datasources_and_workbooks/delete_command.py +++ b/tabcmd/commands/datasources_and_workbooks/delete_command.py @@ -21,10 +21,11 @@ class DeleteCommand(DatasourcesAndWorkbooks): @staticmethod def define_args(delete_parser): - delete_parser.add_argument("name", help=_("content_type.workbook") + "/" + _("content_type.datasource")) - set_ds_xor_wb_options(delete_parser) - set_project_r_arg(delete_parser) - set_parent_project_arg(delete_parser) + group = delete_parser.add_argument_group(title=DeleteCommand.name) + group.add_argument("name", help=_("content_type.workbook") + "/" + _("content_type.datasource")) + set_ds_xor_wb_options(group) + set_project_r_arg(group) + set_parent_project_arg(group) @staticmethod def run_command(args): @@ -72,5 +73,5 @@ def run_command(args): else: server.datasources.delete(item_to_delete.id) logger.info(_("common.output.succeeded")) - except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, "Error deleting from server", e) + except Exception as e: + Errors.exit_with_error(logger, e) diff --git a/tabcmd/commands/datasources_and_workbooks/export_command.py b/tabcmd/commands/datasources_and_workbooks/export_command.py index 428c530f..f652bcd6 100644 --- a/tabcmd/commands/datasources_and_workbooks/export_command.py +++ b/tabcmd/commands/datasources_and_workbooks/export_command.py @@ -16,20 +16,23 @@ class ExportCommand(DatasourcesAndWorkbooks): @staticmethod def define_args(export_parser): - export_parser.add_argument("url", help="url of the workbook or view to export") - export_parser_group = export_parser.add_mutually_exclusive_group(required=True) + group = export_parser.add_argument_group(title=ExportCommand.name) + group.add_argument("url", help="url of the workbook or view to export") + export_parser_group = group.add_mutually_exclusive_group(required=True) export_parser_group.add_argument("--pdf", action="store_true", help=_("export.options.pdf")) export_parser_group.add_argument("--fullpdf", action="store_true", help=_("export.options.fullpdf")) export_parser_group.add_argument("--png", action="store_true", help=_("export.options.png")) export_parser_group.add_argument("--csv", action="store_true", help=_("export.options.csv")) - export_parser.add_argument( + group.add_argument( "--pagelayout", choices=["landscape", "portrait"], + type=str.lower, default=None, help="page orientation (landscape or portrait) of the exported PDF", ) - export_parser.add_argument( + group.add_argument( + "--pagesize", choices=[ pagesize.A3, @@ -47,16 +50,15 @@ def define_args(export_parser): pagesize.Tabloid, pagesize.Unspecified, ], + type=str.lower, default="letter", help="Set the page size of the exported PDF", ) - export_parser.add_argument( - "--width", default=800, help="Set the width of the image in pixels. Default is 800 px" - ) - export_parser.add_argument("--filename", "-f", help="filename to store the exported data") - export_parser.add_argument("--height", default=600, help=_("export.options.height")) - export_parser.add_argument( + group.add_argument("--width", default=800, help="Set the width of the image in pixels. Default is 800 px") + group.add_argument("--filename", "-f", help="filename to store the exported data") + group.add_argument("--height", default=600, help=_("export.options.height")) + group.add_argument( "--filter", metavar="COLUMN:VALUE", help="View filter to apply to the view", @@ -113,7 +115,6 @@ def run_command(args): else: ExportCommand.save_to_file(logger, output, save_name) - except Exception as e: Errors.exit_with_error(logger, "Error saving to file", e) diff --git a/tabcmd/commands/datasources_and_workbooks/get_url_command.py b/tabcmd/commands/datasources_and_workbooks/get_url_command.py index be0d96a0..2428f569 100644 --- a/tabcmd/commands/datasources_and_workbooks/get_url_command.py +++ b/tabcmd/commands/datasources_and_workbooks/get_url_command.py @@ -1,4 +1,7 @@ +import inspect + import tableauserverclient as TSC +from tableauserverclient import ServerResponseError from tabcmd.commands.auth.session import Session from tabcmd.commands.constants import Errors @@ -16,11 +19,14 @@ class GetUrl(DatasourcesAndWorkbooks): name: str = "get" description: str = _("get.short_description") + valid_file_types = {"workbook": ["twbx", "twb"], "datasource": ["tdsx", "tds"], "view": ["pdf", "png", "csv"]} + valid_content_types = ["workbook", "view", "datasource"] @staticmethod def define_args(get_url_parser): - get_url_parser.add_argument("url", help=_("refreshextracts.options.url")) - set_filename_arg(get_url_parser) + group = get_url_parser.add_argument_group(title=GetUrl.name) + group.add_argument("url", help=_("refreshextracts.options.url")) + set_filename_arg(group) # these don't need arguments, although that would be a good future addition # tabcmd get "/views/Finance/InvestmentGrowth.png?:size=640,480" -f growth.png # tabcmd get "/views/Finance/InvestmentGrowth.png?:refresh=yes" -f growth.png @@ -30,6 +36,8 @@ def run_command(args): # A view can be returned in PDF, PNG, or CSV (summary data only) format. # A Tableau workbook is returned as a TWB if it connects to a datasource/live connection, # or a TWBX if it uses an extract. + # A Tableau datasource is returned as a TDS if it connects to a live connection, + # or a TDSX if it uses an extract. logger = log(__class__.__name__, args.logging_level) logger.debug(_("tabcmd.launching")) session = Session() @@ -39,47 +47,32 @@ def run_command(args): url = args.url.lstrip("/") # strip opening / if present - file_type = GetUrl.get_file_type_from_filename(logger, args.filename, url) - content_type = GetUrl.evaluate_content_type(logger, url) - - if content_type == "workbook": - if file_type in ["twbx", "twb"]: - GetUrl.generate_twb(logger, server, args, file_type, url) - else: - Errors.exit_with_error( - logger, message=_("publish.errors.mutually_exclusive_option").format("twb", "twbx") - ) - else: # content type = view - view_url = GetUrl.get_view_url(url, logger) - if file_type == "pdf": - GetUrl.generate_pdf(logger, server, args, view_url) - elif file_type == "png": - GetUrl.generate_png(logger, server, args, view_url) - elif file_type == "csv": - GetUrl.generate_csv(logger, server, args, view_url) - else: - Errors.exit_with_error(logger, message=_("tabcmd.get.extension.not_found")) + ## this first set of methods is all parsing the url and file input from the user @staticmethod def evaluate_content_type(logger, url): - # specify a view to get using "views//." - # specify a workbook to get using "workbooks/.". - if url.startswith("views/"): - return "view" - elif url.startswith("workbooks/"): - return "workbook" - else: - GetUrl.explain_expected_url(logger) - - @staticmethod - def explain_expected_url(logger): - view_example = "/views//." - message = "{} [{}]".format( - _("export.errors.requires_workbook_view_param").format(__class__.__name__), view_example + # specify a view to get using "/views//." + # specify a workbook to get using "/workbooks/.". + # specify a datasource to get using "/datasources/." + content_type = "" + for content_type in GetUrl.valid_content_types: + if url.find(content_type) == 0: + return content_type + Errors.exit_with_error(logger, message=_("get.errors.invalid_content_type").format(url)) + + + @staticmethod + def explain_expected_url(logger, url: str, command: str): + view_example = "/views//[.ext]" + wb_example = "/workbooks/[.ext]" + ds_example = "/datasources/ "wb-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) - workbook_name = name_parts[::-1][0] # last part - workbook_name = GetUrl.strip_query_params(workbook_name) - workbook_name = GetUrl.get_name_without_possible_extension(workbook_name) - return workbook_name + 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) + return resource_name @staticmethod 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) + 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) @@ -143,8 +137,29 @@ def filename_from_args(file_argument, item_name, filetype): file_argument = "{}.{}".format(item_name, filetype) return file_argument + ## methods below here have done all the parsing and just have to do the download and saving + ## these should be able to be shared with export + + @staticmethod + def get_content_as_file(file_type, content_type, logger, args, server, url): + if content_type == "workbook": + return GetUrl.generate_twb(logger, server, args, file_type, url) + elif content_type == "datasource": + return GetUrl.generate_tds(logger, server, args, file_type) + elif content_type == "view": + view_url = GetUrl.get_view_url(url, logger) + if file_type == "pdf": + return GetUrl.generate_pdf(logger, server, args, view_url) + elif file_type == "png": + return GetUrl.generate_png(logger, server, args, view_url) + elif file_type == "csv": + return GetUrl.generate_csv(logger, server, args, view_url) + # all the known options above will return early. If we get here we are confused. + Errors.exit_with_error(logger, message=_("tabcmd.get.extension.not_found")) + @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)) @@ -153,11 +168,12 @@ def generate_pdf(logger, server, args, view_url): server.views.populate_pdf(view_item, req_option_pdf) filename = GetUrl.filename_from_args(args.filename, view_item.name, "pdf") DatasourcesAndWorkbooks.save_to_file(logger, view_item.pdf, filename) - except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) + except Exception as e: + Errors.exit_with_error(logger, e) @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)) @@ -166,11 +182,12 @@ def generate_png(logger, server, args, view_url): server.views.populate_image(view_item, req_option_csv) filename = GetUrl.filename_from_args(args.filename, view_item.name, "png") DatasourcesAndWorkbooks.save_to_file(logger, view_item.image, filename) - except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) + except Exception as e: + Errors.exit_with_error(logger, e) @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)) @@ -179,15 +196,13 @@ def generate_csv(logger, server, args, view_url): server.views.populate_csv(view_item, req_option_csv) file_name_with_path = GetUrl.filename_from_args(args.filename, view_item.name, "csv") DatasourcesAndWorkbooks.save_to_data_file(logger, view_item.csv, file_name_with_path) - except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) except Exception as e: Errors.exit_with_error(logger, exception=e) @staticmethod def generate_twb(logger, server, args, file_extension, url): - workbook_name = GetUrl.get_workbook_name(url, logger) - + 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) logger.debug(_("content_type.workbook") + ": {}".format(workbook_name)) @@ -195,5 +210,19 @@ def generate_twb(logger, server, args, file_extension, url): logger.debug("Saving as {}".format(file_name_with_path)) server.workbooks.download(target_workbook.id, filepath=None, no_extract=False) logger.info(_("export.success").format(target_workbook.name, file_name_with_path)) - except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) + except Exception as e: + Errors.exit_with_error(logger, e) + + @staticmethod + def generate_tds(logger, server, args, file_extension): + 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)) + file_name_with_path = GetUrl.filename_from_args(args.filename, datasource_name, file_extension) + logger.debug("Saving as {}".format(file_name_with_path)) + server.datasources.download(target_datasource.id, filepath=None, no_extract=False) + logger.info(_("export.success").format(target_datasource.name, file_name_with_path)) + except Exception as e: + Errors.exit_with_error(logger, e) diff --git a/tabcmd/commands/datasources_and_workbooks/publish_command.py b/tabcmd/commands/datasources_and_workbooks/publish_command.py index 59432ee1..c4866bbd 100644 --- a/tabcmd/commands/datasources_and_workbooks/publish_command.py +++ b/tabcmd/commands/datasources_and_workbooks/publish_command.py @@ -1,4 +1,5 @@ import tableauserverclient as TSC +from tableauserverclient import ServerResponseError from tabcmd.commands.auth.session import Session from tabcmd.commands.constants import Errors @@ -20,14 +21,15 @@ class PublishCommand(DatasourcesAndWorkbooks): @staticmethod def define_args(publish_parser): - publish_parser.add_argument( + group = publish_parser.add_argument_group(title=PublishCommand.name) + group.add_argument( "filename", metavar="filename.twbx|tdsx|hyper", # this is not actually a File type because we just pass the path to tsc ) - set_publish_args(publish_parser) - set_project_r_arg(publish_parser) - set_parent_project_arg(publish_parser) + set_publish_args(group) + set_project_r_arg(group) + set_parent_project_arg(group) @staticmethod def run_command(args): @@ -50,25 +52,42 @@ 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) - except IOError as ioe: - Errors.exit_with_error(logger, ioe) + new_workbook = server.workbooks.publish( + new_workbook, + args.filename, + publish_mode, + connection_credentials=creds, + as_job=False, + skip_connection_check=False, + ) + except Exception as e: + Errors.exit_with_error(logger, e) logger.info(_("publish.success") + "\n{}".format(new_workbook.webpage_url)) elif source in ["tds", "tdsx", "hyper"]: 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) - except IOError as ioe: + new_datasource = server.datasources.publish( + new_datasource, args.filename, publish_mode, connection_credentials=creds + ) + except Exception as exc: Errors.exit_with_error(logger, exc) logger.info(_("publish.success") + "\n{}".format(new_datasource.webpage_url)) diff --git a/tabcmd/commands/datasources_and_workbooks/runschedule_command.py b/tabcmd/commands/datasources_and_workbooks/runschedule_command.py index f4eceb29..9661135b 100644 --- a/tabcmd/commands/datasources_and_workbooks/runschedule_command.py +++ b/tabcmd/commands/datasources_and_workbooks/runschedule_command.py @@ -15,7 +15,8 @@ class RunSchedule(DatasourcesAndWorkbooks): @staticmethod def define_args(runschedule_parser): - runschedule_parser.add_argument("schedule", help=_("tabcmd.run_schedule.options.schedule")) + group = runschedule_parser.add_argument_group(title=RunSchedule.name) + group.add_argument("schedule", help=_("tabcmd.run_schedule.options.schedule")) @staticmethod def run_command(args): diff --git a/tabcmd/commands/extracts/create_extracts_command.py b/tabcmd/commands/extracts/create_extracts_command.py index f7f93b72..f23be293 100644 --- a/tabcmd/commands/extracts/create_extracts_command.py +++ b/tabcmd/commands/extracts/create_extracts_command.py @@ -18,12 +18,13 @@ class CreateExtracts(Server): @staticmethod def define_args(create_extract_parser): - set_ds_xor_wb_args(create_extract_parser) - set_embedded_datasources_options(create_extract_parser) - set_encryption_option(create_extract_parser) - set_project_arg(create_extract_parser) - set_parent_project_arg(create_extract_parser) - set_site_url_arg(create_extract_parser) + group = create_extract_parser.add_argument_group(title=CreateExtracts.name) + set_ds_xor_wb_args(group) + set_embedded_datasources_options(group) + set_encryption_option(group) + set_project_arg(group) + set_parent_project_arg(group) + set_site_url_arg(group) @staticmethod def run_command(args): @@ -53,8 +54,12 @@ def run_command(args): includeAll=args.include_all, datasources=args.embedded_datasources, ) - except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, exception=e) + except Exception as e: + + if args.continue_if_exists and Errors.is_resource_conflict(e): + logger.info(_("tabcmd.result.already_exists").format(_("content_type.extract"), args.name)) + return + Errors.exit_with_error(logger, e) logger.info(_("common.output.job_queued_success")) logger.debug("Extract creation queued with JobID: {}".format(job.id)) diff --git a/tabcmd/commands/extracts/decrypt_extracts_command.py b/tabcmd/commands/extracts/decrypt_extracts_command.py index 6a983a06..a43d8fa6 100644 --- a/tabcmd/commands/extracts/decrypt_extracts_command.py +++ b/tabcmd/commands/extracts/decrypt_extracts_command.py @@ -16,7 +16,8 @@ class DecryptExtracts(Server): @staticmethod def define_args(decrypt_extract_parser): - decrypt_extract_parser.add_argument("site_name", metavar="site-name", help=_("editsite.options.site-name")) + group = decrypt_extract_parser.add_argument_group(title=DecryptExtracts.name) + group.add_argument("site_name", metavar="site-name", help=_("editsite.options.site-name")) @staticmethod def run_command(args): @@ -28,8 +29,8 @@ def run_command(args): try: logger.info(_("decryptextracts.status").format(args.site_name)) job = server.sites.decrypt_extracts(site_item.id) - except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, "Error decrypting extracts", e) + except Exception as e: + Errors.exit_with_error(logger, e) logger.info(_("common.output.job_queued_success")) logger.debug("Extract decryption queued with JobID: {}".format(job.id)) diff --git a/tabcmd/commands/extracts/delete_extracts_command.py b/tabcmd/commands/extracts/delete_extracts_command.py index 400d7c7a..62ba3bb0 100644 --- a/tabcmd/commands/extracts/delete_extracts_command.py +++ b/tabcmd/commands/extracts/delete_extracts_command.py @@ -18,12 +18,13 @@ class DeleteExtracts(Server): @staticmethod def define_args(delete_extract_parser): - set_ds_xor_wb_args(delete_extract_parser) - set_embedded_datasources_options(delete_extract_parser) - # set_encryption_option(delete_extract_parser) - set_project_arg(delete_extract_parser) - set_parent_project_arg(delete_extract_parser) - delete_extract_parser.add_argument("--url", help=_("createextracts.options.url")) + group = delete_extract_parser.add_argument_group(title=DeleteExtracts.name) + set_ds_xor_wb_args(group) + set_embedded_datasources_options(group) + # set_encryption_option(group) + set_project_arg(group) + set_parent_project_arg(group) + group.add_argument("--url", help=_("createextracts.options.url")) @staticmethod def run_command(args): @@ -40,8 +41,8 @@ def run_command(args): logger.info(_("deleteextracts.for.workbook_name").format(args.workbook)) workbook_item = Server.get_workbook_item(logger, server, args.workbook) job = server.workbooks.delete_extract(workbook_item) - except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, _("deleteextracts.errors.error"), e) + except Exception as e: + Errors.exit_with_error(logger, e) logger.info(_("common.output.job_queued_success")) logger.debug("Extract deletion queued with JobID: {}".format(job.id)) diff --git a/tabcmd/commands/extracts/encrypt_extracts_command.py b/tabcmd/commands/extracts/encrypt_extracts_command.py index 2d3b7ae4..1b697fd7 100644 --- a/tabcmd/commands/extracts/encrypt_extracts_command.py +++ b/tabcmd/commands/extracts/encrypt_extracts_command.py @@ -18,7 +18,8 @@ class EncryptExtracts(Server): @staticmethod def define_args(encrypt_extract_parser): - encrypt_extract_parser.add_argument("site_name", metavar="site-name", help=_("editsite.options.site-name")) + group = encrypt_extract_parser.add_argument_group(title=EncryptExtracts.name) + group.add_argument("site_name", metavar="site-name", help=_("editsite.options.site-name")) @staticmethod def run_command(args): @@ -30,7 +31,7 @@ def run_command(args): try: logger.info(_("encryptextracts.status").format(site_item.name)) job = server.sites.encrypt_extracts(site_item.id) - except TSC.ServerResponseError as e: + except Exception as e: Errors.exit_with_error(logger, e) logger.info(_("common.output.job_queued_success")) diff --git a/tabcmd/commands/extracts/reencrypt_extracts_command.py b/tabcmd/commands/extracts/reencrypt_extracts_command.py index 66785bfe..4f7b4ef5 100644 --- a/tabcmd/commands/extracts/reencrypt_extracts_command.py +++ b/tabcmd/commands/extracts/reencrypt_extracts_command.py @@ -18,7 +18,8 @@ class ReencryptExtracts(Server): @staticmethod def define_args(reencrypt_extract_parser): - reencrypt_extract_parser.add_argument("site_name", metavar="site-name", help=_("editsite.options.site-name")) + group = reencrypt_extract_parser.add_argument_group(title=ReencryptExtracts.name) + group.add_argument("site_name", metavar="site-name", help=_("editsite.options.site-name")) @staticmethod def run_command(args): @@ -30,7 +31,7 @@ def run_command(args): try: logger.info(_("reencryptextracts.status").format(site_item.name)) job = server.sites.encrypt_extracts(site_item.id) - except TSC.ServerResponseError as e: + except Exception as e: Errors.exit_with_error(logger, e) logger.info(_("common.output.job_queued_success")) diff --git a/tabcmd/commands/extracts/refresh_extracts_command.py b/tabcmd/commands/extracts/refresh_extracts_command.py index 562554ba..e18020a3 100644 --- a/tabcmd/commands/extracts/refresh_extracts_command.py +++ b/tabcmd/commands/extracts/refresh_extracts_command.py @@ -16,15 +16,17 @@ class RefreshExtracts(Server): @staticmethod def define_args(refresh_extract_parser): - possible_targets = set_ds_xor_wb_args(refresh_extract_parser) + group = refresh_extract_parser.add_argument_group(title=RefreshExtracts.name) + possible_targets = set_ds_xor_wb_args(group) + # hm, why did I do this instead of group.add_arg? possible_targets.add_argument( "--url", help=_("createextracts.options.url"), ) - set_incremental_options(refresh_extract_parser) - set_calculations_options(refresh_extract_parser) - set_project_arg(refresh_extract_parser) - set_parent_project_arg(refresh_extract_parser) + set_incremental_options(group) + set_calculations_options(group) + set_project_arg(group) + set_parent_project_arg(group) @staticmethod def run_command(args): @@ -77,7 +79,7 @@ def run_command(args): elif args.url: logger.error("URL not yet implemented") - except TSC.ServerResponseError as e: + except Exception as e: Errors.exit_with_error(logger, _("refreshextracts.errors.error"), e) logger.info(_("common.output.job_queued_success")) diff --git a/tabcmd/commands/group/create_group_command.py b/tabcmd/commands/group/create_group_command.py index 489bf4db..8aeddb84 100644 --- a/tabcmd/commands/group/create_group_command.py +++ b/tabcmd/commands/group/create_group_command.py @@ -17,7 +17,8 @@ class CreateGroupCommand(Server): @staticmethod def define_args(create_group_parser): - create_group_parser.add_argument("name") + args_group = create_group_parser.add_argument_group(title=CreateGroupCommand.name) + args_group.add_argument("name") @staticmethod def run_command(args): @@ -30,9 +31,8 @@ def run_command(args): new_group = TSC.GroupItem(args.name) server.groups.create(new_group) logger.info(_("common.output.succeeded")) - except TSC.ServerResponseError as e: - # quite likely a 403 because you must be server/site admin to call this + except Exception as e: if args.continue_if_exists and Errors.is_resource_conflict(e): - logger.info(_("tabcmd.result.already_exists.group").format(args.name)) + logger.info(_("tabcmd.result.already_exists").format(_("content_type.group"), args.name)) return Errors.exit_with_error(logger, _("tabcmd.result.failed.create_group")) diff --git a/tabcmd/commands/group/delete_group_command.py b/tabcmd/commands/group/delete_group_command.py index 6d2741f1..accf62e4 100644 --- a/tabcmd/commands/group/delete_group_command.py +++ b/tabcmd/commands/group/delete_group_command.py @@ -17,7 +17,8 @@ class DeleteGroupCommand(Server): @staticmethod def define_args(delete_group_parser): - delete_group_parser.add_argument("name") + args_group = delete_group_parser.add_argument_group(title=DeleteGroupCommand.name) + args_group.add_argument("name") @staticmethod def run_command(args): @@ -31,6 +32,5 @@ def run_command(args): logger.info(_("deletegroup.status").format(group_id)) server.groups.delete(group_id) logger.info(_("common.output.succeeded")) - except TSC.ServerResponseError as e: - # quite likely a 403 because you must be server/site admin to call this + except Exception as e: Errors.exit_with_error(logger, _("tabcmd.result.failed.delete.group"), e) diff --git a/tabcmd/commands/help/__init__.py b/tabcmd/commands/help/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tabcmd/commands/help/help_command.py b/tabcmd/commands/help/help_command.py deleted file mode 100644 index 6c5409d8..00000000 --- a/tabcmd/commands/help/help_command.py +++ /dev/null @@ -1,74 +0,0 @@ -import argparse -from typing import Any, List - -from tabcmd.execution.localize import _ -from tabcmd.execution.logger_config import log - - -class HelpCommand: - """ - Command to show user help options - """ - - name: str = "help" - description: str = "Show Help and exit" - - @staticmethod - def define_args(parser): - parser.add_argument("help_option", nargs="?") - - @staticmethod - def run_command(args: argparse.Namespace): - - # whaddya mean, '__class__' is not defined ??!?!!? - logger = log(__class__.__name__, args.logging_level) # type: ignore[name-defined] - logger.debug(_("tabcmd.launching")) - - # delayed import, TODO fix cyclic imports - from tabcmd.execution.map_of_commands import CommandsMap - - all_commands: List[Any] = CommandsMap.commands_hash_map - - description: str = ( - "tabcmd - Tableau Server Command Line Utility 2.0 \n \n" - "tabcmd help -- List all available commands and global options \n" - "tabcmd help -- Show Help for a specific command\n\n" - ) - - if args.help_option: - - if args.help_option in map(lambda command: command.name, all_commands): - command_objects = filter(lambda command: command.name == args.help_option, all_commands) - cli_cmd = list(command_objects)[0] - logger.info(cli_cmd.name.ljust(25) + cli_cmd.description + "\n") - command_parser = argparse.ArgumentParser(parents=[]) - cli_cmd.define_args(command_parser) - - positionals = [] - optionals = [] - for option in command_parser._actions: - if option.option_strings: - optionals.append(option) - else: - positionals.append(option) - - if positionals: - logger.info("Required arguments") - for option in positionals: - logger.info("{0} {1}{2}{3}".format(option.dest.ljust(25), "{", option.help, "}")) - if optionals: - logger.info("\nOptional arguments") - for option in optionals: - logger.info("{0} {1} ".format(option.option_strings, option.help)) - - logger.info("\nUsage") - usage = cli_cmd.name + " " - for opt in positionals: - usage = usage + opt.dest - if len(positionals) < len(command_parser._actions): - usage = usage + " [--optional arguments]" - logger.info(usage) - - else: - for cmd in all_commands: - logger.info(cmd.name + ": " + cmd.description) diff --git a/tabcmd/commands/project/create_project_command.py b/tabcmd/commands/project/create_project_command.py index b1838b48..f1bec5b0 100644 --- a/tabcmd/commands/project/create_project_command.py +++ b/tabcmd/commands/project/create_project_command.py @@ -18,7 +18,8 @@ class CreateProjectCommand(Server): @staticmethod def define_args(create_project_parser): - create_project_parser.add_argument( + args_group = create_project_parser.add_argument_group(title=CreateProjectCommand.name) + args_group.add_argument( "--name", "-n", dest="project_name", required=True, help=_("createproject.options.name") ) set_parent_project_arg(create_project_parser) @@ -36,8 +37,8 @@ def run_command(args): try: logger.info(_("tabcmd.find.parent_project").format(args.parent_project_path)) parent = Server.get_project_by_name_and_parent_path(logger, server, None, args.parent_project_path) - except TSC.ServerResponseError as exc: - Errors.exit_with_error(logger, _("publish.errors.server_resource_not_found"), exc) + except Exception as exc: + Errors.exit_with_error(logger, exc) readable_name = "{0}/{1}".format(args.parent_project_path, args.project_name) parent_id = parent.id logger.debug("parent project = `{0}`, id = {1}".format(args.parent_project_path, parent_id)) @@ -47,11 +48,8 @@ def run_command(args): project_item = server.projects.create(new_project) logger.info(_("common.output.succeeded")) return project_item - except TSC.ServerResponseError as e: - if Errors.is_resource_conflict(e): - if args.continue_if_exists: - logger.info(_("tabcmd.result.already_exists").format(args.project_name)) - return - else: - Errors.exit_with_error(logger, _("tabcmd.result.already_exists").format(args.project_name)) - Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) + except Exception as e: + if Errors.is_resource_conflict(e) and args.continue_if_exists: + logger.info(_("tabcmd.result.already_exists").format(_("content_type.project"), args.project_name)) + return + Errors.exit_with_error(logger, e) diff --git a/tabcmd/commands/project/delete_project_command.py b/tabcmd/commands/project/delete_project_command.py index 9ea30d8d..74b72395 100644 --- a/tabcmd/commands/project/delete_project_command.py +++ b/tabcmd/commands/project/delete_project_command.py @@ -18,8 +18,9 @@ class DeleteProjectCommand(Server): @staticmethod def define_args(delete_project_parser): - delete_project_parser.add_argument("project_name", metavar="project-name", help=_("createproject.options.name")) - set_parent_project_arg(delete_project_parser) + args_group = delete_project_parser.add_argument_group(title=DeleteProjectCommand.name) + args_group.add_argument("project_name", metavar="project-name", help=_("createproject.options.name")) + set_parent_project_arg(args_group) @staticmethod def run_command(args): @@ -31,17 +32,17 @@ def run_command(args): logger.debug("parent path: {}".format(args.parent_project_path)) try: - logger.debug(_("deleteproject.status").format(args.parent_project_path, args.project_name)) + logger.debug(_("deleteproject.status").format((args.parent_project_path or "") + "/" + args.project_name)) project = Server.get_project_by_name_and_parent_path( logger, server, args.project_name, args.parent_project_path ) - except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) + except Exception as e: + Errors.exit_with_error(logger, e) project_id = project.id try: logger.info(_("deleteproject.status").format(args.project_name)) server.projects.delete(project_id) logger.info(_("common.output.succeeded")) - except TSC.ServerResponseError as e: + except Exception as e: Errors.exit_with_error(logger, "tabcmd.result.failure.delete.project", e) diff --git a/tabcmd/commands/project/publish_samples_command.py b/tabcmd/commands/project/publish_samples_command.py index 65f47358..6d8a54c4 100644 --- a/tabcmd/commands/project/publish_samples_command.py +++ b/tabcmd/commands/project/publish_samples_command.py @@ -17,13 +17,14 @@ class PublishSamplesCommand(Server): @staticmethod def define_args(publish_samples_parser): - publish_samples_parser.add_argument( + args_group = publish_samples_parser.add_argument_group(title=PublishSamplesCommand.name) + args_group.add_argument( "--name", "-n", dest="project_name", required=True, ) - set_parent_project_arg(publish_samples_parser) # args.parent_project_name + set_parent_project_arg(args_group) # args.parent_project_name @staticmethod def run_command(args): diff --git a/tabcmd/commands/server.py b/tabcmd/commands/server.py index fbec528e..f215e3a8 100644 --- a/tabcmd/commands/server.py +++ b/tabcmd/commands/server.py @@ -43,12 +43,12 @@ def find_user(logger, server, username): return Server.get_items_by_name(logger, server.users, username)[0] @staticmethod - def get_items_by_name(logger, item_endpoint, item_name: str, container: TSC.ProjectItem = None) -> List: + 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)) @@ -91,7 +91,7 @@ def get_site_for_command_or_throw(logger, server, site_name): return site_item @staticmethod - def get_site_by_name(logger, server, site_name): + def get_site_by_name(logger, server, site_name) -> TSC.SiteItem: try: # sites don't use the normal filter site_item = server.sites.get_by_name(site_name) diff --git a/tabcmd/commands/site/create_site_command.py b/tabcmd/commands/site/create_site_command.py index e8ca8c03..84208188 100644 --- a/tabcmd/commands/site/create_site_command.py +++ b/tabcmd/commands/site/create_site_command.py @@ -18,8 +18,9 @@ class CreateSiteCommand(Server): @staticmethod def define_args(create_site_parser): - create_site_parser.add_argument("new_site_name", metavar="site-name", help=_("editsite.options.site-name")) - set_common_site_args(create_site_parser) + args_group = create_site_parser.add_argument_group(title=CreateSiteCommand.name) + args_group.add_argument("new_site_name", metavar="site-name", help=_("editsite.options.site-name")) + set_common_site_args(args_group) @staticmethod def run_command(args): @@ -41,14 +42,8 @@ def run_command(args): logger.info(_("createsite.status").format(args.new_site_name)) server.sites.create(new_site) logger.info(_("common.output.succeeded")) - except TSC.ServerResponseError as e: - if Errors.is_resource_conflict(e): - if args.continue_if_exists: - logger.info(_("createsite.errors.site_name_already_exists").format(args.new_site_name)) - return - else: - Errors.exit_with_error( - logger, _("createsite.errors.site_name_already_exists").format(args.site_name) - ) - - Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) + except Exception as e: + if Errors.is_resource_conflict(e) and args.continue_if_exists: + logger.info(_("createsite.errors.site_name_already_exists").format(args.new_site_name)) + return + Errors.exit_with_error(logger, e) diff --git a/tabcmd/commands/site/delete_site_command.py b/tabcmd/commands/site/delete_site_command.py index 2d210c86..56256c8e 100644 --- a/tabcmd/commands/site/delete_site_command.py +++ b/tabcmd/commands/site/delete_site_command.py @@ -17,7 +17,8 @@ class DeleteSiteCommand(Server): @staticmethod def define_args(delete_site_parser): - delete_site_parser.add_argument("site_name_to_delete", metavar="site-name", help="name of site to delete") + args_group = delete_site_parser.add_argument_group(title=DeleteSiteCommand.name) + args_group.add_argument("site_name_to_delete", metavar="site-name", help=strings[2]) @staticmethod def run_command(args): @@ -25,9 +26,20 @@ def run_command(args): logger.debug(_("tabcmd.launching")) session = Session() server = session.create_session(args) - site_url = Server.get_site_by_name(logger, server, args.site_name_to_delete).content_url + target_site: TSC.SiteItem = Server.get_site_by_name(logger, server, args.site_name_to_delete) + target_site_id = target_site.id + logger.debug(strings[3].format(target_site_id, server.site_id)) try: - server.sites.delete(site_url) - logger.info("Successfully deleted the site") - except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, "Error deleting site", e) + server.sites.delete(target_site_id) + except Exception as e: + Errors.exit_with_error(logger, strings[4], e) + logger.info(strings[0].format(args.site_name_to_delete)) + + +strings = [ + "Successfully deleted site {}", + "Server responded with an error while deleting site", + "name of site to delete", + "Deleting site {0}, logged in to site {1}", + "Error while deleting site", +] diff --git a/tabcmd/commands/site/edit_site_command.py b/tabcmd/commands/site/edit_site_command.py index cbd58f31..2d7e3db4 100644 --- a/tabcmd/commands/site/edit_site_command.py +++ b/tabcmd/commands/site/edit_site_command.py @@ -19,13 +19,11 @@ class EditSiteCommand(Server): @staticmethod def define_args(edit_site_parser): - edit_site_parser.add_argument("site_name", metavar="site-name", help="editsite.options.site-name") - edit_site_parser.add_argument( - "--site-name", default=None, dest="new_site_name", help=_("editsite.options.site-name") - ) - - set_common_site_args(edit_site_parser) - set_site_status_arg(edit_site_parser) + args_group = edit_site_parser.add_argument_group(title=EditSiteCommand.name) + args_group.add_argument("site_name", metavar="site-name", help="editsite.options.site-name") + args_group.add_argument("--site-name", default=None, dest="new_site_name", help=_("editsite.options.site-name")) + set_common_site_args(args_group) + set_site_status_arg(args_group) @staticmethod def run_command(args): @@ -48,5 +46,5 @@ def run_command(args): server.sites.update(site_item) logger.info(_("common.output.succeeded")) - except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) + except Exception as e: + Errors.exit_with_error(logger, e) diff --git a/tabcmd/commands/site/list_command.py b/tabcmd/commands/site/list_command.py index 2eff8ec5..a77eab75 100644 --- a/tabcmd/commands/site/list_command.py +++ b/tabcmd/commands/site/list_command.py @@ -18,7 +18,8 @@ class ListCommand(Server): @staticmethod def define_args(list_parser): - list_parser.add_argument("content", choices=["projects", "workbooks", "datasources"], help="View content") + args_group = list_parser.add_argument_group(title=ListCommand.name) + args_group.add_argument("content", choices=["projects", "workbooks", "datasources"], help="View content") @staticmethod def run_command(args): @@ -38,8 +39,8 @@ def run_command(args): logger.info("===== Listing {0} content for user {1}...".format(content_type, session.username)) for item in items: - print("NAME:".rjust(10), item.name) - print("ID:".rjust(10), item.id) + logger.info("NAME:".rjust(10), item.name) + logger.info("ID:".rjust(10), item.id) - except TSC.ServerResponseError as e: + except Exception as e: Errors.exit_with_error(logger, e) diff --git a/tabcmd/commands/site/list_sites_command.py b/tabcmd/commands/site/list_sites_command.py index 55e69f85..66ad24c7 100644 --- a/tabcmd/commands/site/list_sites_command.py +++ b/tabcmd/commands/site/list_sites_command.py @@ -18,7 +18,8 @@ class ListSiteCommand(Server): @staticmethod def define_args(list_site_parser): - set_site_detail_option(list_site_parser) + group = list_site_parser.add_argument_group(title=ListSiteCommand.name) + set_site_detail_option(group) @staticmethod def run_command(args): @@ -30,9 +31,9 @@ def run_command(args): sites, pagination = server.sites.get() logger.info(_("listsites.status").format(session.username)) for site in sites: - print("NAME:".rjust(10), site.name) - print("SITEID:".rjust(10), site.content_url) + logger.info("NAME:".rjust(10), site.name) + logger.info("SITEID:".rjust(10), site.content_url) if args.get_extract_encryption_mode: - print("EXTRACTENCRYPTION:", site.extract_encryption_mode) - except TSC.ServerResponseError as e: + logger.info("EXTRACTENCRYPTION:", site.extract_encryption_mode) + except Exception as e: Errors.exit_with_error(logger, e) diff --git a/tabcmd/commands/user/add_users_command.py b/tabcmd/commands/user/add_users_command.py index 2d580ac3..801f0253 100644 --- a/tabcmd/commands/user/add_users_command.py +++ b/tabcmd/commands/user/add_users_command.py @@ -15,9 +15,10 @@ class AddUserCommand(UserCommand): @staticmethod def define_args(add_user_parser): - add_user_parser.add_argument("name", help="name of group to add users to") - set_users_file_arg(add_user_parser) - set_completeness_options(add_user_parser) + args_group = add_user_parser.add_argument_group(title=AddUserCommand.name) + args_group.add_argument("name", help="name of group to add users to") + set_users_file_arg(args_group) + set_completeness_options(args_group) @staticmethod def run_command(args): diff --git a/tabcmd/commands/user/create_site_users.py b/tabcmd/commands/user/create_site_users.py index 37b7eaaa..2e470544 100644 --- a/tabcmd/commands/user/create_site_users.py +++ b/tabcmd/commands/user/create_site_users.py @@ -19,9 +19,10 @@ class CreateSiteUsersCommand(UserCommand): @staticmethod def define_args(create_site_users_parser): - set_role_arg(create_site_users_parser) - set_users_file_positional(create_site_users_parser) - set_completeness_options(create_site_users_parser) + args_group = create_site_users_parser.add_argument_group(title=CreateSiteUsersCommand.name) + set_role_arg(args_group) + set_users_file_positional(args_group) + set_completeness_options(args_group) @staticmethod def run_command(args): diff --git a/tabcmd/commands/user/create_users_command.py b/tabcmd/commands/user/create_users_command.py index 2a44854f..66414427 100644 --- a/tabcmd/commands/user/create_users_command.py +++ b/tabcmd/commands/user/create_users_command.py @@ -20,9 +20,10 @@ class CreateUsersCommand(UserCommand): @staticmethod def define_args(create_users_parser): - set_role_arg(create_users_parser) - set_users_file_positional(create_users_parser) - set_completeness_options(create_users_parser) + args_group = create_users_parser.add_argument_group(title=CreateUsersCommand.name) + set_role_arg(args_group) + set_users_file_positional(args_group) + set_completeness_options(args_group) @staticmethod def run_command(args): diff --git a/tabcmd/commands/user/delete_site_users_command.py b/tabcmd/commands/user/delete_site_users_command.py index aa749a5a..42f088fd 100644 --- a/tabcmd/commands/user/delete_site_users_command.py +++ b/tabcmd/commands/user/delete_site_users_command.py @@ -19,8 +19,9 @@ class DeleteSiteUsersCommand(Server): @staticmethod def define_args(delete_site_users_parser): - set_users_file_positional(delete_site_users_parser) - set_completeness_options(delete_site_users_parser) + args_group = delete_site_users_parser.add_argument_group(title=DeleteSiteUsersCommand.name) + set_users_file_positional(args_group) + set_completeness_options(args_group) @staticmethod def run_command(args): diff --git a/tabcmd/commands/user/remove_users_command.py b/tabcmd/commands/user/remove_users_command.py index bedbd394..722ce3d5 100644 --- a/tabcmd/commands/user/remove_users_command.py +++ b/tabcmd/commands/user/remove_users_command.py @@ -15,9 +15,10 @@ class RemoveUserCommand(UserCommand): @staticmethod def define_args(remove_users_parser): - remove_users_parser.add_argument("name", help="The group to remove users from.") - set_users_file_arg(remove_users_parser) - set_completeness_options(remove_users_parser) + args_group = remove_users_parser.add_argument_group(title=RemoveUserCommand.name) + args_group.add_argument("name", help="The group to remove users from.") + set_users_file_arg(args_group) + set_completeness_options(args_group) @staticmethod def run_command(args): diff --git a/tabcmd/commands/user/user_data.py b/tabcmd/commands/user/user_data.py index f8c088f7..a95448d7 100644 --- a/tabcmd/commands/user/user_data.py +++ b/tabcmd/commands/user/user_data.py @@ -218,10 +218,8 @@ def act_on_users( group = None try: group = UserCommand.find_group(logger, server, args.name) - except TSC.ServerResponseError as e: - Errors.exit_with_error( - logger, _("errors.reportable.impersonation.group_not_found").format(args.name), exception=e - ) + except Exception as e: + Errors.exit_with_error(logger, e) n_users_handled: int = 0 number_of_errors: int = 0 diff --git a/tabcmd/execution/global_options.py b/tabcmd/execution/global_options.py index a1f76c02..8292ab20 100644 --- a/tabcmd/execution/global_options.py +++ b/tabcmd/execution/global_options.py @@ -40,7 +40,7 @@ def set_users_file_arg(parser): parser.add_argument( "--users", required=True, - type=argparse.FileType("r", encoding="UTF-8"), + type=argparse.FileType("r", encoding="utf-8-sig"), help="CSV file containing a list of users.", ) return parser @@ -50,7 +50,7 @@ def set_users_file_positional(parser): parser.add_argument( "filename", metavar="filename.csv", - type=argparse.FileType("r", encoding="UTF-8"), + type=argparse.FileType("r", encoding="utf-8-sig"), help="CSV file containing a list of users.", ) return parser @@ -65,25 +65,29 @@ def set_no_wait_option(parser): return parser -# TODO make this lower case? +site_roles = [ + "ServerAdministrator", + "SiteAdministratorCreator", + "SiteAdministratorExplorer", + "SiteAdministrator", + "Creator", + "ExplorerCanPublish", + "Publisher", + "Explorer", + "Interactor", + "Viewer", + "Unlicensed", +] + + def set_role_arg(parser): parser.add_argument( "-r", "--role", - choices=[ - "ServerAdministrator", - "SiteAdministratorCreator", - "SiteAdministratorExplorer", - "SiteAdministrator", - "Creator", - "ExplorerCanPublish", - "Publisher", - "Explorer", - "Interactor", - "Viewer", - "Unlicensed", - ], - help="Specifies a site role for all users in the .csv file.", + choices=list(map(lambda x: x.lower(), site_roles)), + type=str.lower, + help="Specifies a site role for all users in the .csv file. Possible roles: " + ", ".join(site_roles), + metavar="SITE_ROLE", ) return parser @@ -202,6 +206,7 @@ def set_site_status_arg(parser): parser.add_argument( "--status", choices=["ACTIVE", "SUSPENDED"], + type=str.upper, help="Set to ACTIVE to activate a site, or to SUSPENDED to suspend a site.", ) return parser @@ -244,8 +249,9 @@ def set_common_site_args(parser): parser.add_argument( "--run-now-enabled", - help="Allow or deny users from running extract refreshes, flows, or schedules manually. \ - true to allow users to run tasks manually or false to prevent users from running tasks manually.", + choices=["true", "false"], + type=str.lower, + help="Allow or deny users from running extract refreshes, flows, or schedules manually.", ) return parser @@ -285,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", @@ -298,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", @@ -314,15 +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): append_group = parser.add_mutually_exclusive_group() diff --git a/tabcmd/execution/localize.py b/tabcmd/execution/localize.py index d6eed6dd..f38995cc 100644 --- a/tabcmd/execution/localize.py +++ b/tabcmd/execution/localize.py @@ -4,7 +4,7 @@ import os import sys from os import listdir -from typing import Any +from typing import Any, Optional from typing import Callable translate = None @@ -14,7 +14,7 @@ def _(string_key: str) -> str: global translate if not translate: - translate = set_client_locale(None, None) + translate = set_client_locale() if not translate: translate = _identity_func return translate(string_key) @@ -25,7 +25,7 @@ def _identity_func(x: Any) -> Any: # The client should present text in the OS language, or english if not present. -def set_client_locale(lang: str = None, logger=None) -> Callable: +def set_client_locale(lang: str = "", logger=None) -> Optional[Callable]: if not logger: logger = logging.getLogger() @@ -33,7 +33,7 @@ def set_client_locale(lang: str = None, logger=None) -> Callable: try: locale_options = [_validate_lang(lang), _get_default_locale(), "en"] except Exception as e: - print(e) + print(sys.stderr, e) locale_options = ["en"] logger.debug("Language options: {}".format(locale_options)) @@ -45,7 +45,7 @@ def set_client_locale(lang: str = None, logger=None) -> Callable: translate = _load_language(lang, domain, logger) break except Exception as e: - print("Failed to load language '", lang, "':", e) + print(sys.stderr, "Failed to load language '", lang, "':", e) return translate or _identity_func diff --git a/tabcmd/execution/logger_config.py b/tabcmd/execution/logger_config.py index 8a42e2cb..17fe6907 100644 --- a/tabcmd/execution/logger_config.py +++ b/tabcmd/execution/logger_config.py @@ -6,7 +6,7 @@ FORMATS = { logging.ERROR: "%(asctime)s %(levelname)-5s:(%(name)-10s %(filename)-10s: %(lineno)d): %(message)-30s", logging.WARN: "%(asctime)s %(levelname)-5s: (%(name)-10s %(filename)-10s: %(lineno)d): %(message)-30s", - logging.INFO: "%(filename)-10s: %(message)-30s", + logging.INFO: "%(message)-30s", logging.DEBUG: "%(asctime)s %(levelname)-5s: (%(name)-10s %(filename)-10s: %(lineno)d): %(message)-30s", } @@ -45,6 +45,9 @@ def configure_log(name: str, logging_level_input: str): """function for logging statements to console and logfile""" logging_level = getattr(logging, logging_level_input.upper()) log_format = FORMATS[logging_level] + if logging_level is not logging.INFO: + FORMATS[logging.INFO] = "%(filename)-10s: %(message)-30s" + logging.basicConfig( level=logging_level, format=log_format, filename="tabcmd.log", filemode="a", datefmt="%Y-%m" "-%d " "%H:%M:%S" ) diff --git a/tabcmd/execution/map_of_commands.py b/tabcmd/execution/map_of_commands.py index c6f7a679..4b60e854 100644 --- a/tabcmd/execution/map_of_commands.py +++ b/tabcmd/execution/map_of_commands.py @@ -12,7 +12,6 @@ from tabcmd.commands.extracts.refresh_extracts_command import * from tabcmd.commands.group.create_group_command import * from tabcmd.commands.group.delete_group_command import * -from tabcmd.commands.help.help_command import * from tabcmd.commands.project.create_project_command import * from tabcmd.commands.project.delete_project_command import * from tabcmd.commands.project.publish_samples_command import * @@ -50,7 +49,6 @@ class CommandsMap: EncryptExtracts, ExportCommand, GetUrl, - HelpCommand, ListSiteCommand, ListCommand, LoginCommand, diff --git a/tabcmd/execution/parent_parser.py b/tabcmd/execution/parent_parser.py index cb1a68d0..a2495792 100644 --- a/tabcmd/execution/parent_parser.py +++ b/tabcmd/execution/parent_parser.py @@ -1,5 +1,10 @@ import argparse +import logging + from .localize import _ +from .logger_config import log +from .map_of_commands import CommandsMap + # when we drop python 3.8, this could be replaced with this lighter weight option # from importlib.metadata import version, PackageNotFoundError @@ -12,143 +17,212 @@ pass +""" +Note: output order is influenced first by grouping, then by order they are added in here +Most of this function is about making the help output look nice. +Argparse uses argument groups to separate arguments in the help output - but that doesn't +work quite as documented when in nested parsers, like we have. +Everything that is just added directly to the parser will be in the default set of +'optional arguments' directly on the newly created parser, which is renamed 'behavior arguments' to +differentiate from the signin/connection options. +Everything we add to a mutually-exclusive-group in here will be in the default set of +'optional arguments' on the *parent* parser, which is displayed all together. To make this a nice set, +many arguments are added to a mutually-exclusive-group of one argument. They are named things like +'formatting_group1' to make it clear this is a formatting choice, not functional. +The arguments for each command must be added to a group for that command. +""" + + +def parent_parser_with_global_options(): + parser = argparse.ArgumentParser(usage=argparse.SUPPRESS, add_help=False) + parser._optionals.title = strings[0] + + formatting_group1 = parser.add_mutually_exclusive_group() + formatting_group1.add_argument( + "-s", + "--server", + default=None, # default is handled in Session class + metavar="", + help=_("session.options.server"), + ) + + formatting_group2 = parser.add_mutually_exclusive_group() + formatting_group2.add_argument( + "-t", "--site", default="", dest="site_name", metavar="SITEID", help=_("session.options.site") + ) + + auth_options = parser.add_mutually_exclusive_group() + auth_options.add_argument("--token-name", default=None, metavar="", help=strings[13]) + auth_options.add_argument("-u", "--username", default=None, metavar="", help=_("session.options.username")) + + secret_values = parser.add_mutually_exclusive_group() + secret_values.add_argument( + "--token-value", + default=None, + metavar="", + help=strings[12], + ) + secret_values.add_argument( + "-p", "--password", default=None, metavar="", help=_("session.options.password") + ) + secret_values.add_argument( + "--password-file", default=None, metavar="", help=_("session.options.password-file") + ) + secret_values.add_argument("--token-file", default=None, metavar="", help=strings[11]) + + formatting_group3 = parser.add_mutually_exclusive_group() + formatting_group3.add_argument("--no-prompt", action="store_true", help=_("session.options.no-prompt")) + + certificates = parser.add_mutually_exclusive_group() + certificates.add_argument( + "-c", + "--use-certificate", + dest="certificate", + default=None, + metavar="", + help=_("session.options.use-certificate"), + ) + certificates.add_argument( + "--no-certcheck", + action="store_true", + help=_("session.options.no-certcheck"), + ) + + formatting_group4 = parser.add_mutually_exclusive_group() + formatting_group4.add_argument("--no-cookie", action="store_true", help=_("session.options.no-cookie")) + + proxy_group = parser.add_mutually_exclusive_group() + proxy_group.add_argument( + "-x", "--proxy", dest="proxy", default=None, metavar="", help=_("session.options.proxy") + ) + proxy_group.add_argument( + "--no-proxy", + action="store_false", + help=_("session.options.no-proxy"), + ) + + formatting_group5 = parser.add_mutually_exclusive_group() + formatting_group5.add_argument( + "--timeout", + default=None, # default is handled in Session class + metavar="", # can't use -t, it's already used for --site + help=_("session.options.timeout"), + ) + + # general behavioral options + parser.add_argument( + "--continue-if-exists", + action="store_false", + help=strings[9], + ) + + parser.add_argument( + "--country", + choices=["de", "en", "es", "fr", "it", "ja", "ko", "pt", "sv", "zh"], + type=str.lower, # coerce input to lowercase to act case insensitive + help=_("export.options.country"), + ) + + parser.add_argument( + "--language", + choices=["de", "en", "es", "fr", "it", "ja", "ko", "pt", "sv", "zh"], + type=str.lower, # coerce input to lowercase to act case insensitive + help=strings[10], + ) + + parser.add_argument( + "-l", + "--logging-level", + choices=["TRACE", "DEBUG", "INFO", "ERROR"], + type=str.upper, # coerce input to uppercase to act case insensitive + default="info", + help=strings[8], + ) + + parser.add_argument( + "-v", + "--version", + action="version", + version=strings[6] + "v" + version + "\n \n", + help=strings[7], + ) + return parser + + class ParentParser: # Ref https://docs.python.org/3/library/argparse.html """Parser that will be inherited by all commands. Contains authentication and logging level setting""" def __init__(self): - self.global_options = self.parent_parser_with_global_options() - self.root = argparse.ArgumentParser(parents=[self.global_options]) + self.global_options = parent_parser_with_global_options() + self.root = argparse.ArgumentParser( + prog="tabcmd", description=strings[15], parents=[self.global_options], epilog=strings[2] + ) + self.root._optionals.title = strings[1] # https://stackoverflow.com/questions/7498595/python-argparse-add-argument-to-multiple-subparsers - self.subparsers = self.root.add_subparsers() + self.subparsers = self.root.add_subparsers( + title=strings[3], + description=strings[4], + metavar=strings[5], # instead of printing the list of choices + ) def get_root_parser(self): return self.root + def connect_commands(self): + commands = CommandsMap.commands_hash_map + for command in commands: + self.include(command) + return self.root + def include(self, command): additional_parser = self.subparsers.add_parser( command.name, help=command.description, parents=[self.global_options] ) + additional_parser._optionals.title = strings[1] # This line is where we actually set each parser to call the correct command additional_parser.set_defaults(func=command) command.define_args(additional_parser) return additional_parser - # ordered alphabetically by short option - this is reflected directly in help output - def parent_parser_with_global_options(self): - parser = argparse.ArgumentParser(usage=argparse.SUPPRESS, add_help=False) - - certificates = parser.add_mutually_exclusive_group() - certificates.add_argument( - "-c", - "--use-certificate", - dest="certificate", - default=None, - metavar="", - help=_("session.options.use-certificate"), - ) - certificates.add_argument( - "--no-certcheck", - action="store_true", - help=_("session.options.no-certcheck"), - ) - - parser.add_argument( - "--continue-if-exists", - action="store_false", - help="Treat resource conflicts as item creation success e.g project already exists", - ) - - parser.add_argument("--no-cookie", action="store_true", help=_("session.options.no-cookie")) - - parser.add_argument( - "-l", - "--logging-level", - choices=["TRACE", "DEBUG", "INFO", "ERROR"], - type=str.upper, # coerce input to uppercase to act case insensitive - default="info", - help="Use the specified logging level. The default level is INFO.", - ) - - parser.add_argument("--no-prompt", action="store_true", help=_("session.options.no-prompt")) - - auth_options = parser.add_mutually_exclusive_group() - auth_options.add_argument( - "--token-name", - default=None, - metavar="", - help="The name of the Tableau Server Personal Access Token. If using a token to sign in,\ - this is required at least once to begin session.", - ) - auth_options.add_argument( - "-u", "--username", default=None, metavar="", help=_("session.options.username") - ) - - secret_values = parser.add_mutually_exclusive_group() - secret_values.add_argument( - "--token-value", - default=None, - metavar="", - help="Use the specified Tableau Server Personal Access Token. Requires --token-name to be set.", - ) - secret_values.add_argument( - "-p", "--password", default=None, metavar="", help=_("session.options.password") - ) - secret_values.add_argument( - "--password-file", default=None, metavar="", help=_("session.options.password-file") - ) - - proxy_group = parser.add_mutually_exclusive_group() - proxy_group.add_argument( - "-x", "--proxy", dest="proxy", default=None, metavar="", help=_("session.options.proxy") - ) - proxy_group.add_argument( - "--no-proxy", - action="store_false", - help=_("session.options.no-proxy"), - ) - - parser.add_argument( - "-s", - "--server", - default=None, # default is handled in Session class - metavar="", - help=_("session.options.server"), - ) - parser.add_argument( - "-t", "--site", default="", dest="site_name", metavar="SITEID", help=_("session.options.site") - ) - - parser.add_argument( - "--timeout", - default=None, # default is handled in Session class - metavar="", # can't use -t, it's already used for --site - help=_("session.options.timeout"), - ) - - # TODO get the list of choices dynamically? - parser.add_argument( - "--language", - choices=["de", "en", "es", "fr", "it", "ja", "ko", "pt", "sv", "zh"], - help="Set the language to use. Exported data will be returned in this lang/locale." - "If not set, the client will use your computer locale, and the server will use your user account locale", - ) - - parser.add_argument( - "--country", - choices=["de", "en", "es", "fr", "it", "ja", "ko", "pt", "sv", "zh"], - help=_("export.options.country"), - ) - - # -h goes to argparse default help - - parser.add_argument( - "-v", - "--version", - action="version", - version="Tableau Server Command Line Utility v" + version + "\n \n", - help="Show version information and exit.", - ) - - return parser + def include_help(self): + additional_parser = self.subparsers.add_parser("help", help=strings[14], parents=[self.global_options]) + additional_parser._optionals.title = strings[1] + additional_parser.set_defaults(func=Help(self)) + + +class Help: + + parser = None + # This needs to have access to the parser when it gets called + def __init__(self, _parser: ParentParser): + self.parser = _parser + + def run_command(self, args): + logger = log(__name__, "info") + logger.info(strings[6] + " " + version + "\n") + logger.info(self.parser.root.format_help()) + + +strings = [ + "global behavioral arguments", # 0 - global_behavior_args + "global connection arguments", # 1 - global_conn_args + "For more help see https://tableau.github.io/tabcmd/", # 2 - for_more_help + "list of tabcmd commands", # 3 + "For help on a specific command use 'tabcmd -h'.", # 4 + "{ [command args]}", # 5 + "Tableau Server Command Line Utility", # 6 + "Show version information and exit.", # 7 + "Use the specified logging level. The default level is INFO.", # 8 + "Treat resource conflicts as item creation success e.g project already exists", # 9 + "Set the language to use. Exported data will be returned in this lang/locale.\n \ + If not set, the client will use your computer locale, and the server will use \ + your user account locale", # 10 + "Read the Personal Access Token from a file.", # 11 + "Use the specified Tableau Server Personal Access Token. Requires --token-name to be set.", # 12 + "The name of the Tableau Server Personal Access Token. If using a token to sign in,\ + this is required at least once to begin session.", # 13 + "Show message listing commands and global options, then exit", # 14 + "tabcmd -- Run a specific command", # 15 +] diff --git a/tabcmd/execution/tabcmd_controller.py b/tabcmd/execution/tabcmd_controller.py index 0dee0ad0..1f7f0fdc 100644 --- a/tabcmd/execution/tabcmd_controller.py +++ b/tabcmd/execution/tabcmd_controller.py @@ -2,7 +2,7 @@ import sys from .localize import set_client_locale -from .map_of_commands import * +from .logger_config import log from .parent_parser import ParentParser @@ -10,10 +10,8 @@ class TabcmdController: @staticmethod def initialize(): manager = ParentParser() - parent = manager.get_root_parser() - commands = CommandsMap.commands_hash_map - for command in commands: - manager.include(command) + parent = manager.connect_commands() + manager.include_help() return parent # during normal execution, leaving input as none will default to sys.argv @@ -25,11 +23,11 @@ def run(parser, user_input=None): sys.exit(0) user_input = user_input or sys.argv[1:] namespace = parser.parse_args(user_input) - if namespace.logging_level: + if namespace.logging_level and namespace.logging_level != logging.INFO: print("logging:", namespace.logging_level) logger = log(__name__, namespace.logging_level or logging.INFO) - if namespace.password: + if namespace.password or namespace.token_value: logger.trace(namespace.func) else: logger.trace(namespace) diff --git a/tabcmd/locales/en/LC_MESSAGES/tabcmd.po b/tabcmd/locales/en/LC_MESSAGES/tabcmd.po index 613e6904..1fae5004 100644 --- a/tabcmd/locales/en/LC_MESSAGES/tabcmd.po +++ b/tabcmd/locales/en/LC_MESSAGES/tabcmd.po @@ -13,8 +13,8 @@ msgstr "" # https://sourcegraph.prod.tableautools.com/teams/near/-/blob/workgroup/src/shared/libraries/tab-strings/res/localization/shared_wg_en.properties # how much of this can be replaced by server responses? -msgid "tabcmd.result.already_exists.group" -msgstr "Group called ''{}'' already exists" +msgid "tabcmd.result.already_exists" +msgstr "{} called ''{}'' already exists" msgid "tabcmd.result.failed.delete.group" msgstr "Error deleting group from server" @@ -1702,6 +1702,14 @@ msgstr "Shared version" msgid "flows.drafts.default_name" msgstr "New Flow" +#: +msgid "content_type.group" +msgstr "Group" + +#: +msgid "content_type.user" +msgstr "User" + #: msgid "content_type.project" msgstr "Project" @@ -1734,6 +1742,10 @@ msgstr "Collection" msgid "content_type.visualization" msgstr "Visualization" +#: +msgid "content_type.extract" +msgstr "Extract" + #: msgid "content_type.askdata_visualization" msgstr "Ask Data Visualization" diff --git a/tabcmd/locales/en/tabcmd_messages_en.properties b/tabcmd/locales/en/tabcmd_messages_en.properties index 6992a7f2..e0dc83e0 100644 --- a/tabcmd/locales/en/tabcmd_messages_en.properties +++ b/tabcmd/locales/en/tabcmd_messages_en.properties @@ -165,6 +165,7 @@ export.errors.need_country_and_languge=The options --country and --language must export.errors.white_space_workbook_view=The name of the workbook or view to export cannot include spaces. Use the normalized name of the workbook or view as it appears in the URL. export.errors.requires_workbook_view_name=The ''{0}'' command requires a / name export.errors.requires_workbook_view_param=The ''{0}'' command requires a / parameter, and there must be at least one slash (/) in this parameter +export.errors.requires_resource_param=The ''{0}'' command requires a resource path in a specific format. Given: {0}. Accepted values: {1}, {2}, {3} export.options.country=If not using user''s default locale, the country abbreviation for locale (find in IANA Language Subtag Registry). Must use with --language export.options.csv=Export data in CSV format (default) export.options.fullpdf=Export visual views in PDF format (if workbook was published with tabs) @@ -185,6 +186,8 @@ fileuploader.progress=Upload progress: {0}% get.description=Get a file from the server. The file will be named after the last component of the path if no filename argument is specified get.errors.requires_url=The ''{0}'' command requires an URL +get.errors.invalid_content_type=Could not find one of the expected types to export in '{0}' +get.errors.invalid_file_type=To export a {0}, the file type must be one of {1} get.options.file=Name to save the file as get.short_description=Get a file from the server diff --git a/tabcmd/tabcmd.py b/tabcmd/tabcmd.py index 75951897..eeaec6ed 100644 --- a/tabcmd/tabcmd.py +++ b/tabcmd/tabcmd.py @@ -14,8 +14,9 @@ def main(): parser = TabcmdController.initialize() TabcmdController.run(parser) except Exception as e: - print("Unhandled exception: {}".format(type(e).__name__)) + print(sys.stderr, "Unhandled exception: {}".format(type(e).__name__)) print( + sys.stderr, f"{type(e).__name__} at line {e.__traceback__.tb_lineno} of {__file__}: {e}", ) sys.exit(1) diff --git a/tests/commands/test_geturl_utils.py b/tests/commands/test_geturl_utils.py index fb354141..117a9181 100644 --- a/tests/commands/test_geturl_utils.py +++ b/tests/commands/test_geturl_utils.py @@ -60,7 +60,7 @@ def test_get_view_without_extension_that_doesnt_have_one(self): assert GetUrl.get_name_without_possible_extension(filename) == filename def test_get_workbook_name(self): - assert GetUrl.get_workbook_name("workbooks/wbname", mock_logger) == "wbname" + assert GetUrl.get_resource_name("workbooks/wbname", mock_logger) == "wbname" def test_view_name(self): assert GetUrl.get_view_url("views/wb-name/view-name", None) == "wb-name/sheets/view-name" @@ -114,7 +114,6 @@ def test_download_csv(self, mock_server): mock_args.url = url ExportCommand.download_csv(mock_server, mock_view, mock_args, mock_logger) - @mock.patch("tableauserverclient.Server") def test_download_image(self, mock_server): mock_server.views = mock.MagicMock() @@ -184,7 +183,6 @@ def test_refresh_y(self): ExportCommand.apply_values_from_url_params(options, url, mock_logger) assert options.max_age == 0 - def test_save_to_binary_file(self): mock_content = bytes() filename = "test_out.pdf" diff --git a/tests/commands/test_run_commands.py b/tests/commands/test_run_commands.py index 22b9dda0..7a1afbe2 100644 --- a/tests/commands/test_run_commands.py +++ b/tests/commands/test_run_commands.py @@ -20,7 +20,6 @@ refresh_extracts_command, ) from tabcmd.commands.group import create_group_command, delete_group_command -from tabcmd.commands.help import help_command from tabcmd.commands.project import create_project_command, delete_project_command, publish_samples_command from tabcmd.commands.site import ( create_site_command, @@ -139,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() @@ -252,13 +253,6 @@ def test_delete_group(self, mock_session, mock_server): delete_group_command.DeleteGroupCommand.run_command(mock_args) mock_session.assert_called() - # help - def test_help(self, mock_session, mock_server): - RunCommandsTest._set_up_session(mock_session, mock_server) - mock_args.help_option = "boo" - help_command.HelpCommand.run_command(mock_args) - mock_session.assert_not_called() - # project def test_create_project(self, mock_session, mock_server): RunCommandsTest._set_up_session(mock_session, mock_server) diff --git a/tests/commands/test_session.py b/tests/commands/test_session.py index ebcf770b..8a909608 100644 --- a/tests/commands/test_session.py +++ b/tests/commands/test_session.py @@ -45,7 +45,7 @@ no_prompt=False, ) -fakeserver = "http://SRVR" +fakeserver = "http://SRVR".lower() def _set_mocks_for_json_file_saved_username(mock_json_load, auth_token, username): diff --git a/tests/e2e/language_tests.py b/tests/e2e/language_tests.py index 5e80e7a8..6fced7a3 100644 --- a/tests/e2e/language_tests.py +++ b/tests/e2e/language_tests.py @@ -74,17 +74,33 @@ def _publish_wb(self, file, name): 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) + def _delete_wb(self, file): command = "delete" arguments = [command, "-w", file] _test_command(arguments) + def _delete_ds(self, file): + command = "delete" + arguments = [command, file, "--datasource"] + _test_command(arguments) + def _get_view(self, wb_name_on_server, sheet_name): server_file = "/views/" + wb_name_on_server + "/" + sheet_name command = "get" arguments = [command, server_file] _test_command(arguments) + def _get_datasource(self, server_file): + command = "get" + server_file = "/datasources/" + server_file + arguments = [command, server_file] + _test_command(arguments) + def _get_custom_view(self): command = "get" @@ -113,6 +129,8 @@ def _delete_extract(self, wb_name): TWBX_WITH_EXTRACT_SHEET = "sheet1" TWBX_FILE_WITHOUT_EXTRACT = "simple-data.twbx" TWBX_WITHOUT_EXTRACT_NAME = "WorkbookWithoutExtract" + TDSX_WITH_EXTRACT_NAME = "WorldIndicators" + TDSX_FILE_WITH_EXTRACT = "World Indicators.tdsx" @pytest.mark.order(2) def test_create_site_users(self): @@ -200,7 +218,23 @@ def test__delete_wb(self): name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME self._delete_wb(name_on_server) - @pytest.mark.order(12) + @pytest.mark.order(13) + 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) + + @pytest.mark.order(14) + def test__get_ds(self): + ds_name_on_server = OnlineCommandTest.TDSX_WITH_EXTRACT_NAME + self._get_datasource(ds_name_on_server + ".tdsx") + + @pytest.mark.order(15) + def test__delete_ds(self): + name_on_server = OnlineCommandTest.TDSX_WITH_EXTRACT_NAME + self._delete_ds(name_on_server) + + @pytest.mark.order(16) def test_create_extract(self): # This workbook doesn't work for creating an extract name_on_server = OnlineCommandTest.TWBX_WITHOUT_EXTRACT_NAME @@ -208,12 +242,12 @@ def test_create_extract(self): self._publish_wb(file, name_on_server) # which damn workbook will work here self._create_extract(name_on_server) - @pytest.mark.order(13) + @pytest.mark.order(17) def test_refresh_extract(self): name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME self._refresh_extract(name_on_server) - @pytest.mark.order(14) + @pytest.mark.order(18) def test_delete_extract(self): name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME file = os.path.join("tests", "assets", OnlineCommandTest.TWBX_FILE_WITH_EXTRACT) @@ -231,7 +265,7 @@ def test_help(self): # def test_logout(self): # _test_command(["logout"]) - @pytest.mark.order(15) + @pytest.mark.order(19) def test_export(self): name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME file = os.path.join("tests", "assets", OnlineCommandTest.TWBX_FILE_WITH_EXTRACT) @@ -241,7 +275,7 @@ def test_export(self): arguments = [command, friendly_name, "--fullpdf", "-f", "exported_file.pdf"] _test_command(arguments) - @pytest.mark.order(16) + @pytest.mark.order(20) def test_delete_site_users(self): command = "deletesiteusers" users = os.path.join("tests", "assets", "usernames.csv") diff --git a/tests/e2e/online_tests.py b/tests/e2e/online_tests.py index f7da3677..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" @@ -54,6 +55,7 @@ def _create_project(self, project_name, parent_path=None): if parent_path: arguments.append("--parent-project-path") arguments.append(parent_path) + arguments.append("--continue-if-exists") _test_command(arguments) def _delete_project(self, project_name, parent_path=None): @@ -69,16 +71,39 @@ 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) + 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" arguments = [command, file] _test_command(arguments) + def _delete_ds(self, file): + command = "delete" + arguments = [command, file, "--datasource"] + _test_command(arguments) + def _get_view(self, wb_name_on_server, sheet_name): server_file = "/views/" + wb_name_on_server + "/" + sheet_name command = "get" @@ -97,6 +122,12 @@ def _get_workbook(self, server_file): arguments = [command, server_file] _test_command(arguments) + def _get_datasource(self, server_file): + command = "get" + server_file = "/datasources/" + server_file + arguments = [command, server_file] + _test_command(arguments) + def _create_extract(self, wb_name): command = "createextracts" arguments = [command, "-w", wb_name, "--encrypt"] @@ -125,6 +156,9 @@ def _list(self, item_type: str): TWBX_FILE_WITHOUT_EXTRACT = "simple-data.twbx" TWBX_WITHOUT_EXTRACT_NAME = "WorkbookWithoutExtract" 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): @@ -161,6 +195,7 @@ def test_group_creategroup(self): groupname = group_name command = "creategroup" arguments = [command, groupname] + arguments.append("--continue-if-exists") _test_command(arguments) @pytest.mark.order(4) @@ -172,6 +207,7 @@ def test_users_add_to_group(self): command = "addusers" filename = os.path.join("tests", "assets", "usernames.csv") arguments = [command, groupname, "--users", filename] + arguments.append("--continue-if-exists") _test_command(arguments) @pytest.mark.order(5) @@ -218,12 +254,6 @@ def test_create_projects(self): def test_list_projects(self): self._list("projects") - """ - @pytest.mark.order(9) - def test_publish_samples(self): - self._publish_samples(project_name) - """ - @pytest.mark.order(10) def test_delete_projects(self): if not project_admin: @@ -235,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): @@ -264,53 +295,82 @@ 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_extract_delete(self): + def test_publish_ds(self): + name_on_server = OnlineCommandTest.TDSX_WITH_EXTRACT_NAME + file = os.path.join("tests", "assets", OnlineCommandTest.TDSX_FILE_WITH_EXTRACT) + arguments = self._publish_args(file, name_on_server) + _test_command(arguments) + + @pytest.mark.order(13) + def test__get_ds(self): + ds_name_on_server = OnlineCommandTest.TDSX_WITH_EXTRACT_NAME + self._get_datasource(ds_name_on_server + ".tdsx") + + @pytest.mark.order(14) + def test__delete_ds(self): + name_on_server = OnlineCommandTest.TDSX_WITH_EXTRACT_NAME + self._delete_ds(name_on_server) + + @pytest.mark.order(15) + 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(13) - def test_extract_create(self): + @pytest.mark.order(16) + def test_create_extract(self): # Fails because it 'already has an extract' :/ name_on_server = OnlineCommandTest.TWBX_WITHOUT_EXTRACT_NAME self._create_extract(name_on_server) - @pytest.mark.order(14) - def test_extract_refresh(self): + @pytest.mark.order(17) + 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) self._delete_wb(name_on_server) - @pytest.mark.order(15) + @pytest.mark.order(19) 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"] _test_command(arguments) - @pytest.mark.order(15) + @pytest.mark.order(19) 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"] _test_command(arguments) - @pytest.mark.order(16) - def test_users_delete_site_users(self): + @pytest.mark.order(20) + def test_delete_site_users(self): if not server_admin and not site_admin: pytest.skip("Must be server or site administrator to delete site users") @@ -318,7 +378,7 @@ def test_users_delete_site_users(self): users = os.path.join("tests", "assets", "usernames.csv") _test_command([command, users]) - @pytest.mark.order(20) + @pytest.mark.order(21) def test_list_sites(self): if not server_admin: pytest.skip("Must be server administrator to list sites") diff --git a/tests/parsers/common_setup.py b/tests/parsers/common_setup.py index 6530998b..619d4805 100644 --- a/tests/parsers/common_setup.py +++ b/tests/parsers/common_setup.py @@ -2,6 +2,9 @@ from collections import namedtuple +encoding = "utf-8-sig" + + def mock_command_action(): print("a mockery!") diff --git a/tests/parsers/test_parser_add_user.py b/tests/parsers/test_parser_add_user.py index fe229bce..fe57f669 100644 --- a/tests/parsers/test_parser_add_user.py +++ b/tests/parsers/test_parser_add_user.py @@ -27,7 +27,7 @@ def test_add_users_parser_users_file(self): mock_args = [commandname, "group-name", "--users", "users.csv"] args = self.parser_under_test.parse_args(mock_args) self.assertEqual(args.name, "group-name"), args - open_file.assert_called_with("users.csv", "r", -1, "UTF-8", None), args + open_file.assert_called_with("users.csv", "r", -1, encoding, None), args @mock.patch("builtins.open") def test_add_user_parser_missing_group_name(self, filereader): diff --git a/tests/parsers/test_parser_create_site_users.py b/tests/parsers/test_parser_create_site_users.py index ea157a98..5b46bd00 100644 --- a/tests/parsers/test_parser_create_site_users.py +++ b/tests/parsers/test_parser_create_site_users.py @@ -16,7 +16,7 @@ def test_create_site_users_parser_users_file(self): with mock.patch("builtins.open", mock.mock_open(read_data="test")) as open_file: mock_args = [commandname, "users.csv"] args = self.parser_under_test.parse_args(mock_args) - open_file.assert_called_with("users.csv", "r", -1, "UTF-8", None) + open_file.assert_called_with("users.csv", "r", -1, encoding, None) def test_create_site_user_parser_missing_arguments(self): mock_args = [commandname] diff --git a/tests/parsers/test_parser_create_user.py b/tests/parsers/test_parser_create_user.py index f93ea58f..7b67f24f 100644 --- a/tests/parsers/test_parser_create_user.py +++ b/tests/parsers/test_parser_create_user.py @@ -16,7 +16,7 @@ def test_create_users_parser_users_file(self): with mock.patch("builtins.open", mock.mock_open(read_data="test")) as open_file: mock_args = [commandname, "users.csv"] args = self.parser_under_test.parse_args(mock_args) - open_file.assert_called_with("users.csv", "r", -1, "UTF-8", None) + open_file.assert_called_with("users.csv", "r", -1, encoding, None) def test_create_user_parser_missing_arguments(self): mock_args = [commandname] @@ -27,4 +27,4 @@ def test_create_user_parser_role(self): with mock.patch("builtins.open", mock.mock_open(read_data="test")): mock_args = [commandname, "users.csv", "-r", "SiteAdministrator"] args = self.parser_under_test.parse_args(mock_args) - assert args.role == "SiteAdministrator", args + assert args.role.lower() == "SiteAdministrator".lower(), args diff --git a/tests/parsers/test_parser_delete_site_user.py b/tests/parsers/test_parser_delete_site_user.py index df66af66..c0d81b7e 100644 --- a/tests/parsers/test_parser_delete_site_user.py +++ b/tests/parsers/test_parser_delete_site_user.py @@ -18,7 +18,7 @@ def test_delete_site_user_parser(self): with mock.patch("builtins.open", mock.mock_open(read_data="test")) as open_file: mock_args = [commandname, "users.csv"] args = self.parser_under_test.parse_args(mock_args) - open_file.assert_called_with("users.csv", "r", -1, "UTF-8", None) + open_file.assert_called_with("users.csv", "r", -1, encoding, None) def test_delete_site_user_parser_missing_arguments(self): mock_args = [commandname] diff --git a/tests/parsers/test_parser_remove_user.py b/tests/parsers/test_parser_remove_user.py index 83562c97..6441354a 100644 --- a/tests/parsers/test_parser_remove_user.py +++ b/tests/parsers/test_parser_remove_user.py @@ -23,7 +23,7 @@ def test_remove_users_parser_users_file(self): mock_args = [commandname, "group-name", "--users", "users.csv"] args = self.parser_under_test.parse_args(mock_args) self.assertEqual(args.name, "group-name") - open_file.assert_called_with("users.csv", "r", -1, "UTF-8", None) + open_file.assert_called_with("users.csv", "r", -1, encoding, None) def test_remove_users_parser_missing_group_name(self): with mock.patch("builtins.open", mock.mock_open(read_data="test")):