From f1f6bbec8b877994b97c77f50759dcd1525aabb9 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Mon, 21 Nov 2022 11:54:46 -0800 Subject: [PATCH 01/12] different output based on switch --- tabcmd/commands/help/help_command.py | 80 ++++----- tabcmd/execution/parent_parser.py | 239 ++++++++++++++------------- 2 files changed, 156 insertions(+), 163 deletions(-) diff --git a/tabcmd/commands/help/help_command.py b/tabcmd/commands/help/help_command.py index 6c5409d8..0d4b35c0 100644 --- a/tabcmd/commands/help/help_command.py +++ b/tabcmd/commands/help/help_command.py @@ -1,6 +1,4 @@ import argparse -from typing import Any, List - from tabcmd.execution.localize import _ from tabcmd.execution.logger_config import log @@ -11,7 +9,14 @@ class HelpCommand: """ name: str = "help" - description: str = "Show Help and exit" + description = "Show message listing commands and global options, then exit" + usage = ( + "tabcmd help -- Show message listing commands and global options, then exit\n" + "tabcmd -- Run a specific command {0}\n" + "tabcmd -h -- Show Help for a specific command\n\n" + "More help: https://tableau.github.io/tabcmd/\n\n" + ) + @staticmethod def define_args(parser): @@ -19,56 +24,37 @@ def define_args(parser): @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 + # delayed import because cyclical - commands shouldn't generally reference the command structure from tabcmd.execution.map_of_commands import CommandsMap + from tabcmd.execution.parent_parser import version + logger = log(__name__, args.logging_level) + logger.debug(_("tabcmd.launching")) + all_commands = CommandsMap.commands_hash_map - 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" - ) + logger.info("tabcmd - Tableau Server Command Line Utility {0} \n".format(version)) + logger.info("Usage:\n") + logger.info(HelpCommand.usage.format("(see list below)")) if args.help_option: + # identify if the command was 'tabcmd help -h' - they just want instructions for running help + if args.help_option == "-h": + exit(0) - 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) + logger.info(all_commands.a) + cmd = list(all_commands).filter(lambda command: command.name == args.help_option, all_commands) + if cmd is not None: + from tabcmd.execution.parent_parser import ParentParser + parser = ParentParser.get_command_args(cmd) + logger.info(parser.print_help()) - 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: + logger.info("Tabcmd commands:\n") for cmd in all_commands: - logger.info(cmd.name + ": " + cmd.description) + logger.info("\t" + cmd.name + ": " + cmd.description) + logger.info("\nGlobal settings") + + from tabcmd.execution.parent_parser import parent_parser_with_global_options + parser = parent_parser_with_global_options() + logger.info(parser.print_help()) + logger.info(HelpCommand.usage.format("")) diff --git a/tabcmd/execution/parent_parser.py b/tabcmd/execution/parent_parser.py index 57b6d6b2..7f823bae 100644 --- a/tabcmd/execution/parent_parser.py +++ b/tabcmd/execution/parent_parser.py @@ -12,13 +12,131 @@ pass +# ordered alphabetically by short option - this is reflected directly in help output +def parent_parser_with_global_options(): + 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"), + ) + + # let argparse show its default help for -h + + 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 + + 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.global_options = parent_parser_with_global_options() self.root = argparse.ArgumentParser(parents=[self.global_options]) # https://stackoverflow.com/questions/7498595/python-argparse-add-argument-to-multiple-subparsers self.subparsers = self.root.add_subparsers() @@ -35,120 +153,9 @@ def include(self, 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_true", - 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") - ) + def get_command_args(self, command): + root = argparse.ArgumentParser() + command_parser = root.subparsers.add_parser(command.name, help=command.description) + return command_parser - 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 From 9daa1f8c79c0b450906a1a7f819416648a967a1c Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Tue, 29 Nov 2022 22:08:58 -0800 Subject: [PATCH 02/12] hide token-value like we do password --- tabcmd/execution/tabcmd_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tabcmd/execution/tabcmd_controller.py b/tabcmd/execution/tabcmd_controller.py index 0dee0ad0..25d47830 100644 --- a/tabcmd/execution/tabcmd_controller.py +++ b/tabcmd/execution/tabcmd_controller.py @@ -29,7 +29,7 @@ def run(parser, user_input=None): 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) From 8707617d6d065416641a8f405d2ebe437347bc77 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Tue, 29 Nov 2022 22:09:10 -0800 Subject: [PATCH 03/12] remove filename from normal output --- tabcmd/execution/logger_config.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tabcmd/execution/logger_config.py b/tabcmd/execution/logger_config.py index 8a42e2cb..f8e0188f 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,10 @@ 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: + print("error in the next line: str cannot be assigned or something?") + log_format[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" ) From 5fe7d58aa3d2e809b3921d8f8ab0583d80148046 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Tue, 29 Nov 2022 23:32:35 -0800 Subject: [PATCH 04/12] tweak loading error message Shouldn't be relevant except during development, but it's always nice to be accurate --- tabcmd/__main__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tabcmd/__main__.py b/tabcmd/__main__.py index 29886963..0c800dea 100644 --- a/tabcmd/__main__.py +++ b/tabcmd/__main__.py @@ -3,7 +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("Error importing dependencies.") + print("Possible cause: Tabcmd needs to be run as a module, it cannot be run as a script") print("Try running python -m tabcmd") sys.exit(1) From 34e9f580667e01cc762a81155479e54ddc076ce2 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Tue, 29 Nov 2022 23:35:01 -0800 Subject: [PATCH 05/12] rewrite 'help' handling - Delete help command - Move command registration into parent_parser - Move help output into parent_parser - Refactor parent_parser to present args in groups for help - Add long explanatory comment - Extract all user facing text in parent_parser to enable translation --- tabcmd/commands/help/__init__.py | 0 tabcmd/commands/help/help_command.py | 60 -------- tabcmd/execution/map_of_commands.py | 2 - tabcmd/execution/parent_parser.py | 194 ++++++++++++++++++-------- tabcmd/execution/tabcmd_controller.py | 6 +- 5 files changed, 134 insertions(+), 128 deletions(-) delete mode 100644 tabcmd/commands/help/__init__.py delete mode 100644 tabcmd/commands/help/help_command.py 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 0d4b35c0..00000000 --- a/tabcmd/commands/help/help_command.py +++ /dev/null @@ -1,60 +0,0 @@ -import argparse -from tabcmd.execution.localize import _ -from tabcmd.execution.logger_config import log - - -class HelpCommand: - """ - Command to show user help options - """ - - name: str = "help" - description = "Show message listing commands and global options, then exit" - usage = ( - "tabcmd help -- Show message listing commands and global options, then exit\n" - "tabcmd -- Run a specific command {0}\n" - "tabcmd -h -- Show Help for a specific command\n\n" - "More help: https://tableau.github.io/tabcmd/\n\n" - ) - - - @staticmethod - def define_args(parser): - parser.add_argument("help_option", nargs="?") - - @staticmethod - def run_command(args: argparse.Namespace): - # delayed import because cyclical - commands shouldn't generally reference the command structure - from tabcmd.execution.map_of_commands import CommandsMap - from tabcmd.execution.parent_parser import version - logger = log(__name__, args.logging_level) - logger.debug(_("tabcmd.launching")) - all_commands = CommandsMap.commands_hash_map - - logger.info("tabcmd - Tableau Server Command Line Utility {0} \n".format(version)) - logger.info("Usage:\n") - logger.info(HelpCommand.usage.format("(see list below)")) - - if args.help_option: - # identify if the command was 'tabcmd help -h' - they just want instructions for running help - if args.help_option == "-h": - exit(0) - - logger.info(all_commands.a) - cmd = list(all_commands).filter(lambda command: command.name == args.help_option, all_commands) - if cmd is not None: - from tabcmd.execution.parent_parser import ParentParser - parser = ParentParser.get_command_args(cmd) - logger.info(parser.print_help()) - - - else: - logger.info("Tabcmd commands:\n") - for cmd in all_commands: - logger.info("\t" + cmd.name + ": " + cmd.description) - logger.info("\nGlobal settings") - - from tabcmd.execution.parent_parser import parent_parser_with_global_options - parser = parent_parser_with_global_options() - logger.info(parser.print_help()) - logger.info(HelpCommand.usage.format("")) 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 7f823bae..70fcec5b 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,50 +17,44 @@ pass -# ordered alphabetically by short option - this is reflected directly in help output +""" +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) - 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._optionals.title = strings[0] - parser.add_argument( - "--continue-if-exists", - action="store_false", - help="Treat resource conflicts as item creation success e.g project already exists", + 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"), ) - 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.", + formatting_group2 = parser.add_mutually_exclusive_group() + formatting_group2.add_argument( + "-t", "--site", default="", dest="site_name", metavar="SITEID", help=_("session.options.site") ) - 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.", + help=strings[13] ) auth_options.add_argument( "-u", "--username", default=None, metavar="", help=_("session.options.username") @@ -66,7 +65,7 @@ def parent_parser_with_global_options(): "--token-value", default=None, metavar="", - help="Use the specified Tableau Server Personal Access Token. Requires --token-name to be set.", + help=strings[12], ) secret_values.add_argument( "-p", "--password", default=None, metavar="", help=_("session.options.password") @@ -74,6 +73,31 @@ def parent_parser_with_global_options(): 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( @@ -85,48 +109,51 @@ def parent_parser_with_global_options(): 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( + 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"), ) - # TODO get the list of choices dynamically? + # general behavioral options 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", + "--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"), ) - # let argparse show its default help for -h + 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="Tableau Server Command Line Utility v" + version + "\n \n", - help="Show version information and exit.", + version=strings[6] + "v" + version + "\n \n", + help=strings[7], ) - return parser @@ -137,25 +164,68 @@ class ParentParser: def __init__(self): self.global_options = parent_parser_with_global_options() - self.root = argparse.ArgumentParser(parents=[self.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): + 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 - def get_command_args(self, command): - root = argparse.ArgumentParser() - command_parser = root.subparsers.add_parser(command.name, help=command.description) - return command_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=show_help(self)) + +def show_help(parser: ParentParser): + logger = log(__name__, "info") + logger.info(strings[6] + version + "\n") + logger.info(parser.root.format_help()) + exit(0) + + +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 help'.", # 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 25d47830..8b958efa 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 @@ -11,9 +11,7 @@ class TabcmdController: def initialize(): manager = ParentParser() parent = manager.get_root_parser() - commands = CommandsMap.commands_hash_map - for command in commands: - manager.include(command) + manager.include_help() return parent # during normal execution, leaving input as none will default to sys.argv From efb05fd3cf65399de471a1b3a55996d3ed21fcf6 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Wed, 30 Nov 2022 00:20:01 -0800 Subject: [PATCH 06/12] update help output per command So that arguments for the command show up when you run tabcmd command -h e.g behavioral arguments: ... listsites: --get-extract-encryption-mode Include the extract encryption mode for each site. --- .../delete_command.py | 9 ++++---- .../export_command.py | 19 +++++++++------- .../get_url_command.py | 5 +++-- .../publish_command.py | 9 ++++---- .../runschedule_command.py | 3 ++- .../extracts/create_extracts_command.py | 13 ++++++----- .../extracts/decrypt_extracts_command.py | 3 ++- .../extracts/delete_extracts_command.py | 13 ++++++----- .../extracts/encrypt_extracts_command.py | 3 ++- .../extracts/reencrypt_extracts_command.py | 3 ++- .../extracts/refresh_extracts_command.py | 12 +++++----- tabcmd/commands/group/create_group_command.py | 3 ++- tabcmd/commands/group/delete_group_command.py | 3 ++- .../project/create_project_command.py | 3 ++- .../project/delete_project_command.py | 5 +++-- .../project/publish_samples_command.py | 5 +++-- tabcmd/commands/site/create_site_command.py | 5 +++-- tabcmd/commands/site/delete_site_command.py | 3 ++- tabcmd/commands/site/edit_site_command.py | 10 ++++----- tabcmd/commands/site/list_command.py | 3 ++- tabcmd/commands/site/list_sites_command.py | 3 ++- tabcmd/commands/user/add_users_command.py | 7 +++--- tabcmd/commands/user/create_site_users.py | 7 +++--- tabcmd/commands/user/create_users_command.py | 7 +++--- .../user/delete_site_users_command.py | 5 +++-- tabcmd/commands/user/remove_users_command.py | 7 +++--- tabcmd/execution/global_options.py | 7 +++--- tabcmd/execution/parent_parser.py | 22 +++++++++++++------ 28 files changed, 117 insertions(+), 80 deletions(-) diff --git a/tabcmd/commands/datasources_and_workbooks/delete_command.py b/tabcmd/commands/datasources_and_workbooks/delete_command.py index 8e68cb41..3815b86b 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): diff --git a/tabcmd/commands/datasources_and_workbooks/export_command.py b/tabcmd/commands/datasources_and_workbooks/export_command.py index 27d5d619..f50f2772 100644 --- a/tabcmd/commands/datasources_and_workbooks/export_command.py +++ b/tabcmd/commands/datasources_and_workbooks/export_command.py @@ -16,20 +16,22 @@ 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 +49,17 @@ 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( + group.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("--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", diff --git a/tabcmd/commands/datasources_and_workbooks/get_url_command.py b/tabcmd/commands/datasources_and_workbooks/get_url_command.py index 688426d9..c4ec13ec 100644 --- a/tabcmd/commands/datasources_and_workbooks/get_url_command.py +++ b/tabcmd/commands/datasources_and_workbooks/get_url_command.py @@ -19,8 +19,9 @@ class GetUrl(DatasourcesAndWorkbooks): @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 diff --git a/tabcmd/commands/datasources_and_workbooks/publish_command.py b/tabcmd/commands/datasources_and_workbooks/publish_command.py index 59432ee1..c529604b 100644 --- a/tabcmd/commands/datasources_and_workbooks/publish_command.py +++ b/tabcmd/commands/datasources_and_workbooks/publish_command.py @@ -20,14 +20,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): 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..8f09ba90 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): diff --git a/tabcmd/commands/extracts/decrypt_extracts_command.py b/tabcmd/commands/extracts/decrypt_extracts_command.py index 6a983a06..0fd0b1e3 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): diff --git a/tabcmd/commands/extracts/delete_extracts_command.py b/tabcmd/commands/extracts/delete_extracts_command.py index 400d7c7a..e4e3dcc6 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): diff --git a/tabcmd/commands/extracts/encrypt_extracts_command.py b/tabcmd/commands/extracts/encrypt_extracts_command.py index 2d3b7ae4..a3507624 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): diff --git a/tabcmd/commands/extracts/reencrypt_extracts_command.py b/tabcmd/commands/extracts/reencrypt_extracts_command.py index 66785bfe..37c1c121 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): diff --git a/tabcmd/commands/extracts/refresh_extracts_command.py b/tabcmd/commands/extracts/refresh_extracts_command.py index 562554ba..290e6cf3 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): diff --git a/tabcmd/commands/group/create_group_command.py b/tabcmd/commands/group/create_group_command.py index 489bf4db..8eea4198 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): diff --git a/tabcmd/commands/group/delete_group_command.py b/tabcmd/commands/group/delete_group_command.py index 6d2741f1..d2c8ccf0 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): diff --git a/tabcmd/commands/project/create_project_command.py b/tabcmd/commands/project/create_project_command.py index b1838b48..be5d2507 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) diff --git a/tabcmd/commands/project/delete_project_command.py b/tabcmd/commands/project/delete_project_command.py index 9ea30d8d..4449677d 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): 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/site/create_site_command.py b/tabcmd/commands/site/create_site_command.py index e8ca8c03..b54d57d0 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): diff --git a/tabcmd/commands/site/delete_site_command.py b/tabcmd/commands/site/delete_site_command.py index babc5f41..f0d6b0b9 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=strings[2]) + args_group = delete_site_parser.add_argument_group(title=DeleteSiteCommand.name) + args_group.add_argument("site_name_to_delete", metavar="site-name", help=help=strings[2]) @staticmethod def run_command(args): diff --git a/tabcmd/commands/site/edit_site_command.py b/tabcmd/commands/site/edit_site_command.py index cbd58f31..6f710039 100644 --- a/tabcmd/commands/site/edit_site_command.py +++ b/tabcmd/commands/site/edit_site_command.py @@ -19,13 +19,13 @@ 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( + 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(edit_site_parser) - set_site_status_arg(edit_site_parser) + set_common_site_args(args_group) + set_site_status_arg(args_group) @staticmethod def run_command(args): diff --git a/tabcmd/commands/site/list_command.py b/tabcmd/commands/site/list_command.py index 2eff8ec5..f2af2a72 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): diff --git a/tabcmd/commands/site/list_sites_command.py b/tabcmd/commands/site/list_sites_command.py index 55e69f85..be532584 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): 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/execution/global_options.py b/tabcmd/execution/global_options.py index 61467455..a5d8d095 100644 --- a/tabcmd/execution/global_options.py +++ b/tabcmd/execution/global_options.py @@ -65,7 +65,6 @@ def set_no_wait_option(parser): return parser -# TODO make this lower case? def set_role_arg(parser): parser.add_argument( "-r", @@ -83,6 +82,7 @@ def set_role_arg(parser): "Viewer", "Unlicensed", ], + type=str.lower, help="Specifies a site role for all users in the .csv file.", ) return parser @@ -244,8 +244,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 diff --git a/tabcmd/execution/parent_parser.py b/tabcmd/execution/parent_parser.py index 70fcec5b..45cedd2d 100644 --- a/tabcmd/execution/parent_parser.py +++ b/tabcmd/execution/parent_parser.py @@ -199,13 +199,21 @@ def include_help(self): "help", help=strings[14], parents=[self.global_options] ) additional_parser._optionals.title = strings[1] - additional_parser.set_defaults(func=show_help(self)) + additional_parser.set_defaults(func=Help(self)) -def show_help(parser: ParentParser): - logger = log(__name__, "info") - logger.info(strings[6] + version + "\n") - logger.info(parser.root.format_help()) - exit(0) + +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()) + exit(0) strings = [ @@ -213,7 +221,7 @@ def show_help(parser: ParentParser): "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 help'.", # 4 + "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 From 90b822db26761c683c0003afaa1655c3d4cbfc05 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Wed, 30 Nov 2022 00:27:18 -0800 Subject: [PATCH 07/12] fix merge --- tabcmd/commands/site/delete_site_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tabcmd/commands/site/delete_site_command.py b/tabcmd/commands/site/delete_site_command.py index f0d6b0b9..2d7599da 100644 --- a/tabcmd/commands/site/delete_site_command.py +++ b/tabcmd/commands/site/delete_site_command.py @@ -18,7 +18,7 @@ class DeleteSiteCommand(Server): @staticmethod def define_args(delete_site_parser): args_group = delete_site_parser.add_argument_group(title=DeleteSiteCommand.name) - args_group.add_argument("site_name_to_delete", metavar="site-name", help=help=strings[2]) + args_group.add_argument("site_name_to_delete", metavar="site-name", help=strings[2]) @staticmethod def run_command(args): From e95a8c3c9a90dd5cce56e2433c6ccdb29d59d750 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Wed, 30 Nov 2022 00:45:45 -0800 Subject: [PATCH 08/12] make tests case insensitive --- tabcmd/execution/global_options.py | 35 ++++++++++++++---------- tests/parsers/test_parser_create_user.py | 2 +- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/tabcmd/execution/global_options.py b/tabcmd/execution/global_options.py index a5d8d095..ddeff33a 100644 --- a/tabcmd/execution/global_options.py +++ b/tabcmd/execution/global_options.py @@ -65,25 +65,29 @@ 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=[ - "ServerAdministrator", - "SiteAdministratorCreator", - "SiteAdministratorExplorer", - "SiteAdministrator", - "Creator", - "ExplorerCanPublish", - "Publisher", - "Explorer", - "Interactor", - "Viewer", - "Unlicensed", - ], + choices=list(map(lambda x: x.lower(), site_roles)), type=str.lower, - help="Specifies a site role for all users in the .csv file.", + 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,7 +249,7 @@ def set_common_site_args(parser): parser.add_argument( "--run-now-enabled", - choices=["True", "False"], + choices=["true", "false"], type=str.lower, help="Allow or deny users from running extract refreshes, flows, or schedules manually.", ) diff --git a/tests/parsers/test_parser_create_user.py b/tests/parsers/test_parser_create_user.py index 440d430d..7b67f24f 100644 --- a/tests/parsers/test_parser_create_user.py +++ b/tests/parsers/test_parser_create_user.py @@ -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 From 8afe7a96023dd587532a32299ca9592ec4032c54 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Wed, 30 Nov 2022 00:46:03 -0800 Subject: [PATCH 09/12] fix log config --- tabcmd/execution/logger_config.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tabcmd/execution/logger_config.py b/tabcmd/execution/logger_config.py index f8e0188f..17fe6907 100644 --- a/tabcmd/execution/logger_config.py +++ b/tabcmd/execution/logger_config.py @@ -46,8 +46,7 @@ def configure_log(name: str, logging_level_input: str): logging_level = getattr(logging, logging_level_input.upper()) log_format = FORMATS[logging_level] if logging_level is not logging.INFO: - print("error in the next line: str cannot be assigned or something?") - log_format[logging.INFO] = "%(filename)-10s: %(message)-30s" + 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" From e95072a775a7107de7fdc8364b401a4418cd6c67 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Wed, 30 Nov 2022 00:46:17 -0800 Subject: [PATCH 10/12] remove help command from tests --- tabcmd/execution/parent_parser.py | 3 +-- tests/commands/test_run_commands.py | 8 -------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/tabcmd/execution/parent_parser.py b/tabcmd/execution/parent_parser.py index 45cedd2d..48f6a4d3 100644 --- a/tabcmd/execution/parent_parser.py +++ b/tabcmd/execution/parent_parser.py @@ -213,8 +213,7 @@ def run_command(self, args): logger = log(__name__, "info") logger.info(strings[6] + " " + version + "\n") logger.info(self.parser.root.format_help()) - exit(0) - + strings = [ "global behavioral arguments", # 0 - global_behavior_args diff --git a/tests/commands/test_run_commands.py b/tests/commands/test_run_commands.py index 22b9dda0..a7e03597 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, @@ -252,13 +251,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) From 959031c347a12ed65ff4ba276bea4310fd9eb718 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Wed, 30 Nov 2022 00:50:54 -0800 Subject: [PATCH 11/12] format with black --- .../export_command.py | 4 +- tabcmd/commands/site/edit_site_command.py | 4 +- tabcmd/execution/global_options.py | 26 ++++++------- tabcmd/execution/parent_parser.py | 37 ++++++------------- 4 files changed, 27 insertions(+), 44 deletions(-) diff --git a/tabcmd/commands/datasources_and_workbooks/export_command.py b/tabcmd/commands/datasources_and_workbooks/export_command.py index f50f2772..6775181a 100644 --- a/tabcmd/commands/datasources_and_workbooks/export_command.py +++ b/tabcmd/commands/datasources_and_workbooks/export_command.py @@ -54,9 +54,7 @@ def define_args(export_parser): help="Set the page size of the exported PDF", ) - group.add_argument( - "--width", default=800, help="Set the width of the image in pixels. Default is 800 px" - ) + 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( diff --git a/tabcmd/commands/site/edit_site_command.py b/tabcmd/commands/site/edit_site_command.py index 6f710039..e47f246f 100644 --- a/tabcmd/commands/site/edit_site_command.py +++ b/tabcmd/commands/site/edit_site_command.py @@ -21,9 +21,7 @@ class EditSiteCommand(Server): def define_args(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") - ) + 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) diff --git a/tabcmd/execution/global_options.py b/tabcmd/execution/global_options.py index ddeff33a..0e66cd3c 100644 --- a/tabcmd/execution/global_options.py +++ b/tabcmd/execution/global_options.py @@ -66,18 +66,18 @@ def set_no_wait_option(parser): site_roles = [ - "ServerAdministrator", - "SiteAdministratorCreator", - "SiteAdministratorExplorer", - "SiteAdministrator", - "Creator", - "ExplorerCanPublish", - "Publisher", - "Explorer", - "Interactor", - "Viewer", - "Unlicensed", - ] + "ServerAdministrator", + "SiteAdministratorCreator", + "SiteAdministratorExplorer", + "SiteAdministrator", + "Creator", + "ExplorerCanPublish", + "Publisher", + "Explorer", + "Interactor", + "Viewer", + "Unlicensed", +] def set_role_arg(parser): @@ -87,7 +87,7 @@ def set_role_arg(parser): 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" + metavar="SITE_ROLE", ) return parser diff --git a/tabcmd/execution/parent_parser.py b/tabcmd/execution/parent_parser.py index 48f6a4d3..3aee7d7b 100644 --- a/tabcmd/execution/parent_parser.py +++ b/tabcmd/execution/parent_parser.py @@ -31,6 +31,8 @@ '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] @@ -50,15 +52,8 @@ def parent_parser_with_global_options(): ) 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") - ) + 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( @@ -73,14 +68,11 @@ def parent_parser_with_global_options(): 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] - ) + 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", @@ -135,7 +127,7 @@ def parent_parser_with_global_options(): "--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], + help=strings[10], ) parser.add_argument( @@ -165,11 +157,8 @@ class ParentParser: def __init__(self): self.global_options = parent_parser_with_global_options() self.root = argparse.ArgumentParser( - prog="tabcmd", - description=strings[15], - parents=[self.global_options], - epilog=strings[2] - ) + 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( @@ -195,9 +184,7 @@ def include(self, command): return additional_parser def include_help(self): - additional_parser = self.subparsers.add_parser( - "help", help=strings[14], parents=[self.global_options] - ) + 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)) @@ -213,7 +200,7 @@ 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 @@ -228,11 +215,11 @@ def run_command(self, args): "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 + 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 + "Show message listing commands and global options, then exit", # 14 "tabcmd -- Run a specific command", # 15 ] From b8a474873f2037de3beb161360eea1a7b8ba8ce8 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Wed, 30 Nov 2022 17:23:21 -0800 Subject: [PATCH 12/12] remove duplicate parser setup in tests --- tabcmd/execution/parent_parser.py | 3 +++ tabcmd/execution/tabcmd_controller.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tabcmd/execution/parent_parser.py b/tabcmd/execution/parent_parser.py index 3aee7d7b..a2495792 100644 --- a/tabcmd/execution/parent_parser.py +++ b/tabcmd/execution/parent_parser.py @@ -168,6 +168,9 @@ def __init__(self): ) def get_root_parser(self): + return self.root + + def connect_commands(self): commands = CommandsMap.commands_hash_map for command in commands: self.include(command) diff --git a/tabcmd/execution/tabcmd_controller.py b/tabcmd/execution/tabcmd_controller.py index 8b958efa..001b2b5b 100644 --- a/tabcmd/execution/tabcmd_controller.py +++ b/tabcmd/execution/tabcmd_controller.py @@ -10,7 +10,7 @@ class TabcmdController: @staticmethod def initialize(): manager = ParentParser() - parent = manager.get_root_parser() + parent = manager.connect_commands() manager.include_help() return parent