diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index af05bafe..75bece23 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -26,23 +26,22 @@ jobs: TARGET: windows CMD_BUILD: > pyinstaller tabcmd-windows.spec --clean --noconfirm --distpath ./dist/windows - OUT_FILE_NAME: tabcmd-windows.exe + OUT_FILE_NAME: tabcmd.exe ASSET_MIME: application/vnd.microsoft.portable-executable - os: macos-latest TARGET: macos CMD_BUILD: > - pyinstaller tabcmd-mac.spec --clean --noconfirm --distpath ./dist/macos && ls && ls dist - # zip -r9 mac tabcmd-mac* - OUT_FILE_NAME: tabcmd-mac.app # tabcmd.zip + pyinstaller tabcmd-mac.spec --clean --noconfirm --distpath ./dist/macos + OUT_FILE_NAME: tabcmd.app ASSET_MIME: application/zip - os: ubuntu-latest TARGET: ubuntu # https://stackoverflow.com/questions/31259856 # /how-to-create-an-executable-file-for-linux-machine-using-pyinstaller CMD_BUILD: > - pyinstaller --clean -y --distpath ./dist/linux tabcmd-linux.spec && - chown -R --reference=. ./dist/linux - OUT_FILE_NAME: tab-for-linux + pyinstaller --clean -y --distpath ./dist/ubuntu tabcmd-linux.spec && + chown -R --reference=. ./dist/ubuntu + OUT_FILE_NAME: tabcmd steps: - uses: actions/checkout@v3 @@ -64,12 +63,9 @@ jobs: - name: Package with pyinstaller for ${{matrix.TARGET}} run: ${{matrix.CMD_BUILD}} - - name: Upload assets to release - uses: WebFreak001/upload-asset@v1.0.0 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # automatically provided by github actions - OS: windows # a variable we use in the name pattern? + - name: Validate package for ${{matrix.TARGET}} + run: ./dist/${{ matrix.TARGET }}/${{matrix.OUT_FILE_NAME}} + + - uses: actions/upload-artifact@v3 with: - file: ./dist/${{ matrix.TARGET }}/${{ matrix.OUT_FILE_NAME}} - mime: ${{ matrix.ASSET_MIME}} # required by GitHub API - name: ${{ matrix.OUT_FILE_NAME}} # name pattern to upload the file as + path: ./dist/${{ matrix.TARGET }}/${{ matrix.OUT_FILE_NAME }}/ diff --git a/tabcmd-linux.spec b/tabcmd-linux.spec index c5e367fc..04e381f6 100644 --- a/tabcmd-linux.spec +++ b/tabcmd-linux.spec @@ -8,11 +8,11 @@ print(datas) block_cipher = None a = Analysis( - ['tabcmd\\tabcmd.py'], + ['tabcmd.py'], pathex=[], binaries=[], datas=datas, - hiddenimports=['tableauserverclient', 'requests.packages.urllib3', 'pkg_resources'], + hiddenimports=['tableauserverclient', 'requests', 'urllib3', 'pkg_resources'], hookspath=[], hooksconfig={}, runtime_hooks=[], @@ -31,7 +31,7 @@ exe = EXE( a.zipfiles, a.datas, [], - name='tabcmd-windows', + name='tabcmd', debug=False, bootloader_ignore_signals=False, strip=False, diff --git a/tabcmd-mac.spec b/tabcmd-mac.spec index fee41fa8..8d50c283 100644 --- a/tabcmd-mac.spec +++ b/tabcmd-mac.spec @@ -13,7 +13,7 @@ a = Analysis( pathex=[], binaries=[], datas=datas, - hiddenimports=['tableauserverclient', 'requests.packages.urllib3', 'pkg_resources'], + hiddenimports=['tableauserverclient', 'requests', 'urllib3', 'pkg_resources'], hookspath=[], hooksconfig={}, runtime_hooks=[], @@ -32,19 +32,20 @@ exe = EXE( a.zipfiles, a.datas, [], - name='tabcmd-mac', + name='tabcmd.app', debug=False, bootloader_ignore_signals=False, strip=False, upx=True, - upx_exclude=[], runtime_tmpdir=None, console=True, - disable_windowed_traceback=False, - argv_emulation=False, - target_arch=None, codesign_identity=None, - entitlements_file=None, version='program_metadata.txt', +) + +app = BUNDLE( + exe, + name = 'tabcmd-app', icon='res/tabcmd.icns', + bundle_identifier = None, ) diff --git a/tabcmd-windows.spec b/tabcmd-windows.spec index 03f1dfd1..25acf08c 100644 --- a/tabcmd-windows.spec +++ b/tabcmd-windows.spec @@ -31,7 +31,7 @@ exe = EXE( a.zipfiles, a.datas, [], - name='tabcmd-windows', + name='tabcmd', debug=False, bootloader_ignore_signals=False, strip=False, diff --git a/tabcmd/__main__.py b/tabcmd/__main__.py index 97a969ee..50eace8f 100644 --- a/tabcmd/__main__.py +++ b/tabcmd/__main__.py @@ -2,7 +2,8 @@ try: from tabcmd.tabcmd import main -except ImportError: +except ImportError as e: + print(sys.stderr, e) 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) diff --git a/tabcmd/commands/auth/session.py b/tabcmd/commands/auth/session.py index d0f970a4..986b2c71 100644 --- a/tabcmd/commands/auth/session.py +++ b/tabcmd/commands/auth/session.py @@ -4,6 +4,7 @@ import requests import tableauserverclient as TSC +import urllib3 from urllib3.exceptions import InsecureRequestWarning from tabcmd.commands.constants import Errors @@ -74,7 +75,26 @@ def _update_session_data(self, args): self.no_certcheck = args.no_certcheck or self.no_certcheck self.no_proxy = args.no_proxy or self.no_proxy self.proxy = args.proxy or self.proxy - self.timeout = args.timeout or self.timeout + self.timeout = self.timeout_as_integer(self.logger, args.timeout, self.timeout) + + @staticmethod + def timeout_as_integer(logger, option_1, option_2): + result = None + if option_1: + try: + result = int(option_1) + except Exception as anyE: + result = 0 + if option_2 and (not result or result <= 0): + try: + result = int(option_2) + except Exception as anyE: + result = 0 + if not option_1 and not option_2: + logger.debug(_("setsetting.status").format("timeout", "None")) + elif not result or result <= 0: + logger.warning(_("sessionoptions.errors.bad_timeout").format("--timeout", result)) + return result or 0 @staticmethod def _read_password_from_file(filename): @@ -108,7 +128,7 @@ def _create_new_credential(self, password, credential_type): credentials = self._create_new_token_credential() return credentials else: - Errors.exit_with_error(self.logger, "Couldn't find credentials") + Errors.exit_with_error(self.logger, _("session.errors.missing_arguments").format("")) def _create_new_token_credential(self): if self.token_value: @@ -127,31 +147,60 @@ def _create_new_token_credential(self): else: Errors.exit_with_error(self.logger, _("session.errors.missing_arguments").format("token name")) - def _set_connection_options(self): + def _set_connection_options(self) -> TSC.Server: + self.logger.debug("Setting up request options") # args still to be handled here: # proxy, --no-proxy, # cert - # timeout http_options = {} if self.no_certcheck: - http_options = {"verify": False} - requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning) + http_options["verify"] = False + urllib3.disable_warnings(category=InsecureRequestWarning) + if self.proxy: + # do we catch this error? "sessionoptions.errors.bad_proxy_format" + self.logger.debug("Setting proxy: ", self.proxy) + if self.timeout: + http_options["timeout"] = self.timeout try: - tableau_server = TSC.Server(self.server_url, use_server_version=True, http_options=http_options) + self.logger.debug(http_options) + tableau_server = TSC.Server(self.server_url, http_options=http_options) + except Exception as e: + self.logger.debug( + "Connection args: server {}, site {}, proxy {}, cert {}".format( + self.server_url, self.site_name, self.proxy, self.certificate + ) + ) Errors.exit_with_error(self.logger, "Failed to connect to server", e) + self.logger.debug("Finished setting up connection") return tableau_server - def _create_new_connection(self): + def _verify_server_connection_unauthed(self): + try: + self.tableau_server.use_server_version() + except requests.exceptions.ReadTimeout as timeout_error: + Errors.exit_with_error( + self.logger, + message="Timed out after {} seconds attempting to connect to server".format(self.timeout), + exception=timeout_error, + ) + except requests.exceptions.RequestException as requests_error: + Errors.exit_with_error( + self.logger, message="Error attempting to connect to the server", exception=requests_error + ) + except Exception as e: + Errors.exit_with_error(self.logger, exception=e) + + def _create_new_connection(self) -> TSC.Server: self.logger.info(_("session.new_session")) - self.tableau_server = self._set_connection_options() self._print_server_info() self.logger.info(_("session.connecting")) try: - self.tableau_server.use_server_version() # this will attempt to contact the server + self.tableau_server = self._set_connection_options() except Exception as e: Errors.exit_with_error(self.logger, "Failed to connect to server", e) + return self.tableau_server def _read_existing_state(self): if self._check_json(): @@ -168,7 +217,7 @@ def _print_server_info(self): def _validate_existing_signin(self): self.logger.info(_("session.continuing_session")) - self.tableau_server = self._set_connection_options() + # when do these two messages show up? self.logger.info(_("session.auto_site_login")) try: if self.tableau_server and self.tableau_server.is_signed_in(): response = self.tableau_server.users.get_by_id(self.user_id) @@ -181,7 +230,8 @@ def _validate_existing_signin(self): self.logger.info(_("errors.internal_error.request.message"), e) return None - def _sign_in(self, tableau_auth): + # server connection created, not yet logged in + def _sign_in(self, tableau_auth) -> TSC.Server: self.logger.debug(_("session.login") + self.server_url) self.logger.debug(_("listsites.output").format("", self.username or self.token_name, self.site_name)) try: @@ -245,7 +295,8 @@ def create_session(self, args): if credentials and not signed_in_object: # logging in, not using an existing session - self._create_new_connection() + self.tableau_server = self._create_new_connection() + self._verify_server_connection_unauthed() signed_in_object = self._sign_in(credentials) if not signed_in_object: diff --git a/tabcmd/commands/constants.py b/tabcmd/commands/constants.py index e78daf36..c37cb259 100644 --- a/tabcmd/commands/constants.py +++ b/tabcmd/commands/constants.py @@ -25,7 +25,7 @@ def is_expired_session(error): @staticmethod def is_resource_conflict(error): if hasattr(error, "code"): - return error.code.startswith(Constants.resource_conflict_general) + return error.code == Constants.source_already_exists @staticmethod def is_login_error(error): @@ -86,7 +86,7 @@ def check_common_error_codes_and_explain(logger, exception): # "session.session_expired_login")) # session.renew_session return - if exception.code.startswith(Constants.source_not_found): + if exception.code == 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/export_command.py b/tabcmd/commands/datasources_and_workbooks/export_command.py index f652bcd6..6775181a 100644 --- a/tabcmd/commands/datasources_and_workbooks/export_command.py +++ b/tabcmd/commands/datasources_and_workbooks/export_command.py @@ -32,7 +32,6 @@ def define_args(export_parser): help="page orientation (landscape or portrait) of the exported PDF", ) group.add_argument( - "--pagesize", choices=[ pagesize.A3, diff --git a/tabcmd/commands/datasources_and_workbooks/get_url_command.py b/tabcmd/commands/datasources_and_workbooks/get_url_command.py index 2428f569..83fb158a 100644 --- a/tabcmd/commands/datasources_and_workbooks/get_url_command.py +++ b/tabcmd/commands/datasources_and_workbooks/get_url_command.py @@ -46,6 +46,10 @@ def run_command(args): Errors.exit_with_error(logger, _("export.errors.white_space_workbook_view")) url = args.url.lstrip("/") # strip opening / if present + content_type = GetUrl.evaluate_content_type(logger, url) + file_type = GetUrl.get_file_type_from_filename(logger, url, args.filename) + + GetUrl.get_content_as_file(file_type, content_type, logger, args, server, url) ## this first set of methods is all parsing the url and file input from the user @@ -60,7 +64,6 @@ def evaluate_content_type(logger, url): 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]" @@ -74,13 +77,19 @@ def explain_expected_url(logger, url: str, command: str): Errors.exit_with_error(logger, message) @staticmethod - def get_file_type_from_filename(logger, file_name, url): + def get_file_type_from_filename(logger, url, file_name): + logger.debug("Choosing between {}, {}".format(file_name, url)) file_name = file_name or url logger.debug(_("get.options.file") + ": {}".format(file_name)) type_of_file = GetUrl.get_file_extension(file_name) - if not type_of_file: - Errors.exit_with_error(logger, _("tabcmd.get.extension.not_found").format(file_name)) + if not type_of_file and file_name is not None: + # check the url + backup = GetUrl.get_file_extension(url) + if backup is not None: + type_of_file = backup + else: + Errors.exit_with_error(logger, _("tabcmd.get.extension.not_found").format(file_name)) logger.debug("filetype: {}".format(type_of_file)) if type_of_file in ["pdf", "csv", "png", "twb", "twbx", "tdsx"]: @@ -134,7 +143,9 @@ def get_view_url(url, logger): # "views/wb-name/view-name" -> wb-name/sheets/vi @staticmethod def filename_from_args(file_argument, item_name, filetype): if file_argument is None: - file_argument = "{}.{}".format(item_name, filetype) + file_argument = item_name + if not file_argument.endswith(filetype): + file_argument = "{}.{}".format(file_argument, filetype) return file_argument ## methods below here have done all the parsing and just have to do the download and saving @@ -142,6 +153,7 @@ def filename_from_args(file_argument, item_name, filetype): @staticmethod def get_content_as_file(file_type, content_type, logger, args, server, url): + logger.debug("fetching {} as {}".format(content_type, file_type)) if content_type == "workbook": return GetUrl.generate_twb(logger, server, args, file_type, url) elif content_type == "datasource": @@ -207,9 +219,12 @@ def generate_twb(logger, server, args, file_extension, url): target_workbook = GetUrl.get_wb_by_content_url(logger, server, workbook_name) logger.debug(_("content_type.workbook") + ": {}".format(workbook_name)) file_name_with_path = GetUrl.filename_from_args(args.filename, workbook_name, file_extension) - 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)) + # the download method will add an extension. How do I tell which one? + file_name_with_path = GetUrl.get_name_without_possible_extension(file_name_with_path) + file_name_with_ext = "{}.{}".format(file_name_with_path, file_extension) + logger.debug("Saving as {}".format(file_name_with_ext)) + server.workbooks.download(target_workbook.id, filepath=file_name_with_path, no_extract=False) + logger.info(_("export.success").format(target_workbook.name, file_name_with_ext)) except Exception as e: Errors.exit_with_error(logger, e) @@ -221,8 +236,11 @@ def generate_tds(logger, server, args, file_extension): 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)) + # the download method will add an extension + file_name_with_path = GetUrl.get_name_without_possible_extension(file_name_with_path) + file_name_with_ext = "{}.{}".format(file_name_with_path, file_extension) + logger.debug("Saving as {}".format(file_name_with_ext)) + server.datasources.download(target_datasource.id, filepath=file_name_with_path, no_extract=False) + logger.info(_("export.success").format(target_datasource.name, file_name_with_ext)) except Exception as e: Errors.exit_with_error(logger, e) diff --git a/tabcmd/commands/project/create_project_command.py b/tabcmd/commands/project/create_project_command.py index f1bec5b0..03a7477d 100644 --- a/tabcmd/commands/project/create_project_command.py +++ b/tabcmd/commands/project/create_project_command.py @@ -44,6 +44,7 @@ def run_command(args): logger.debug("parent project = `{0}`, id = {1}".format(args.parent_project_path, parent_id)) logger.info(_("createproject.status").format(readable_name)) new_project = TSC.ProjectItem(args.project_name, args.description, None, parent_id) + project_item = None try: project_item = server.projects.create(new_project) logger.info(_("common.output.succeeded")) @@ -51,5 +52,8 @@ def run_command(args): 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) + logger.info(_("common.output.succeeded")) + else: + Errors.exit_with_error(logger, e) + + return project_item diff --git a/tabcmd/commands/user/create_site_users.py b/tabcmd/commands/user/create_site_users.py index 2e470544..7a01af21 100644 --- a/tabcmd/commands/user/create_site_users.py +++ b/tabcmd/commands/user/create_site_users.py @@ -1,6 +1,7 @@ import tableauserverclient as TSC from tabcmd.commands.auth.session import Session +from tabcmd.commands.constants import Errors from tabcmd.execution.global_options import * from tabcmd.execution.localize import _ from tabcmd.execution.logger_config import log @@ -20,9 +21,10 @@ class CreateSiteUsersCommand(UserCommand): @staticmethod def define_args(create_site_users_parser): args_group = create_site_users_parser.add_argument_group(title=CreateSiteUsersCommand.name) - set_role_arg(args_group) + UserCommand.set_role_arg(args_group) set_users_file_positional(args_group) set_completeness_options(args_group) + UserCommand.set_auth_arg(args_group) @staticmethod def run_command(args): @@ -38,23 +40,33 @@ def run_command(args): UserCommand.validate_file_for_import(args.filename, logger, detailed=True, strict=args.require_all_valid) - logger.info(_("tabcmd.add.users.to_x").format(args.filename.name, creation_site)) + logger.info(_("tabcmd.add.users.to_site").format(args.filename.name, creation_site)) user_obj_list = UserCommand.get_users_from_file(args.filename, logger) logger.info(_("session.monitorjob.percent_complete").format(0)) error_list = [] for user_obj in user_obj_list: try: + if args.role: + user_obj.site_role = args.role # tsc is case sensitive + if args.auth_type: + user_obj.auth_setting = args.auth_type number_of_users_listed += 1 result = server.users.add(user_obj) logger.info(_("tabcmd.result.success.create_user").format(user_obj.name)) number_of_users_added += 1 except TSC.ServerResponseError as e: - number_of_errors += 1 - error_list.append(e) logger.debug(e) + if Errors.is_resource_conflict(e) and args.continue_if_exists: + logger.debug(_("createsite.errors.site_name_already_exists").format(user_obj.name)) + else: + number_of_errors += 1 + logger.debug(number_of_errors) + error_list.append(e.summary + ": " + e.detail) + logger.debug(error_list) logger.info(_("session.monitorjob.percent_complete").format(100)) logger.info(_("importcsvsummary.line.processed").format(number_of_users_listed)) logger.info(_("importcsvsummary.line.skipped").format(number_of_errors)) logger.info(_("importcsvsummary.users.added.count").format(number_of_users_added)) if number_of_errors > 0: - logger.info(_("importcsvsummary.error.details").format(error_list)) + logger.info(_("importcsvsummary.error.details")) + logger.info(error_list) diff --git a/tabcmd/commands/user/create_users_command.py b/tabcmd/commands/user/create_users_command.py index 66414427..71468f39 100644 --- a/tabcmd/commands/user/create_users_command.py +++ b/tabcmd/commands/user/create_users_command.py @@ -21,9 +21,10 @@ class CreateUsersCommand(UserCommand): @staticmethod def define_args(create_users_parser): args_group = create_users_parser.add_argument_group(title=CreateUsersCommand.name) - set_role_arg(args_group) + UserCommand.set_role_arg(args_group) set_users_file_positional(args_group) set_completeness_options(args_group) + UserCommand.set_auth_arg(args_group) @staticmethod def run_command(args): @@ -35,6 +36,10 @@ def run_command(args): number_of_users_added = 0 number_of_errors = 0 + ## TODO + # If the server has only one site (the default site), the user is created and added to the site. + # If the server has multiple sites, the user is created but is not added to any site. + # To add users to a site, use createsiteusers. if args.site_name: creation_site = args.site_name else: @@ -49,12 +54,19 @@ def run_command(args): for user_obj in user_obj_list: try: number_of_users_listed += 1 - # TODO: bring in other attributes in file, actually act on specific site - new_user = TSC.UserItem(user_obj.name, args.role) + if args.role: + user_obj.site_role = args.role + if args.auth_type: + user_obj.auth_setting = args.auth_type + new_user = TSC.UserItem(user_obj.name) server.users.add(new_user) logger.info(_("tabcmd.result.success.create_user").format(user_obj.name)) number_of_users_added += 1 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)) + continue + number_of_errors += 1 error_list.append(e) logger.debug(e) diff --git a/tabcmd/commands/user/user_data.py b/tabcmd/commands/user/user_data.py index a95448d7..cab01427 100644 --- a/tabcmd/commands/user/user_data.py +++ b/tabcmd/commands/user/user_data.py @@ -9,6 +9,7 @@ from tabcmd.commands.constants import Errors from tabcmd.commands.server import Server from tabcmd.execution.localize import _ +from tabcmd.execution.global_options import case_insensitive_string_type class Userdata: @@ -58,9 +59,24 @@ def to_tsc_user(self) -> TSC.UserItem: ["system", "site", "none", "no"], # admin ["yes", "true", "1", "no", "false", "0"], # publisher [], - [TSC.UserItem.Auth.SAML, TSC.UserItem.Auth.OpenID, TSC.UserItem.Auth.ServerDefault], # auth ] +site_roles = [ + "ServerAdministrator", + "SiteAdministratorCreator", + "SiteAdministratorExplorer", + "SiteAdministrator", + "Creator", + "ExplorerCanPublish", + "Publisher", + "Explorer", + "Interactor", + "Viewer", + "Unlicensed", +] + +auth_types = ["Local", TSC.UserItem.Auth.SAML, TSC.UserItem.Auth.OpenID, TSC.UserItem.Auth.ServerDefault, "TableauId"] + # username, password, display_name, license, admin_level, publishing, email, auth type class Column(IntEnum): @@ -71,9 +87,8 @@ class Column(IntEnum): ADMIN = 4 PUBLISHER = 5 EMAIL = 6 - AUTH = 7 - MAX = 7 + MAX = 7 # number of columns class UserCommand(Server): @@ -81,6 +96,32 @@ class UserCommand(Server): This class acts as a base class for user related group of commands """ + @staticmethod + def set_role_arg(parser): + parser.add_argument( + "-r", + "--role", + choices=site_roles, + type=case_insensitive_string_type(site_roles), + help="Specifies a site role for all users in the .csv file. Possible roles: " + ", ".join(site_roles), + metavar="SITE_ROLE", + ) + return parser + + @staticmethod + def set_auth_arg(parser): + parser.add_argument( + "--auth-type", + metavar="TYPE", + choices=auth_types, + type=case_insensitive_string_type(auth_types), + # default="TableauID", # default is Local for on-prem, TableauID for Online. Does the server apply the default? + help="Assigns the authentication type for all users in the CSV file. \ + For Tableau Online, TYPE may be TableauID (default) or SAML. \ + For Tableau Server, TYPE may be Local (default) or SAML.", + ) + return parser + # read the file containing usernames or user details and validate each line # log out any errors encountered # returns the number of valid lines in the file diff --git a/tabcmd/execution/global_options.py b/tabcmd/execution/global_options.py index 8292ab20..2657829d 100644 --- a/tabcmd/execution/global_options.py +++ b/tabcmd/execution/global_options.py @@ -25,6 +25,20 @@ """ +# argparse does case-sensitive comparisons of string inputs by default +# I want the user to be able to enter e.g. "viewer" and have it accepted as "Viewer" +# https://stackoverflow.com/questions/56838004/ +# case-insensitive-argparse-choices-without-losing-case-information-in-choices-lis +def case_insensitive_string_type(choices): + def find_choice(choice): + for key, item in enumerate([choice.lower() for choice in choices]): + if choice.lower() == item: + return choices[key] + else: + return choice + + return find_choice + def set_parent_project_arg(parser): parser.add_argument("--parent-project-path", default=None, help="path of parent project") @@ -65,33 +79,6 @@ def set_no_wait_option(parser): return parser -site_roles = [ - "ServerAdministrator", - "SiteAdministratorCreator", - "SiteAdministratorExplorer", - "SiteAdministrator", - "Creator", - "ExplorerCanPublish", - "Publisher", - "Explorer", - "Interactor", - "Viewer", - "Unlicensed", -] - - -def set_role_arg(parser): - parser.add_argument( - "-r", - "--role", - 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 - - def set_silent_option(parser): parser.add_argument( "--silent-progress", action="store_true", help="Do not display progress messages for the command." @@ -118,10 +105,10 @@ def set_completeness_options(parser): # used in create/delete extract -# docs don't say it, but --embedded-datasources and --include-all could be mutually exclusive def set_embedded_datasources_options(parser): + # one of these is required IFF we are using a workbook instead of datasource embedded_group = parser.add_mutually_exclusive_group() - embedded_group.add_argument( + embedded_group.add_argument( # nargs? "--embedded-datasources", help="A space-separated list of embedded data source names within the target workbook.", ) @@ -189,10 +176,12 @@ def set_ds_xor_wb_options(parser): # pass arguments for either --datasource or --workbook -def set_ds_xor_wb_args(parser): +def set_ds_xor_wb_args(parser, url=False): target_type_group = parser.add_mutually_exclusive_group(required=True) target_type_group.add_argument("-d", "--datasource", help="The name of the target datasource.") target_type_group.add_argument("-w", "--workbook", help="The name of the target workbook.") + if url: + target_type_group.add_argument("--url", "-U", help=_("deleteextracts.options.url")) return parser @@ -241,16 +230,17 @@ def set_common_site_args(parser): help="In MB, the amount of data that can be stored on the site.", ) + encryption_modes = ["enforced", "enabled", "disabled"] parser.add_argument( "--extract-encryption-mode", - choices=["enforced", "enabled", "disabled"], + choices=encryption_modes, + type=case_insensitive_string_type(encryption_modes), help="The extract encryption mode for the site can be enforced, enabled or disabled. ", ) parser.add_argument( "--run-now-enabled", choices=["true", "false"], - type=str.lower, help="Allow or deny users from running extract refreshes, flows, or schedules manually.", ) return parser @@ -332,6 +322,7 @@ def set_publish_args(parser): 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() append_group.add_argument( @@ -405,11 +396,14 @@ def set_target_users_arg(parser): # sync-group +license_modes = ["on-login", "on-sync"] + + def set_update_group_args(parser): parser.add_argument( "--grant-license-mode", - choices=["on-login", "on-sync"], - type=str.lower, + choices=license_modes, + type=case_insensitive_string_type(license_modes), help="Specifies whether a role should be granted on sign in. ", ) parser.add_argument( diff --git a/tabcmd/execution/parent_parser.py b/tabcmd/execution/parent_parser.py index a2495792..0aa6ee75 100644 --- a/tabcmd/execution/parent_parser.py +++ b/tabcmd/execution/parent_parser.py @@ -112,7 +112,7 @@ def parent_parser_with_global_options(): # general behavioral options parser.add_argument( "--continue-if-exists", - action="store_false", + action="store_true", # default behavior matches old tabcmd help=strings[9], ) diff --git a/tabcmd/locales/en/LC_MESSAGES/tabcmd.po b/tabcmd/locales/en/LC_MESSAGES/tabcmd.po index 1fae5004..ad479db8 100644 --- a/tabcmd/locales/en/LC_MESSAGES/tabcmd.po +++ b/tabcmd/locales/en/LC_MESSAGES/tabcmd.po @@ -1854,30 +1854,6 @@ msgstr "name cannot be empty" msgid "askdata.title" msgstr "Ask Data" -#: -msgid "slack.app.upgrade.email.notification.subject" -msgstr "Tableau App for Slack Update" - -#: -msgid "slack.app.upgrade.email.notification.introduction" -msgstr "An update is available for the Tableau app for Slack and can be reinstalled now to work with the next Tableau Online release. Tableau recommends reinstalling the app to maintain app performance and use new features. Look for new features and changes in