From b518881b084d473051467af0327f9b41a3e498d0 Mon Sep 17 00:00:00 2001 From: Jac Date: Fri, 24 Jun 2022 13:45:04 -0700 Subject: [PATCH 01/37] configure black (#133) * configure black * add step to list dependencies for licensing --- .github/workflows/check-coverage.yml | 10 +-- .github/workflows/generate-metadata.yml | 3 + .gitignore | 4 +- bin/black.sh | 3 - bin/e2e.sh | 3 - bin/license-checker.py | 54 +++++++++++++++ contributing.md | 4 +- dodo.py | 70 +++++++++++--------- pyproject.toml | 3 + setup.py | 88 +++++++++++++------------ src/execution/_version.py | 2 +- src/execution/parent_parser.py | 2 + tabcmd.py | 2 +- 13 files changed, 157 insertions(+), 91 deletions(-) delete mode 100644 bin/black.sh delete mode 100644 bin/e2e.sh create mode 100644 bin/license-checker.py diff --git a/.github/workflows/check-coverage.yml b/.github/workflows/check-coverage.yml index 123c3e91..cb83fa36 100644 --- a/.github/workflows/check-coverage.yml +++ b/.github/workflows/check-coverage.yml @@ -26,12 +26,11 @@ jobs: - name: Install dependencies run: | + python --version python -m pip install --upgrade pip - pip install -e .[install] pip install -e .[build] pip install -e .[test] - python setup.py build - python res/versioning.py + doit version python setup.py build # https://github.com/marketplace/actions/pytest-coverage-comment @@ -42,8 +41,9 @@ jobs: uses: MishaKav/pytest-coverage-comment@main with: pytest-coverage-path: ./pytest-coverage.txt - # broken? Error: The head commit for this pull_request event is not ahead of the base commit. - # Please submit an issue on this action's GitHub repo + # TODO: check if this has been fixed + # Error: The head commit for this pull_request event is not ahead of the base commit. + # Please submit an issue on this action's GitHub repo # report-only-changed-files: true # TODO update badge on readme: diff --git a/.github/workflows/generate-metadata.yml b/.github/workflows/generate-metadata.yml index 582c9e0d..12513ace 100644 --- a/.github/workflows/generate-metadata.yml +++ b/.github/workflows/generate-metadata.yml @@ -25,6 +25,9 @@ jobs: doit version python setup.py build + - name: Generate dependencies list + run: python bin/license-checker.py + - name: Type-check run: mypy src tests diff --git a/.gitignore b/.gitignore index aa66e7fb..97a5509d 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,9 @@ __pycache__/ *.pytest_cache *.pyc *.pkl -.coverage +venv/ +tabcmd-dev/ +workon/ # code coverage outputs .coverage diff --git a/bin/black.sh b/bin/black.sh deleted file mode 100644 index 0d34ed7b..00000000 --- a/bin/black.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -set -x -black --line-length 120 tabcmd tests diff --git a/bin/e2e.sh b/bin/e2e.sh deleted file mode 100644 index 8b0142ea..00000000 --- a/bin/e2e.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -# not tested as a script -pytest -q tests\e2e\online_tests.py -r pfE diff --git a/bin/license-checker.py b/bin/license-checker.py new file mode 100644 index 00000000..b9bc00df --- /dev/null +++ b/bin/license-checker.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# https://git.soma.salesforce.com/python-at-sfdc/license_checker +# Modified version of https://stackoverflow.com/a/44090218 +from __future__ import print_function +from collections import defaultdict +import pkg_resources + +def get_pkg_license(pkg): + try: + lines = pkg.get_metadata_lines('METADATA') + except: + lines = pkg.get_metadata_lines('PKG-INFO') + + for line in lines: + if line.startswith('License:'): + return line[9:] + return '(Licence not found)' + exit() + +def print_table(table): + column_index_to_max_width = defaultdict(int) + for row_index, row in enumerate(table): + for cell_index, cell in enumerate(row): + cur_max = column_index_to_max_width[cell_index] + cell_width = len(cell) + if cell_width > cur_max: + column_index_to_max_width[cell_index] = cell_width + for row_index, row in enumerate(table): + line = '' + for cell_index, cell in enumerate(row): + cell_width = column_index_to_max_width[cell_index] + line += cell.ljust(cell_width) + line += " - " + line = line.ljust(25) + line = line.rstrip(" -") + print(line) + if row_index == 0: + print("-" * len(line)) + + +def get_directory(package_name): + return pkg_resources.working_set.find(pkg_resources.Requirement.parse(package_name)).location + + +def print_packages_and_licenses(): + table = [] + table.append(['Package', 'License', 'Location']) + for pkg in sorted(pkg_resources.working_set): + table.append([str(pkg).rjust(25)[:25], get_pkg_license(pkg)[:25], pkg.location]) + print_table(table) + + +if __name__ == "__main__": + print_packages_and_licenses() diff --git a/contributing.md b/contributing.md index ae8dd0ce..c7c77036 100644 --- a/contributing.md +++ b/contributing.md @@ -30,9 +30,9 @@ _(note that running mypy and black is required for code being submitted to the r > pytest - run tests against a live server > python -m tabcmd login {your server info here} -> bin/e2e.sh +> pytest -q tests\e2e\online_tests.py -r pfE - autoformat your code with black (https://pypi.org/project/black/) -> bin/black.sh +> black . - check types > mypy src tests - do test coverage calculation (https://coverage.readthedocs.io/en/6.3.2) diff --git a/dodo.py b/dodo.py index ef2b8b42..3e4272a5 100644 --- a/dodo.py +++ b/dodo.py @@ -4,10 +4,7 @@ import ftfy import setuptools_scm -LOCALES = [ - "en", "de", "es", "fr", "ga", "it", "pt", - "sv", "ja", "ko", - "zh"] +LOCALES = ["en", "de", "es", "fr", "ga", "it", "pt", "sv", "ja", "ko", "zh"] """ https://pydoit.org/ @@ -46,8 +43,8 @@ def process_locales(): outfile.write(ftfy.fixes.decode_escapes(data)) return { - 'actions': [process_locales], - 'verbosity': 2, + "actions": [process_locales], + "verbosity": 2, } @@ -65,40 +62,48 @@ def task_po(): - 3.x, from pip install translate-toolkit: it copies key->comment, value-> msgid, ""->msgstr which is not at all what we want """ + def process_locales(): for current_locale in LOCALES: LOC_PATH = "src/locales/" + current_locale - for file in glob.glob(LOC_PATH+"/*.properties"): + for file in glob.glob(LOC_PATH + "/*.properties"): basename = os.path.basename(file).split(".")[0] print("processing", basename) - result = subprocess.run(["python", - "bin/i18n/prop2po.py", - "--encoding", "utf-8", # for the .po header - "--language", current_locale, # for the .po header - LOC_PATH + "/"+basename+".properties", - LOC_PATH + "/LC_MESSAGES/"+basename+".po"]) + result = subprocess.run( + [ + "python", + "bin/i18n/prop2po.py", + "--encoding", + "utf-8", # for the .po header + "--language", + current_locale, # for the .po header + LOC_PATH + "/" + basename + ".properties", + LOC_PATH + "/LC_MESSAGES/" + basename + ".po", + ] + ) print("\n", result) # print("stdout:", result.stdout) if not result.returncode == 0: print("stderr:", result.stderr) + return { - 'actions': [process_locales], - 'verbosity': 2, + "actions": [process_locales], + "verbosity": 2, } def task_clean_all(): - """For all languages: removes all generated artifacts (.po, .mo) which source from properties files. """ + """For all languages: removes all generated artifacts (.po, .mo) which source from properties files.""" def process_locales(): for current_locale in LOCALES: LOC_PATH = "src/locales/" + current_locale - for file in glob.glob(LOC_PATH+"/*.properties"): + for file in glob.glob(LOC_PATH + "/*.properties"): basename = os.path.basename(file).split(".")[0] - print("deleting",basename + ".*") + print("deleting", basename + ".*") try: - os.remove(LOC_PATH + "/LC_MESSAGES/"+basename+".po") + os.remove(LOC_PATH + "/LC_MESSAGES/" + basename + ".po") except OSError: pass try: @@ -111,10 +116,9 @@ def process_locales(): except OSError: pass - return { - 'actions': [process_locales], - 'verbosity': 2, + "actions": [process_locales], + "verbosity": 2, } @@ -128,7 +132,7 @@ def process_locales(): LOC_PATH = "src/locales/" + current_locale + "/LC_MESSAGES" - with open(LOC_PATH + "/tabcmd.po", 'w+', encoding="utf-8") as outfile: + with open(LOC_PATH + "/tabcmd.po", "w+", encoding="utf-8") as outfile: for file in glob.glob(LOC_PATH + "/*.po"): if file.endswith("tabcmd.po"): pass @@ -139,8 +143,8 @@ def process_locales(): outfile.write("\n") return { - 'actions': [process_locales], - 'verbosity': 2, + "actions": [process_locales], + "verbosity": 2, } @@ -164,14 +168,15 @@ def process_locales(): print("stderr:", result.stderr) return { - 'actions': [process_locales], - 'verbosity': 2, + "actions": [process_locales], + "verbosity": 2, } def task_version(): - """ Generates a metadata info file with current version to be bundled by pyinstaller""" + """Generates a metadata info file with current version to be bundled by pyinstaller""" + def write_for_pyinstaller(): import pyinstaller_versionfile import os @@ -183,12 +188,13 @@ def write_for_pyinstaller(): output_file = os.path.join(".", "program_metadata.txt") input_file = os.path.join("res", "metadata.yml") pyinstaller_versionfile.create_versionfile_from_input_file( - output_file, input_file, + output_file, + input_file, # optional, can be set to overwrite version information (equivalent to --version when using the CLI) - version=numeric_version + version=numeric_version, ) return { - 'actions': [write_for_pyinstaller], - 'verbosity': 2, + "actions": [write_for_pyinstaller], + "verbosity": 2, } diff --git a/pyproject.toml b/pyproject.toml index 4b7830a6..485decd9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,3 +2,6 @@ requires = ["build", "setuptools>=62", "wheel", "setuptools_scm>=6.2"] [tool.setuptools_scm] "local_scheme"= "no-local-version" # require pypi supported versions always +[tool.black] +line-length = 120 +extend-exclude = '^/bin/*' diff --git a/setup.py b/setup.py index 9955108f..57466d7d 100644 --- a/setup.py +++ b/setup.py @@ -1,61 +1,63 @@ from setuptools import setup, find_packages setup( - name='tabcmd', - author='Tableau', - author_email='github@tableau.com', - description='A command line client for working with Tableau Server.', - long_description='A command line client for working with Tableau Server.', - license='MIT', - url='https://github.com/tableau/tabcmd', - - python_requires='>=3.7', + name="tabcmd", + author="Tableau", + author_email="github@tableau.com", + description="A command line client for working with Tableau Server.", + long_description="A command line client for working with Tableau Server.", + license="MIT", + url="https://github.com/tableau/tabcmd", + python_requires=">=3.7", packages=find_packages(), - package_data={'tabcmd': ['src.locales/**/*.mo']}, + package_data={"tabcmd": ["src.locales/**/*.mo"]}, include_package_data=True, - entry_points={ - 'console_scripts': [ - 'tabcmd = src.tabcmd:main' - ] - }, + entry_points={"console_scripts": ["tabcmd = src.tabcmd:main"]}, setup_requires=[ # copy of pyproject.toml for back compat - "setuptools>=62", "wheel", "setuptools_scm>=6.2" + "build", + "setuptools>=62", + "setuptools_scm>=6.2", + "wheel", ], install_requires=[ - 'polling2', - 'requests>=2.11,<3.0', - 'tableauserverclient>=0.19', - 'urllib3>=1.24.3,<2.0', + "polling2", + "requests>=2.11,<3.0", + "tableauserverclient>=0.19", + "urllib3>=1.24.3,<2.0", ], extras_require={ - 'localize': [ - 'doit', - 'ftfy', + "localize": [ + "doit", + "ftfy", ], - 'build': [ - 'appdirs', - 'black', - 'doit', - 'ftfy', - 'mypy', - 'pyinstaller_versionfile', - 'setuptools>=62', - 'setuptools_scm', - 'types-appdirs', - 'types-mock', - 'types-requests', + "build": [ + "appdirs", + "black", + "doit", + "ftfy", + "mypy", + "pyinstaller_versionfile", + "setuptools>=62", + "setuptools_scm", + "types-appdirs", + "types-mock", + "types-requests", + "types-setuptools", ], - 'package': [ - 'pyinstaller>=5.1', - 'pyinstaller-versionfile', + "package": [ + "pyinstaller>=5.1", + "pyinstaller-versionfile", ], - 'test': [ - 'mock', - 'pytest', 'pytest-cov', 'pytest-order', 'pytest-runner', - 'requests-mock>=1.0,<2.0', + "test": [ + "mock", + "pytest", + "pytest-cov", + "pytest-order", + "pytest-runner", + "requests-mock>=1.0,<2.0", ], }, - test_suite='tests', + test_suite="tests", zip_safe=False, ) diff --git a/src/execution/_version.py b/src/execution/_version.py index fcf69b5e..1d50c3ef 100644 --- a/src/execution/_version.py +++ b/src/execution/_version.py @@ -1,5 +1,5 @@ # coding: utf-8 # file generated by setuptools_scm # don't change, don't track in version control -version = '38' +version = "38" version_tuple = (38,) diff --git a/src/execution/parent_parser.py b/src/execution/parent_parser.py index b04b6168..d7018031 100644 --- a/src/execution/parent_parser.py +++ b/src/execution/parent_parser.py @@ -1,8 +1,10 @@ import argparse from .localize import _ + # when we drop python 3.8, this could be replaced with this lighter weight option # from importlib.metadata import version, PackageNotFoundError from pkg_resources import get_distribution, DistributionNotFound + try: version = get_distribution("tabcmd").version except DistributionNotFound: diff --git a/tabcmd.py b/tabcmd.py index 981ca265..1c987d62 100644 --- a/tabcmd.py +++ b/tabcmd.py @@ -1,4 +1,4 @@ from src import tabcmd -if __name__ == '__main__': +if __name__ == "__main__": tabcmd.main() From 648d7b26ddf6dc7192ed5de0f607998a76d46d7d Mon Sep 17 00:00:00 2001 From: Jac Date: Fri, 8 Jul 2022 13:04:58 -0700 Subject: [PATCH 02/37] Jac/args (#134) * Defect 1427376: [Tabcmd WAM] --save-db-password flag thinks it should get an argument -updated several other publishing arguments that had the same problem - change argument from "token" to "token-value" so it is not a substring of "token-name", remove the 2-letter short arguments * fix reencrypt_extracts help string * Hand format a nice help output --- src/commands/auth/session.py | 16 ++--- .../extracts/reencrypt_extracts_command.py | 2 +- src/commands/help/help_command.py | 67 ++++++++++++++++--- src/commands/server.py | 4 +- src/execution/global_options.py | 25 ++++--- src/execution/logger_config.py | 2 +- src/execution/parent_parser.py | 25 +++---- src/execution/tabcmd_controller.py | 3 +- tests/commands/test_run_commands.py | 1 + tests/commands/test_session.py | 16 ++--- tests/parsers/test_login_parser.py | 4 +- tests/parsers/test_parser_publish.py | 13 ++++ tests/parsers/test_parser_publish_samples.py | 2 +- tests/parsers/test_parser_refresh_extracts.py | 3 +- 14 files changed, 128 insertions(+), 55 deletions(-) diff --git a/src/commands/auth/session.py b/src/commands/auth/session.py index 812d294c..c3fc2687 100644 --- a/src/commands/auth/session.py +++ b/src/commands/auth/session.py @@ -28,7 +28,7 @@ def __init__(self): self.user_id = None self.auth_token = None self.token_name = None - self.token = None + self.token_value = None self.password_file = None self.site_name = None # The site name, e.g 'alpodev' self.site_id = None # The site id, e.g 'abcd-1234-1234-1244-1234' @@ -63,7 +63,7 @@ def _update_session_data(self, args): self.logging_level = args.logging_level or self.logging_level self.password_file = args.password_file self.token_name = args.token_name or self.token_name - self.token = args.token or self.token + self.token_value = args.token_value or self.token_value self.no_prompt = args.no_prompt or self.no_prompt self.certificate = args.certificate or self.certificate @@ -107,8 +107,8 @@ def _create_new_credential(self, password, credential_type): Errors.exit_with_error(self.logger, "Couldn't find credentials") def _create_new_token_credential(self): - if self.token: - token = self.token + if self.token_value: + token = self.token_value elif self.password_file: token = Session._read_password_from_file(self.password_file) elif self._allow_prompt(): @@ -220,7 +220,7 @@ def create_session(self, args): credentials = self._create_new_credential(args.password, Session.PASSWORD_CRED_TYPE) else: credentials = self._create_new_credential(args.password, Session.TOKEN_CRED_TYPE) - elif args.token: + elif args.token_value: self._end_session() credentials = self._create_new_token_credential() else: # no login arguments given - look for saved info @@ -262,7 +262,7 @@ def _clear_data(self): self.user_id = None self.auth_token = None self.token_name = None - self.token = None + self.token_value = None self.site_name = None self.site_id = None self.server = None @@ -300,7 +300,7 @@ def _read_from_json(self): self.username = auth["username"] self.user_id = auth["user_id"] self.token_name = auth["personal_access_token_name"] - self.token = auth["personal_access_token"] + self.token_value = auth["personal_access_token"] self.last_login_using = auth["last_login_using"] self.password_file = auth["password_file"] self.no_prompt = auth["no_prompt"] @@ -341,7 +341,7 @@ def _serialize_for_save(self): "site_name": self.site_name, "site_id": self.site_id, "personal_access_token_name": self.token_name, - "personal_access_token": self.token, + "personal_access_token": self.token_value, "last_login_using": self.last_login_using, "password_file": self.password_file, "no_prompt": self.no_prompt, diff --git a/src/commands/extracts/reencrypt_extracts_command.py b/src/commands/extracts/reencrypt_extracts_command.py index 98ea8c34..3071757b 100644 --- a/src/commands/extracts/reencrypt_extracts_command.py +++ b/src/commands/extracts/reencrypt_extracts_command.py @@ -14,7 +14,7 @@ class ReencryptExtracts(Server): """ name: str = "reencryptextracts" - description: str = _("reencryptextracts.short_description=") + description: str = _("reencryptextracts.short_description") @staticmethod def define_args(reencrypt_extract_parser): diff --git a/src/commands/help/help_command.py b/src/commands/help/help_command.py index 30aacc3b..2436cd21 100644 --- a/src/commands/help/help_command.py +++ b/src/commands/help/help_command.py @@ -1,4 +1,8 @@ -import sys +import argparse +from typing import Any, List + +from src.execution.localize import _ +from src.execution.logger_config import log class HelpCommand: @@ -11,15 +15,60 @@ class HelpCommand: @staticmethod def define_args(parser): - # takes no args - pass + parser.add_argument("help_option", nargs="?") @staticmethod - def run_command(args): - description = ( + def run_command(args: argparse.Namespace): + + # whaddya mean, '__class__' is not defined ??!?!!? + logger = log(__class__.__name__, args.logging_level) # type: ignore[name-defined] + logger.debug(_("tabcmd.launching")) + + # delayed import, TODO fix cyclic imports + from src.execution.map_of_commands import CommandsMap + + all_commands: List[Any] = CommandsMap.commands_hash_map + + description: str = ( "tabcmd - Tableau Server Command Line Utility 2.0 \n \n" - "tabcmd help -- Help for tabcmd commands \n" - "tabcmd help -- Show Help for a specific command\n" - "tabcmd help commands -- List all available commands\n\n" + "tabcmd help -- List all available commands and global options \n" + "tabcmd help -- Show Help for a specific command\n\n" ) - sys.stdout.write(description) + + if args.help_option: + + if args.help_option in map(lambda command: command.name, all_commands): + command_objects = filter(lambda command: command.name == args.help_option, all_commands) + cli_cmd = list(command_objects)[0] + logger.info(cli_cmd.name.ljust(25) + cli_cmd.description + "\n") + command_parser = argparse.ArgumentParser(parents=[]) + cli_cmd.define_args(command_parser) + + positionals = [] + optionals = [] + for option in command_parser._actions: + if option.option_strings: + optionals.append(option) + else: + positionals.append(option) + + if positionals: + logger.info("Required arguments") + for option in positionals: + logger.info("{0} {1}{2}{3}".format(option.dest.ljust(25), "{", option.help, "}")) + if optionals: + logger.info("\nOptional arguments") + for option in optionals: + logger.info("{0} {1} ".format(option.option_strings, option.help)) + + logger.info("\nUsage") + usage = cli_cmd.name + " " + for opt in positionals: + usage = usage + opt.dest + if len(positionals) < len(command_parser._actions): + usage = usage + " [--optional arguments]" + logger.info(usage) + + else: + for cmd in all_commands: + logger.info(cmd.name + ": " + cmd.description) diff --git a/src/commands/server.py b/src/commands/server.py index 9ba01b62..3f171234 100644 --- a/src/commands/server.py +++ b/src/commands/server.py @@ -1,8 +1,8 @@ import os - import tableauserverclient as TSC from src.commands.constants import Errors +from src.execution.localize import _ class Server: @@ -74,7 +74,7 @@ def get_site_for_command_or_throw(logger, server, args): Errors.exit_with_error(logger, exception=e) else: logger.debug("Use logged in site") - # site_item = server.sites.get_by_id(server.site_id) + site_item = server.sites.get_by_id(server.site_id) if not site_item: raise ResourceWarning("Could not get site from server") return site_item diff --git a/src/execution/global_options.py b/src/execution/global_options.py index 9d0b834b..8c259028 100644 --- a/src/execution/global_options.py +++ b/src/execution/global_options.py @@ -65,6 +65,7 @@ def set_no_wait_option(parser): return parser +# TODO make this lower case? def set_role_arg(parser): parser.add_argument( "-r", @@ -88,7 +89,9 @@ def set_role_arg(parser): def set_silent_option(parser): - parser.add_argument("--silent-progress", help="Do not display progress messages for the command.") + parser.add_argument( + "--silent-progress", action="store_true", help="Do not display progress messages for the command." + ) return parser @@ -301,6 +304,7 @@ def set_publish_args(parser): ) parser.add_argument( "--save-db-password", + action="store_true", help="Stores the provided database password on the server.", ) parser.add_argument( @@ -309,25 +313,29 @@ def set_publish_args(parser): help="When a workbook with tabbed views is published, each sheet becomes a tab that viewers can use to \ navigate through the workbook", ) - parser.add_argument("--replace", help="Use the extract file to replace the existing data source.") - parser.add_argument("--disable-uploader", help="Disable the incremental file uploader.") + parser.add_argument( + "--replace", action="store_true", help="Use the extract file to replace the existing data source." + ) + parser.add_argument("--disable-uploader", action="store_true", help="Disable the incremental file uploader.") parser.add_argument("--restart", help="Restart the file upload.") parser.add_argument( "--encrypt-extracts", + action="store_true", help="Encrypt extracts in the workbook, datasource, or extract being published to the server", ) parser.add_argument("--oauth-username", help="The email address of a preconfigured OAuth connection") - parser.add_argument("--save-oauth") - parser.add_argument("--thumbnail-username") - parser.add_argument("--thumbnail-group") # not implemented in the REST API + parser.add_argument("--save-oauth", action="store_true", help="Save embedded OAuth credentials in the datasource") + parser.add_argument("--thumbnail-username", help="Not yet implemented") + parser.add_argument("--thumbnail-group", help="Not yet implemented") # not implemented in the REST API # refresh-extracts def set_incremental_options(parser): sync_group = parser.add_mutually_exclusive_group() - sync_group.add_argument("--incremental", help="Runs the incremental refresh operation.") + sync_group.add_argument("--incremental", action="store_true", help="Runs the incremental refresh operation.") sync_group.add_argument( "--synchronous", + action="store_true", help="Adds the full refresh operation to the queue used by the Backgrounder process, to be run as soon as a \ Backgrounder process is available.", ) @@ -368,7 +376,7 @@ def set_domain_arguments(parser): def set_target_users_arg(parser): target_users_group = parser.add_mutually_exclusive_group() target_users_group.add_argument("--target-username", help="Clears sub value for the specified individual user.") - target_users_group.add_argument("--all", help="Clears sub values for all users.") + target_users_group.add_argument("--all", action="store_true", help="Clears sub values for all users.") return parser @@ -383,6 +391,7 @@ def set_update_group_args(parser): parser.add_argument( "--grant-license-mode", choices=["on-login", "on-sync"], + type=str.lower, help="Specifies whether a role should be granted on sign in. ", ) parser.add_argument( diff --git a/src/execution/logger_config.py b/src/execution/logger_config.py index eee39ba1..1837e951 100644 --- a/src/execution/logger_config.py +++ b/src/execution/logger_config.py @@ -7,7 +7,7 @@ logging.ERROR: "ERROR: %(name)-10s: %(lineno)d: %(message)s", logging.WARN: "WARN: %(message)s", logging.DEBUG: "DEBUG: %(name)-10s: %(lineno)d: %(message)-10s", - logging.INFO: "INFO: %(message)s", + logging.INFO: "%(message)s", "TRACE": "TRACE: %(asctime)-12s %(name)-10s: %(lineno)d: %(message)-10s", "DEFAULT": "%(message)s", } diff --git a/src/execution/parent_parser.py b/src/execution/parent_parser.py index d7018031..82261f28 100644 --- a/src/execution/parent_parser.py +++ b/src/execution/parent_parser.py @@ -66,8 +66,8 @@ def parent_parser_with_global_options(self): "-l", "--logging-level", choices=["DEBUG", "INFO", "ERROR"], + type=str.upper, # coerce input to uppercase to act case insensitive default="info", - metavar="", help="Use the specified logging level. The default level is INFO.", ) @@ -75,7 +75,6 @@ def parent_parser_with_global_options(self): auth_options = parser.add_mutually_exclusive_group() auth_options.add_argument( - "-tn", "--token-name", default=None, metavar="", @@ -88,8 +87,7 @@ def parent_parser_with_global_options(self): secret_values = parser.add_mutually_exclusive_group() secret_values.add_argument( - "-to", - "--token", + "--token-value", default=None, metavar="", help="Use the specified Tableau Server Personal Access Token. Requires --token-name to be set.", @@ -129,14 +127,6 @@ def parent_parser_with_global_options(self): help=_("session.options.timeout"), ) - parser.add_argument( - "-v", - "--version", - action="version", - version="tabcmd.exe - Tableau Server Command Line Utility v" + version + "\n \n", - help="Show version information and exit.", - ) - # TODO get the list of choices dynamically? parser.add_argument( "--language", @@ -150,4 +140,15 @@ def parent_parser_with_global_options(self): 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 diff --git a/src/execution/tabcmd_controller.py b/src/execution/tabcmd_controller.py index 7b8b2346..f54f6b02 100644 --- a/src/execution/tabcmd_controller.py +++ b/src/execution/tabcmd_controller.py @@ -1,4 +1,5 @@ import logging +import sys from .localize import set_client_locale from .map_of_commands import * @@ -26,7 +27,7 @@ def run(parser, user_input=None): namespace = parser.parse_args(user_input) logger = log(__name__, namespace.logging_level or logging.INFO) - logger.debug(namespace) + # logger.debug(namespace) if namespace.language: set_client_locale(namespace.language, logger) diff --git a/tests/commands/test_run_commands.py b/tests/commands/test_run_commands.py index 10915a3e..67c032c3 100644 --- a/tests/commands/test_run_commands.py +++ b/tests/commands/test_run_commands.py @@ -243,6 +243,7 @@ def test_delete_group(self, mock_session, mock_server): # 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() diff --git a/tests/commands/test_session.py b/tests/commands/test_session.py index 76580e03..64ea26a9 100644 --- a/tests/commands/test_session.py +++ b/tests/commands/test_session.py @@ -13,7 +13,7 @@ password_file=None, server=None, token_name=None, - token=None, + token_value=None, logging_level=None, no_certcheck=None, no_prompt=False, @@ -117,9 +117,9 @@ def test__create_new_username_credential_fails_no_args(self, mock_pass): # These two already have a username saved and pass in a password as argument def test__create_new_token_credential_succeeds_new_token(self, mock_pass): test_args = Namespace(**vars(args_to_mock)) - test_args.token = "gibberish" + test_args.token_value = "gibberish" active_session = Session() - assert active_session.token is None, active_session.token + assert active_session.token_value is None, active_session.token_value active_session.token_name = "readable" auth = active_session._create_new_token_credential() assert auth is not None @@ -136,7 +136,7 @@ def test__create_new_username_credential_succeeds_new_password(self, mock_pass): # this one has a token saved def test__create_new_token_credential_succeeds_from_self(self, mock_pass): active_session = Session() - active_session.token = "gibberish2" + active_session.token_value = "gibberish2" active_session.token_name = "readable2" auth = active_session._create_new_token_credential() assert mock_pass.is_not_called() @@ -157,7 +157,7 @@ def test__create_new_username_credential_succeeds_from_self(self, mock_pass): def test__create_new_token_credential_succeeds_from_args(self, mock_pass): test_args = Namespace(**vars(args_to_mock)) - test_args.token = "gibberish" + test_args.token_value = "gibberish" test_args.token_name = "readable" active_session = Session() active_session._update_session_data(test_args) @@ -220,13 +220,13 @@ def test_create_session_first_time_with_token_arg( assert mock_path.exists("anything") is False test_args = Namespace(**vars(args_to_mock)) test_args.token_name = "tn" - test_args.token = "foo" + test_args.token_value = "foo" new_session = Session() auth = new_session.create_session(test_args) assert auth is not None, auth assert auth.auth_token is not None, auth.auth_token assert auth.auth_token.name is not None, auth.auth_token - assert new_session.token == "foo", new_session.token + assert new_session.token_value == "foo", new_session.token_value assert new_session.token_name == "tn", new_session @mock.patch("tableauserverclient.Server") @@ -302,7 +302,7 @@ def test_create_session_with_active_session_saved( _set_mocks_for_json_file_exists(mock_path, True) _set_mocks_for_json_file_saved_username(mock_json_load, "auth_token", None) test_args = Namespace(**vars(args_to_mock)) - test_args.token = "tn" + test_args.token_value = "tn" test_args.token_name = "tnnnn" test_args.no_prompt = False new_session = Session() diff --git a/tests/parsers/test_login_parser.py b/tests/parsers/test_login_parser.py index 0c0f0413..7369ed37 100644 --- a/tests/parsers/test_login_parser.py +++ b/tests/parsers/test_login_parser.py @@ -93,10 +93,10 @@ def test_login_parser_test_username_password(self): assert args.password == "pw", args def test_login_parser_test_token(self): - mock_args = [commandname, "--token", "token", "--token-name", "tn"] + mock_args = [commandname, "--token-value", "token", "--token-name", "tn"] args = self.parser_under_test.parse_args(mock_args) assert args.token_name == "tn", args - assert args.token == "token", args + assert args.token_value == "token", args def test_login_token_and_username(self): mock_args = [commandname, "--token-name", "to", "--username", "u"] diff --git a/tests/parsers/test_parser_publish.py b/tests/parsers/test_parser_publish.py index f93dc748..356bfe60 100644 --- a/tests/parsers/test_parser_publish.py +++ b/tests/parsers/test_parser_publish.py @@ -25,3 +25,16 @@ def test_publish_parser_tabbed(self): mock_args = [commandname, "filename.twbx", "--tabbed"] args = self.parser_under_test.parse_args(mock_args) assert args.tabbed is True, args + + def test_publish_parser_save_password(self): + mock_args = [ + commandname, + "filename.twbx", + "--db-username", + "user", + "--db-password", + "somepassword", + "--save-db-password", + ] + args = self.parser_under_test.parse_args(mock_args) + assert args.save_db_password is True, args diff --git a/tests/parsers/test_parser_publish_samples.py b/tests/parsers/test_parser_publish_samples.py index b8500e8f..52e04c6d 100644 --- a/tests/parsers/test_parser_publish_samples.py +++ b/tests/parsers/test_parser_publish_samples.py @@ -6,7 +6,7 @@ commandname = "publishsamples" -class PublishParserParserTest(unittest.TestCase): +class PublishSamplesParserTest(unittest.TestCase): @classmethod def setUpClass(cls): cls.parser_under_test = initialize_test_pieces(commandname, PublishSamplesCommand) diff --git a/tests/parsers/test_parser_refresh_extracts.py b/tests/parsers/test_parser_refresh_extracts.py index ed2c035d..942182d3 100644 --- a/tests/parsers/test_parser_refresh_extracts.py +++ b/tests/parsers/test_parser_refresh_extracts.py @@ -22,13 +22,12 @@ def test_refresh_extract_parser_optional_arguments(self): "--datasource", "hello", "--incremental", - "True", "--removecalculations", "--project", "testproject", ] args = self.parser_under_test.parse_args(mock_args) - assert args.incremental == "True", args + assert args.incremental, args def test_refresh_extract_parser_missing_all_args(self): mock_args = [commandname] From 9f21a456a72617b4ec53ce18bc28aa4b821c7d5a Mon Sep 17 00:00:00 2001 From: Brian Cantoni Date: Fri, 8 Jul 2022 13:06:11 -0700 Subject: [PATCH 03/37] Fix exit_with_error handling (#137) * Fix exit_with_error handling --- src/commands/datasources_and_workbooks/get_url_command.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/commands/datasources_and_workbooks/get_url_command.py b/src/commands/datasources_and_workbooks/get_url_command.py index 924c1a2d..52989ad5 100644 --- a/src/commands/datasources_and_workbooks/get_url_command.py +++ b/src/commands/datasources_and_workbooks/get_url_command.py @@ -144,7 +144,7 @@ def generate_pdf(logger, server, args): f.write(view_item.pdf) logger.info(_("export.success").format(view_item.name, filename)) except TSC.ServerResponseError as e: - GetUrl.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) + Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) @staticmethod def generate_png(logger, server, args): @@ -159,7 +159,7 @@ def generate_png(logger, server, args): f.write(view_item.png) logger.info(_("export.success").format(view_item.name, filename)) except TSC.ServerResponseError as e: - GetUrl.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) + Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) @staticmethod def generate_csv(logger, server, args): @@ -174,7 +174,7 @@ def generate_csv(logger, server, args): f.write(view_item.csv) logger.info(_("export.success"), views_from_list.name, formatted_file_name) except TSC.ServerResponseError as e: - GetUrl.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) + Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) except Exception as e: Errors.exit_with_error(logger, exception=e) @@ -188,4 +188,4 @@ def generate_twb(logger, server, args, file_extension): 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_path)) except TSC.ServerResponseError as e: - GetUrl.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) + Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) From d493cddfc662593b050f998068c94833d48f110e Mon Sep 17 00:00:00 2001 From: Jac Date: Sun, 10 Jul 2022 13:16:10 -0700 Subject: [PATCH 04/37] TFS 1428581 publish project (#139) --- .gitignore | 6 ++++++ src/commands/datasources_and_workbooks/publish_command.py | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 97a5509d..d586d56e 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,9 @@ html # doit .doit.* +# venv +site-packages + +# local +tabcmd-dev +workon diff --git a/src/commands/datasources_and_workbooks/publish_command.py b/src/commands/datasources_and_workbooks/publish_command.py index 5ad5b87c..b360b775 100644 --- a/src/commands/datasources_and_workbooks/publish_command.py +++ b/src/commands/datasources_and_workbooks/publish_command.py @@ -38,9 +38,10 @@ def run_command(args): if args.project_name: try: - project_id = Server.get_project_by_name_and_parent_path( + dest_project = Server.get_project_by_name_and_parent_path( logger, server, args.project_name, args.parent_project_path ) + project_id = dest_project.id except Exception as exc: Errors.exit_with_error(logger, _("publish.errors.server_resource_not_found"), exc) else: @@ -49,6 +50,7 @@ def run_command(args): args.parent_project_path = "" publish_mode = PublishCommand.get_publish_mode(args) + logger.info("Publishing as " + publish_mode) source = PublishCommand.get_filename_extension_if_tableau_type(logger, args.filename) logger.info(_("publish.status").format(args.filename)) From e099732ffa4fb0771a3682cfa49325c3a011148e Mon Sep 17 00:00:00 2001 From: Jac Date: Tue, 12 Jul 2022 13:57:41 -0700 Subject: [PATCH 05/37] Jac/groups (#140) * Defect 1426885: [Tabcmd WAM] addusers and removeusers error * fix deletegroup * update unlocalized strings --- src/commands/auth/session.py | 13 ++++++++++--- .../runschedule_command.py | 3 --- src/commands/group/create_group_command.py | 5 +++-- src/commands/group/delete_group_command.py | 5 +++-- src/commands/server.py | 13 ++++++++++--- src/commands/user/user_data.py | 9 +++++---- tests/commands/test_run_commands.py | 2 +- 7 files changed, 32 insertions(+), 18 deletions(-) diff --git a/src/commands/auth/session.py b/src/commands/auth/session.py index c3fc2687..3deecb49 100644 --- a/src/commands/auth/session.py +++ b/src/commands/auth/session.py @@ -4,6 +4,7 @@ import requests import tableauserverclient as TSC +import tableauserverclient.server.endpoint.exceptions from urllib3.exceptions import InsecureRequestWarning from src.commands.constants import Errors @@ -132,7 +133,10 @@ def _set_connection_options(self): if self.no_certcheck: http_options = {"verify": False} requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning) - tableau_server = TSC.Server(self.server_url, use_server_version=True, http_options=http_options) + try: + tableau_server = TSC.Server(self.server_url, use_server_version=True, http_options=http_options) + except Exception as e: + Errors.exit_with_error(self.logger, "Failed to connect to server", e) return tableau_server @@ -141,7 +145,10 @@ def _create_new_connection(self): self.tableau_server = self._set_connection_options() self._print_server_info() self.logger.info(_("session.connecting")) - self.tableau_server.use_server_version() # this will attempt to contact the server + try: + self.tableau_server.use_server_version() # this will attempt to contact the server + except Exception as e: + Errors.exit_with_error(self.logger, "Failed to connect to server", e) def _read_existing_state(self): if self._check_json(): @@ -175,7 +182,7 @@ def _sign_in(self, tableau_auth): self.logger.debug(_("listsites.output").format("", self.username or self.token_name, self.site_name)) try: self.tableau_server.auth.sign_in(tableau_auth) # it's the same call for token or user-pass - except TSC.ServerResponseError as e: + except Exception as e: Errors.exit_with_error(self.logger, exception=e) try: self.site_id = self.tableau_server.site_id diff --git a/src/commands/datasources_and_workbooks/runschedule_command.py b/src/commands/datasources_and_workbooks/runschedule_command.py index 4a1a8911..0b58e396 100644 --- a/src/commands/datasources_and_workbooks/runschedule_command.py +++ b/src/commands/datasources_and_workbooks/runschedule_command.py @@ -23,10 +23,7 @@ def run_command(args): logger.debug(_("tabcmd.launching")) session = Session() server = session.create_session(args) - logger.info(_("export.status").format(args.schedule)) schedule = DatasourcesAndWorkbooks.get_items_by_name(logger, server.schedules, args.schedule)[0] - if not schedule: - Errors.exit_with_error(logger, _("publish.errors.server_resource_not_found")) logger.info(_("runschedule.status")) Errors.exit_with_error(logger, "Not yet implemented") diff --git a/src/commands/group/create_group_command.py b/src/commands/group/create_group_command.py index 9344ea2a..9220b5b5 100644 --- a/src/commands/group/create_group_command.py +++ b/src/commands/group/create_group_command.py @@ -29,9 +29,10 @@ def run_command(args): logger.info(_("creategroup.status").format(args.name)) new_group = TSC.GroupItem(args.name) server.groups.create(new_group) - logger.info(_("tabcmd.result.succeeded")) + logger.info(_("common.output.succeeded")) except TSC.ServerResponseError as e: + # quite likely a 403 because you must be server/site admin to call this if args.continue_if_exists and Errors.is_resource_conflict(e): logger.info(_("tabcmd.result.already_exists.group").format(args.name)) return - Errors.exit_with_error(logger, "tabcmd.result.failed.create_group") + Errors.exit_with_error(logger, _("tabcmd.result.failed.create_group")) diff --git a/src/commands/group/delete_group_command.py b/src/commands/group/delete_group_command.py index 1e94b910..ef76584b 100644 --- a/src/commands/group/delete_group_command.py +++ b/src/commands/group/delete_group_command.py @@ -30,6 +30,7 @@ def run_command(args): group_id = Server.find_group_id(logger, server, args.name) logger.info(_("deletegroup.status").format(group_id)) server.groups.delete(group_id) - logger.info(_("tabcmd.result.succeeded")) + logger.info(_("common.output.succeeded")) except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, "tabcmd.result.failed.delete.group", e) + # quite likely a 403 because you must be server/site admin to call this + Errors.exit_with_error(logger, _("tabcmd.result.failed.delete.group"), e) diff --git a/src/commands/server.py b/src/commands/server.py index 3f171234..dee5dd0b 100644 --- a/src/commands/server.py +++ b/src/commands/server.py @@ -38,7 +38,7 @@ def find_group(logger, server, group_name): @staticmethod def find_group_id(logger, server, group_name): - return Server.find_group(logger, server, group_name)[0].id + return Server.find_group(logger, server, group_name).id @staticmethod def find_user_id(logger, server, username): @@ -49,7 +49,12 @@ def find_user_id(logger, server, username): @staticmethod def get_items_by_name(logger, item_endpoint, item_name, container=None): - logger.debug(_("export.status").format(item_name)) + item_type = type(item_endpoint).__name__ + item_log_name = item_name + if container: + item_log_name = container + "/" + item_log_name + item_log_name = "[" + item_type + "] " + item_log_name + logger.debug(_("export.status").format(item_log_name)) req_option = TSC.RequestOptions() req_option.filter.add(TSC.Filter(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Operator.Equals, item_name)) if container: @@ -59,9 +64,11 @@ def get_items_by_name(logger, item_endpoint, item_name, container=None): ) all_items, pagination_item = item_endpoint.get(req_option) if all_items is None or all_items == []: - raise ValueError(_("publish.errors.server_resource_not_found")) + raise ValueError("[" + item_type + "] " + _("errors.xmlapi.not_found")) if len(all_items) > 1: logger.debug("{}+ items of this name were found. Returning first page.".format(len(all_items))) + logger.debug(all_items[0].name + ", " + all_items[1].name + ", " + all_items[2].name) + return all_items # Get site by name or get currently logged in site diff --git a/src/commands/user/user_data.py b/src/commands/user/user_data.py index c6fcafd1..dd5d9c53 100644 --- a/src/commands/user/user_data.py +++ b/src/commands/user/user_data.py @@ -214,10 +214,6 @@ def evaluate_site_role(license_level, admin_level, publisher): def act_on_users( logger: logging.Logger, server: object, action_name: str, server_method: Callable, args: argparse.Namespace ) -> None: - n_users_handled: int = 0 - number_of_errors: int = 0 - n_users_listed: int = UserCommand.validate_file_for_import(args.users, logger, strict=args.require_all_valid) - logger.debug(_("importcsvsummary.line.processed").format(n_users_listed)) group = None try: @@ -227,6 +223,11 @@ def act_on_users( logger, _("errors.reportable.impersonation.group_not_found").format(args.name), exception=e ) + n_users_handled: int = 0 + number_of_errors: int = 0 + n_users_listed: int = UserCommand.validate_file_for_import(args.users, logger, strict=args.require_all_valid) + logger.debug(_("importcsvsummary.line.processed").format(n_users_listed)) + error_list = [] user_obj_list: List[TSC.UserItem] = UserCommand.get_users_from_file(args.users) logger.debug(_("tabcmd.result.success.parsed_users").format(len(user_obj_list))) diff --git a/tests/commands/test_run_commands.py b/tests/commands/test_run_commands.py index 67c032c3..7ffd9e2c 100644 --- a/tests/commands/test_run_commands.py +++ b/tests/commands/test_run_commands.py @@ -162,7 +162,7 @@ def test_decrypt(self, mock_session, mock_server): def test_delete_extract(self, mock_session, mock_server): RunCommandsTest._set_up_session(mock_session, mock_server) mock_server.datasources = getter - mock_args.datasource = True + mock_args.datasource = "datasource-name" delete_extracts_command.DeleteExtracts.run_command(mock_args) mock_session.assert_called() From e62bc12f272d0e300bfa7ef1d478a9c917233b8a Mon Sep 17 00:00:00 2001 From: Jac Date: Tue, 12 Jul 2022 22:36:34 -0700 Subject: [PATCH 06/37] Update session.py (#141) clearly indicate when using the default site --- src/commands/auth/session.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commands/auth/session.py b/src/commands/auth/session.py index 3deecb49..827dac98 100644 --- a/src/commands/auth/session.py +++ b/src/commands/auth/session.py @@ -160,7 +160,8 @@ def _print_server_info(self): self.logger.info("===== Username: {}".format(self.username)) else: self.logger.info("===== Token Name: {}".format(self.token_name)) - self.logger.info(_("dataconnections.classes.tableau_server_site") + ": {}".format(self.site_name)) + site_display_name = self.site_name or "Default Site" + self.logger.info(_("dataconnections.classes.tableau_server_site") + ": {}".format(site_display_name)) def _validate_existing_signin(self): self.logger.info(_("session.continuing_session")) From 2ad6352b7eb41777000664201a24a986cfd9bc94 Mon Sep 17 00:00:00 2001 From: Brian Cantoni Date: Wed, 13 Jul 2022 01:30:08 -0700 Subject: [PATCH 07/37] Fix export success log message params (#142) --- src/commands/datasources_and_workbooks/get_url_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/datasources_and_workbooks/get_url_command.py b/src/commands/datasources_and_workbooks/get_url_command.py index 52989ad5..557cfd1c 100644 --- a/src/commands/datasources_and_workbooks/get_url_command.py +++ b/src/commands/datasources_and_workbooks/get_url_command.py @@ -172,7 +172,7 @@ def generate_csv(logger, server, args): file_name_with_path = GetUrl.filename_from_args(args.filename, view_item.name, "csv") with open(file_name_with_path, "wb") as f: f.write(view_item.csv) - logger.info(_("export.success"), views_from_list.name, formatted_file_name) + logger.info(_("export.success").format(view_item.name, file_name_with_path)) except TSC.ServerResponseError as e: Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) except Exception as e: From 313e486eb94ffba7dc12ab2238fa0470cfce523b Mon Sep 17 00:00:00 2001 From: Jac Date: Fri, 15 Jul 2022 11:50:13 -0700 Subject: [PATCH 08/37] Jac/tfs 1428582 create edit site (#146) --- src/commands/auth/session.py | 2 +- .../extracts/decrypt_extracts_command.py | 2 +- .../extracts/encrypt_extracts_command.py | 2 +- .../extracts/reencrypt_extracts_command.py | 2 +- src/commands/server.py | 17 +++++++++++------ src/commands/site/create_site_command.py | 15 +++++++++------ src/commands/site/delete_site_command.py | 8 +++----- src/commands/site/edit_site_command.py | 2 +- tests/commands/test_run_commands.py | 10 +++++----- tests/parsers/test_parser_create_site.py | 4 ++-- tests/parsers/test_parser_delete_site.py | 2 +- 11 files changed, 36 insertions(+), 30 deletions(-) diff --git a/src/commands/auth/session.py b/src/commands/auth/session.py index 827dac98..e224d35d 100644 --- a/src/commands/auth/session.py +++ b/src/commands/auth/session.py @@ -191,10 +191,10 @@ def _sign_in(self, tableau_auth): self.auth_token = self.tableau_server._auth_token if not self.username: self.username = self.tableau_server.users.get_by_id(self.user_id).name - self.logger.debug("Signed into {0}{1} as {2}".format(self.server_url, self.site_name, self.username)) self.logger.info(_("common.output.succeeded")) except TSC.ServerResponseError as e: Errors.exit_with_error(self.logger, _("publish.errors.unexpected_server_response"), e) + self.logger.debug("Signed into {0}{1} as {2}".format(self.server_url, self.site_name, self.username)) return self.tableau_server diff --git a/src/commands/extracts/decrypt_extracts_command.py b/src/commands/extracts/decrypt_extracts_command.py index 61605bc5..2f02ec7a 100644 --- a/src/commands/extracts/decrypt_extracts_command.py +++ b/src/commands/extracts/decrypt_extracts_command.py @@ -24,7 +24,7 @@ def run_command(args): logger.debug(_("tabcmd.launching")) session = Session() server = session.create_session(args) - site_item = Server.get_site_for_command_or_throw(logger, server, args) + site_item = Server.get_site_for_command_or_throw(logger, server, args.site_name) try: logger.info(_("decryptextracts.status").format(args.site_name)) job = server.sites.decrypt_extracts(site_item.id) diff --git a/src/commands/extracts/encrypt_extracts_command.py b/src/commands/extracts/encrypt_extracts_command.py index 8cf32057..96d18422 100644 --- a/src/commands/extracts/encrypt_extracts_command.py +++ b/src/commands/extracts/encrypt_extracts_command.py @@ -26,7 +26,7 @@ def run_command(args): logger.debug(_("tabcmd.launching")) session = Session() server = session.create_session(args) - site_item = Server.get_site_for_command_or_throw(logger, server, args) + site_item = Server.get_site_for_command_or_throw(logger, server, args.site_name) try: logger.info(_("encryptextracts.status").format(site_item.name)) job = server.sites.encrypt_extracts(site_item.id) diff --git a/src/commands/extracts/reencrypt_extracts_command.py b/src/commands/extracts/reencrypt_extracts_command.py index 3071757b..42021a10 100644 --- a/src/commands/extracts/reencrypt_extracts_command.py +++ b/src/commands/extracts/reencrypt_extracts_command.py @@ -26,7 +26,7 @@ def run_command(args): logger.debug(_("tabcmd.launching")) session = Session() server = session.create_session(args) - site_item = Server.get_site_for_command_or_throw(logger, server, args) + site_item = Server.get_site_for_command_or_throw(logger, server, args.site_name) try: logger.info(_("reencryptextracts.status").format(site_item.name)) job = server.sites.encrypt_extracts(site_item.id) diff --git a/src/commands/server.py b/src/commands/server.py index dee5dd0b..3bec44ec 100644 --- a/src/commands/server.py +++ b/src/commands/server.py @@ -73,12 +73,9 @@ def get_items_by_name(logger, item_endpoint, item_name, container=None): # Get site by name or get currently logged in site @staticmethod - def get_site_for_command_or_throw(logger, server, args): - if args.site_name: - try: - site_item = Server.get_items_by_name(logger, server.sites, args.site_name)[0] - except Exception as e: - Errors.exit_with_error(logger, exception=e) + def get_site_for_command_or_throw(logger, server, site_name): + if site_name: + site_item = Server.get_site_by_name(logger, server, site_name) else: logger.debug("Use logged in site") site_item = server.sites.get_by_id(server.site_id) @@ -86,6 +83,14 @@ def get_site_for_command_or_throw(logger, server, args): raise ResourceWarning("Could not get site from server") return site_item + @staticmethod + def get_site_by_name(logger, server, site_name): + try: + site_item = Server.get_items_by_name(logger, server.sites, site_name)[0] + except Exception as e: + Errors.exit_with_error(logger, exception=e) + return site_item + @staticmethod def get_filename_extension_if_tableau_type(logger, filename): logger.debug("Filename given: {}".format(filename)) diff --git a/src/commands/site/create_site_command.py b/src/commands/site/create_site_command.py index 1e7489e8..ef44b56d 100644 --- a/src/commands/site/create_site_command.py +++ b/src/commands/site/create_site_command.py @@ -18,7 +18,7 @@ class CreateSiteCommand(Server): @staticmethod def define_args(create_site_parser): - create_site_parser.add_argument("site_name", metavar="site-name", help=_("editsite.options.site-name")) + create_site_parser.add_argument("new_site_name", metavar="site-name", help=_("editsite.options.site-name")) set_common_site_args(create_site_parser) @staticmethod @@ -27,21 +27,24 @@ def run_command(args): logger.debug(_("tabcmd.launching")) session = Session() server = session.create_session(args) + admin_mode = "ContentAndUsers" # default: allow site admins to manage users + if not args.site_admin_user_management: + admin_mode = "ContentOnly" new_site = TSC.SiteItem( - name=args.site_name, - content_url=args.url, - admin_mode=args.admin_mode, + name=args.new_site_name, + content_url=args.url or args.new_site_name, + admin_mode=admin_mode, user_quota=args.user_quota, storage_quota=args.storage_quota, ) try: - logger.info(_("createsite.status").format(args.site_name)) + logger.info(_("createsite.status").format(args.new_site_name)) server.sites.create(new_site) logger.info(_("common.output.succeeded")) except TSC.ServerResponseError as e: if Errors.is_resource_conflict(e): if args.continue_if_exists: - logger.info(_("createsite.errors.site_name_already_exists").format(args.site_name)) + logger.info(_("createsite.errors.site_name_already_exists").format(args.new_site_name)) return else: Errors.exit_with_error( diff --git a/src/commands/site/delete_site_command.py b/src/commands/site/delete_site_command.py index 1e6622c0..2f9ea640 100644 --- a/src/commands/site/delete_site_command.py +++ b/src/commands/site/delete_site_command.py @@ -17,7 +17,7 @@ class DeleteSiteCommand(Server): @staticmethod def define_args(delete_site_parser): - delete_site_parser.add_argument("site_name", help="name of site to delete") + delete_site_parser.add_argument("site_name_to_delete", metavar="site-name", help="name of site to delete") @staticmethod def run_command(args): @@ -25,11 +25,9 @@ def run_command(args): logger.debug(_("tabcmd.launching")) session = Session() server = session.create_session(args) - site_id = server.sites.get_by_name(args.site_name) - if site_id == session.site_id: - Errors.exit_with_error(logger, "Cannot delete the site you are logged in to") + site_url = Server.get_site_by_name(logger, server, args.site_name_to_delete).content_url try: - server.sites.delete(site_id) + server.sites.delete(site_url) logger.info("Successfully deleted the site") except TSC.ServerResponseError as e: Errors.exit_with_error(logger, "Error deleting site", e) diff --git a/src/commands/site/edit_site_command.py b/src/commands/site/edit_site_command.py index d384f34d..a055a45f 100644 --- a/src/commands/site/edit_site_command.py +++ b/src/commands/site/edit_site_command.py @@ -34,7 +34,7 @@ def run_command(args): session = Session() server = session.create_session(args) - site_item = Server.get_site_for_command_or_throw(logger, server, args) + site_item = Server.get_site_for_command_or_throw(logger, server, args.site_name) if args.url: site_item.content_url = args.url if args.user_quota: diff --git a/tests/commands/test_run_commands.py b/tests/commands/test_run_commands.py index 7ffd9e2c..f654598b 100644 --- a/tests/commands/test_run_commands.py +++ b/tests/commands/test_run_commands.py @@ -224,9 +224,9 @@ def test_create_project_already_exists(self, mock_session, mock_server): def test_create_site_already_exists(self, mock_session, mock_server): RunCommandsTest._set_up_session(mock_session, mock_server) mock_args.continue_if_exists = True - mock_args.site_name = "duplicate" + mock_args.new_site_name = "duplicate" mock_args.url = "dplct" - mock_args.admin_mode = None + mock_args.site_admin_user_management = None mock_args.user_quota = None mock_args.storage_quota = None mock_server.sites.create.return_value = TSC.ServerResponseError(409, "already exists", "detail") @@ -278,9 +278,9 @@ def test_publish_samples(self, mock_session, mock_server): # site def test_create_site(self, mock_session, mock_server): RunCommandsTest._set_up_session(mock_session, mock_server) - mock_args.site_name = "site-name" + mock_args.new_site_name = "site-name" mock_args.url = "site-content-url" - mock_args.admin_mode = None + mock_args.site_admin_user_management = None mock_args.user_quota = (None,) mock_args.storage_quota = None create_site_command.CreateSiteCommand.run_command(mock_args) @@ -289,7 +289,7 @@ def test_create_site(self, mock_session, mock_server): def test_delete_site(self, mock_session, mock_server): RunCommandsTest._set_up_session(mock_session, mock_server) mock_server.sites = getter - mock_args.site_name = "site-name" + mock_args.site_name_to_delete = "site-name" delete_site_command.DeleteSiteCommand.run_command(mock_args) mock_session.assert_called() diff --git a/tests/parsers/test_parser_create_site.py b/tests/parsers/test_parser_create_site.py index 1cffbb88..68f40b7d 100644 --- a/tests/parsers/test_parser_create_site.py +++ b/tests/parsers/test_parser_create_site.py @@ -14,7 +14,7 @@ def setUpClass(cls): def test_create_site_parser_just_a_name(self): mock_args = [commandname, "site-name"] args = self.parser_under_test.parse_args(mock_args) - assert args.site_name == "site-name", args + assert args.new_site_name == "site-name", args def test_create_site_parser_missing_required_name(self): mock_args = [commandname] @@ -37,6 +37,6 @@ def test_create_site_parser_with_all_args(self): ] # what else? args = self.parser_under_test.parse_args(mock_args) print(args) - assert args.site_name == "site-name", args + assert args.new_site_name == "site-name", args assert args.user_quota == 12, args assert args.storage_quota == 12, args diff --git a/tests/parsers/test_parser_delete_site.py b/tests/parsers/test_parser_delete_site.py index 00332236..843e9c82 100644 --- a/tests/parsers/test_parser_delete_site.py +++ b/tests/parsers/test_parser_delete_site.py @@ -14,7 +14,7 @@ def setUpClass(cls): def test_delete_site(self): mock_args = [commandname, "site-name"] args = self.parser_under_test.parse_args(mock_args) - assert args.site_name == "site-name", args + assert args.site_name_to_delete == "site-name", args def test_delete_site_required_name_none(self): mock_args = [commandname] From c1f3941158a773d3967eca312296146aeec76bbe Mon Sep 17 00:00:00 2001 From: Bhuvnesh Singh Date: Mon, 18 Jul 2022 10:18:00 -0700 Subject: [PATCH 09/37] Getting workbook works as expected, but appends extra file extensions --- src/commands/datasources_and_workbooks/get_url_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/datasources_and_workbooks/get_url_command.py b/src/commands/datasources_and_workbooks/get_url_command.py index 557cfd1c..36bc8f24 100644 --- a/src/commands/datasources_and_workbooks/get_url_command.py +++ b/src/commands/datasources_and_workbooks/get_url_command.py @@ -185,7 +185,7 @@ def generate_twb(logger, server, args, file_extension): 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) - server.workbooks.download(target_workbook.id, filepath=file_name_with_path, no_extract=False) + server.workbooks.download(target_workbook.id, filepath=None, no_extract=False) logger.info(_("export.success").format(target_workbook.name, file_name_with_path)) except TSC.ServerResponseError as e: Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) From 52a479352f2ad72ad2020dc9dec259ecdc65d3f0 Mon Sep 17 00:00:00 2001 From: Jac Date: Mon, 18 Jul 2022 16:06:09 -0700 Subject: [PATCH 10/37] Fix get view, get csv (#143) --- .../datasources_and_workbooks/get_url_command.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/commands/datasources_and_workbooks/get_url_command.py b/src/commands/datasources_and_workbooks/get_url_command.py index 557cfd1c..7718ad4b 100644 --- a/src/commands/datasources_and_workbooks/get_url_command.py +++ b/src/commands/datasources_and_workbooks/get_url_command.py @@ -65,7 +65,9 @@ def evaluate_content_type(logger, url): elif url.find("/workbooks/") == 0: return "workbook" else: - Errors.exit_with_error(logger, message=_("export.errors.requires_workbook_view_param").format(GetUrl.name)) + Errors.exit_with_error( + logger, message=_("export.errors.requires_workbook_view_param").format(__class__.__name__) + ) @staticmethod def get_file_type_from_filename(logger, file_name, url): @@ -77,7 +79,7 @@ def get_file_type_from_filename(logger, file_name, url): if not type_of_file: Errors.exit_with_error(logger, _("tabcmd.get.extension.not_found").format(file_name)) else: - logger.debug(_("get.options.file") + ": {}".format(type_of_file)) + logger.debug("filetype: {}".format(type_of_file)) if type_of_file in ["pdf", "csv", "png", "twb", "twbx"]: return type_of_file @@ -152,11 +154,11 @@ def generate_png(logger, server, args): try: view_item: TSC.ViewItem = GetUrl.get_view_by_content_url(logger, server, view) logger.debug(_("content_type.view") + ": {}".format(view_item.name)) - req_option_csv = TSC.CSVRequestOptions(maxage=1) - server.views.populate_csv(view_item, req_option_csv) + req_option_csv = TSC.CSVRequestOptions(maxage=1) # same as png + server.views.populate_image(view_item, req_option_csv) filename = GetUrl.filename_from_args(args.filename, view_item.name, "png") with open(filename, "wb") as f: - f.write(view_item.png) + f.write(view_item.image) logger.info(_("export.success").format(view_item.name, filename)) except TSC.ServerResponseError as e: Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) @@ -171,7 +173,7 @@ def generate_csv(logger, server, args): server.views.populate_csv(view_item, req_option_csv) file_name_with_path = GetUrl.filename_from_args(args.filename, view_item.name, "csv") with open(file_name_with_path, "wb") as f: - f.write(view_item.csv) + f.writelines(view_item.csv) logger.info(_("export.success").format(view_item.name, file_name_with_path)) except TSC.ServerResponseError as e: Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) @@ -181,10 +183,12 @@ def generate_csv(logger, server, args): @staticmethod def generate_twb(logger, server, args, file_extension): workbook_name = GetUrl.get_workbook_name(logger, args.url) + try: 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=file_name_with_path, no_extract=False) logger.info(_("export.success").format(target_workbook.name, file_name_with_path)) except TSC.ServerResponseError as e: From 76964342053302f0999e337c67bde8709e224d9b Mon Sep 17 00:00:00 2001 From: Brian Cantoni Date: Tue, 19 Jul 2022 10:42:14 -0700 Subject: [PATCH 11/37] Change install destination (#150) * Rename src dir to tabcmd so that setup.py install will go to the 'tabcmd' destination directory * Update references in scripts and code to reflect src-->tabcmd --- .github/workflows/generate-metadata.yml | 2 +- README.md | 6 +- contributing.md | 2 +- dodo.py | 12 ++-- setup.py | 4 +- src/execution/map_of_commands.py | 61 ------------------ tabcmd-linux.spec | 4 +- tabcmd-mac.spec | 4 +- tabcmd-windows.spec | 4 +- tabcmd.spec | 2 +- {src => tabcmd}/__init__.py | 0 {src => tabcmd}/__main__.py | 2 +- {src => tabcmd}/commands/__init__.py | 0 {src => tabcmd}/commands/auth/__init__.py | 0 .../commands/auth/login_command.py | 6 +- .../commands/auth/logout_command.py | 8 +-- {src => tabcmd}/commands/auth/session.py | 6 +- {src => tabcmd}/commands/commands.py | 0 {src => tabcmd}/commands/constants.py | 2 +- .../datasources_and_workbooks/__init__.py | 0 .../datasources_and_workbooks_command.py | 6 +- .../delete_command.py | 10 +-- .../export_command.py | 8 +-- .../get_url_command.py | 10 +-- .../publish_command.py | 12 ++-- .../runschedule_command.py | 8 +-- {src => tabcmd}/commands/extracts/__init__.py | 0 .../extracts/create_extracts_command.py | 12 ++-- .../extracts/decrypt_extracts_command.py | 10 +-- .../extracts/delete_extracts_command.py | 12 ++-- .../extracts/encrypt_extracts_command.py | 10 +-- .../extracts/reencrypt_extracts_command.py | 10 +-- .../extracts/refresh_extracts_command.py | 12 ++-- {src => tabcmd}/commands/group/__init__.py | 0 .../commands/group/create_group_command.py | 10 +-- .../commands/group/delete_group_command.py | 10 +-- {src => tabcmd}/commands/help/__init__.py | 0 {src => tabcmd}/commands/help/help_command.py | 6 +- {src => tabcmd}/commands/project/__init__.py | 0 .../project/create_project_command.py | 12 ++-- .../project/delete_project_command.py | 12 ++-- .../project/publish_samples_command.py | 12 ++-- {src => tabcmd}/commands/server.py | 4 +- {src => tabcmd}/commands/site/__init__.py | 0 .../commands/site/create_site_command.py | 12 ++-- .../commands/site/delete_site_command.py | 10 +-- .../commands/site/edit_site_command.py | 12 ++-- .../commands/site/list_sites_command.py | 12 ++-- {src => tabcmd}/commands/user/__init__.py | 0 .../commands/user/add_users_command.py | 8 +-- .../commands/user/create_site_users.py | 8 +-- .../commands/user/create_users_command.py | 10 +-- .../user/delete_site_users_command.py | 14 ++-- .../commands/user/remove_users_command.py | 8 +-- {src => tabcmd}/commands/user/user_data.py | 6 +- {src => tabcmd}/execution/__init__.py | 0 {src => tabcmd}/execution/_version.py | 0 {src => tabcmd}/execution/global_options.py | 0 {src => tabcmd}/execution/localize.py | 0 {src => tabcmd}/execution/logger_config.py | 0 tabcmd/execution/map_of_commands.py | 61 ++++++++++++++++++ {src => tabcmd}/execution/parent_parser.py | 0 .../execution/tabcmd_controller.py | 0 .../locales/de/LC_MESSAGES/.gitkeep | 0 {src => tabcmd}/locales/de/LC_MESSAGES/de.po | 0 .../locales/de/LC_MESSAGES/shared_wg_de.po | 0 .../locales/de/LC_MESSAGES/tabcmd.mo | Bin .../locales/de/LC_MESSAGES/tabcmd.po | 0 .../de/LC_MESSAGES/tabcmd_messages_de.po | 0 .../locales/de/shared_wg_de.properties | 0 .../locales/de/tabcmd_messages_de.properties | 0 .../locales/en/LC_MESSAGES/en-US.po | 0 .../locales/en/LC_MESSAGES/shared_wg_en.po | 0 .../locales/en/LC_MESSAGES/tabcmd.mo | Bin .../locales/en/LC_MESSAGES/tabcmd.po | 0 .../en/LC_MESSAGES/tabcmd_messages_en.po | 0 .../locales/en/shared_wg_en.properties | 0 .../locales/en/tabcmd_messages_en.properties | 0 .../locales/es/LC_MESSAGES/.gitkeep | 0 {src => tabcmd}/locales/es/LC_MESSAGES/es.po | 0 .../locales/es/LC_MESSAGES/shared_wg_es.po | 0 .../locales/es/LC_MESSAGES/tabcmd.mo | Bin .../locales/es/LC_MESSAGES/tabcmd.po | 0 .../es/LC_MESSAGES/tabcmd_messages_es.po | 0 .../locales/es/shared_wg_es.properties | 0 .../locales/es/tabcmd_messages_es.properties | 0 .../locales/fr/LC_MESSAGES/.gitkeep | 0 {src => tabcmd}/locales/fr/LC_MESSAGES/fr.po | 0 .../locales/fr/LC_MESSAGES/shared_wg_fr.po | 0 .../locales/fr/LC_MESSAGES/tabcmd.mo | Bin .../locales/fr/LC_MESSAGES/tabcmd.po | 0 .../fr/LC_MESSAGES/tabcmd_messages_fr.po | 0 .../locales/fr/shared_wg_fr.properties | 0 .../locales/fr/tabcmd_messages_fr.properties | 0 .../locales/ga/LC_MESSAGES/.gitkeep | 0 {src => tabcmd}/locales/ga/LC_MESSAGES/ga.po | 0 .../locales/ga/LC_MESSAGES/shared_wg_ga.po | 0 .../locales/ga/LC_MESSAGES/tabcmd.mo | Bin .../locales/ga/LC_MESSAGES/tabcmd.po | 0 .../ga/LC_MESSAGES/tabcmd_messages_ga.po | 0 .../locales/ga/shared_wg_ga.properties | 0 .../locales/ga/tabcmd_messages_ga.properties | 0 .../locales/it/LC_MESSAGES/.gitkeep | 0 {src => tabcmd}/locales/it/LC_MESSAGES/it.po | 0 .../locales/it/LC_MESSAGES/shared_wg_it.po | 0 .../locales/it/LC_MESSAGES/tabcmd.mo | Bin .../locales/it/LC_MESSAGES/tabcmd.po | 0 .../it/LC_MESSAGES/tabcmd_messages_it.po | 0 .../locales/it/shared_wg_it.properties | 0 .../locales/it/tabcmd_messages_it.properties | 0 .../locales/ja/LC_MESSAGES/.gitkeep | 0 {src => tabcmd}/locales/ja/LC_MESSAGES/ja.po | 0 .../locales/ja/LC_MESSAGES/shared_wg_ja.po | 0 .../locales/ja/LC_MESSAGES/tabcmd.mo | Bin .../locales/ja/LC_MESSAGES/tabcmd.po | 0 .../ja/LC_MESSAGES/tabcmd_messages_ja.po | 0 .../locales/ja/shared_wg_ja.properties | 0 .../locales/ja/tabcmd_messages_ja.properties | 0 .../locales/ko/LC_MESSAGES/.gitkeep | 0 {src => tabcmd}/locales/ko/LC_MESSAGES/ko.po | 0 .../locales/ko/LC_MESSAGES/shared_wg_ko.po | 0 .../locales/ko/LC_MESSAGES/tabcmd.mo | Bin .../locales/ko/LC_MESSAGES/tabcmd.po | 0 .../ko/LC_MESSAGES/tabcmd_messages_ko.po | 0 .../locales/ko/shared_wg_ko.properties | 0 .../locales/ko/tabcmd_messages_ko.properties | 0 .../locales/pt/LC_MESSAGES/.gitkeep | 0 {src => tabcmd}/locales/pt/LC_MESSAGES/pt.po | 0 .../locales/pt/LC_MESSAGES/shared_wg_pt.po | 0 .../locales/pt/LC_MESSAGES/tabcmd.mo | Bin .../locales/pt/LC_MESSAGES/tabcmd.po | 0 .../pt/LC_MESSAGES/tabcmd_messages_pt.po | 0 .../locales/pt/shared_wg_pt.properties | 0 .../locales/pt/tabcmd_messages_pt.properties | 0 .../locales/sv/LC_MESSAGES/.gitkeep | 0 .../locales/sv/LC_MESSAGES/shared_wg_sv.po | 0 {src => tabcmd}/locales/sv/LC_MESSAGES/sv.po | 0 .../locales/sv/LC_MESSAGES/tabcmd.mo | Bin .../locales/sv/LC_MESSAGES/tabcmd.po | 0 .../sv/LC_MESSAGES/tabcmd_messages_sv.po | 0 .../locales/sv/shared_wg_sv.properties | 0 .../locales/sv/tabcmd_messages_sv.properties | 0 .../locales/zh/LC_MESSAGES/.gitkeep | 0 .../locales/zh/LC_MESSAGES/shared_wg_zh.po | 0 .../locales/zh/LC_MESSAGES/tabcmd.mo | Bin .../locales/zh/LC_MESSAGES/tabcmd.po | 0 .../zh/LC_MESSAGES/tabcmd_messages_zh.po | 0 {src => tabcmd}/locales/zh/LC_MESSAGES/zh.po | 0 .../locales/zh/shared_wg_zh.properties | 0 .../locales/zh/tabcmd_messages_zh.properties | 0 {src => tabcmd}/tabcmd.py | 2 +- tests/commands/test_execution.py | 4 +- tests/commands/test_geturl_utils.py | 6 +- tests/commands/test_localize.py | 2 +- tests/commands/test_projects_utils.py | 4 +- tests/commands/test_run_commands.py | 20 +++--- tests/commands/test_server_handler.py | 2 +- tests/commands/test_session.py | 2 +- tests/commands/test_user_utils.py | 4 +- tests/e2e/tests_integration.py | 6 +- tests/parsers/common_setup.py | 2 +- tests/parsers/test_login_parser.py | 2 +- tests/parsers/test_logout_parser.py | 2 +- tests/parsers/test_parser_add_user.py | 2 +- tests/parsers/test_parser_create_extracts.py | 2 +- tests/parsers/test_parser_create_group.py | 2 +- tests/parsers/test_parser_create_project.py | 2 +- tests/parsers/test_parser_create_site.py | 2 +- .../parsers/test_parser_create_site_users.py | 2 +- tests/parsers/test_parser_create_user.py | 2 +- tests/parsers/test_parser_decrypt_extracts.py | 2 +- tests/parsers/test_parser_delete.py | 2 +- tests/parsers/test_parser_delete_extracts.py | 2 +- tests/parsers/test_parser_delete_group.py | 2 +- tests/parsers/test_parser_delete_project.py | 2 +- tests/parsers/test_parser_delete_site.py | 2 +- tests/parsers/test_parser_delete_site_user.py | 2 +- tests/parsers/test_parser_edit_site.py | 2 +- tests/parsers/test_parser_encrypt_extracts.py | 2 +- tests/parsers/test_parser_export.py | 2 +- tests/parsers/test_parser_get_url.py | 2 +- tests/parsers/test_parser_list_sites.py | 2 +- tests/parsers/test_parser_publish.py | 2 +- tests/parsers/test_parser_publish_samples.py | 2 +- .../parsers/test_parser_reencrypt_extracts.py | 2 +- tests/parsers/test_parser_refresh_extracts.py | 2 +- tests/parsers/test_parser_remove_user.py | 2 +- tests/parsers/test_parser_runschedule.py | 2 +- 188 files changed, 290 insertions(+), 290 deletions(-) delete mode 100644 src/execution/map_of_commands.py rename {src => tabcmd}/__init__.py (100%) rename {src => tabcmd}/__main__.py (85%) rename {src => tabcmd}/commands/__init__.py (100%) rename {src => tabcmd}/commands/auth/__init__.py (100%) rename {src => tabcmd}/commands/auth/login_command.py (79%) rename {src => tabcmd}/commands/auth/logout_command.py (72%) rename {src => tabcmd}/commands/auth/session.py (99%) rename {src => tabcmd}/commands/commands.py (100%) rename {src => tabcmd}/commands/constants.py (97%) rename {src => tabcmd}/commands/datasources_and_workbooks/__init__.py (100%) rename {src => tabcmd}/commands/datasources_and_workbooks/datasources_and_workbooks_command.py (93%) rename {src => tabcmd}/commands/datasources_and_workbooks/delete_command.py (89%) rename {src => tabcmd}/commands/datasources_and_workbooks/export_command.py (97%) rename {src => tabcmd}/commands/datasources_and_workbooks/get_url_command.py (97%) rename {src => tabcmd}/commands/datasources_and_workbooks/publish_command.py (91%) rename {src => tabcmd}/commands/datasources_and_workbooks/runschedule_command.py (83%) rename {src => tabcmd}/commands/extracts/__init__.py (100%) rename {src => tabcmd}/commands/extracts/create_extracts_command.py (88%) rename {src => tabcmd}/commands/extracts/decrypt_extracts_command.py (84%) rename {src => tabcmd}/commands/extracts/delete_extracts_command.py (86%) rename {src => tabcmd}/commands/extracts/encrypt_extracts_command.py (84%) rename {src => tabcmd}/commands/extracts/reencrypt_extracts_command.py (84%) rename {src => tabcmd}/commands/extracts/refresh_extracts_command.py (94%) rename {src => tabcmd}/commands/group/__init__.py (100%) rename {src => tabcmd}/commands/group/create_group_command.py (84%) rename {src => tabcmd}/commands/group/delete_group_command.py (83%) rename {src => tabcmd}/commands/help/__init__.py (100%) rename {src => tabcmd}/commands/help/help_command.py (94%) rename {src => tabcmd}/commands/project/__init__.py (100%) rename {src => tabcmd}/commands/project/create_project_command.py (89%) rename {src => tabcmd}/commands/project/delete_project_command.py (85%) rename {src => tabcmd}/commands/project/publish_samples_command.py (85%) rename {src => tabcmd}/commands/server.py (98%) rename {src => tabcmd}/commands/site/__init__.py (100%) rename {src => tabcmd}/commands/site/create_site_command.py (87%) rename {src => tabcmd}/commands/site/delete_site_command.py (80%) rename {src => tabcmd}/commands/site/edit_site_command.py (86%) rename {src => tabcmd}/commands/site/list_sites_command.py (80%) rename {src => tabcmd}/commands/user/__init__.py (100%) rename {src => tabcmd}/commands/user/add_users_command.py (82%) rename {src => tabcmd}/commands/user/create_site_users.py (92%) rename {src => tabcmd}/commands/user/create_users_command.py (91%) rename {src => tabcmd}/commands/user/delete_site_users_command.py (86%) rename {src => tabcmd}/commands/user/remove_users_command.py (83%) rename {src => tabcmd}/commands/user/user_data.py (98%) rename {src => tabcmd}/execution/__init__.py (100%) rename {src => tabcmd}/execution/_version.py (100%) rename {src => tabcmd}/execution/global_options.py (100%) rename {src => tabcmd}/execution/localize.py (100%) rename {src => tabcmd}/execution/logger_config.py (100%) create mode 100644 tabcmd/execution/map_of_commands.py rename {src => tabcmd}/execution/parent_parser.py (100%) rename {src => tabcmd}/execution/tabcmd_controller.py (100%) rename {src => tabcmd}/locales/de/LC_MESSAGES/.gitkeep (100%) rename {src => tabcmd}/locales/de/LC_MESSAGES/de.po (100%) rename {src => tabcmd}/locales/de/LC_MESSAGES/shared_wg_de.po (100%) rename {src => tabcmd}/locales/de/LC_MESSAGES/tabcmd.mo (100%) rename {src => tabcmd}/locales/de/LC_MESSAGES/tabcmd.po (100%) rename {src => tabcmd}/locales/de/LC_MESSAGES/tabcmd_messages_de.po (100%) rename {src => tabcmd}/locales/de/shared_wg_de.properties (100%) rename {src => tabcmd}/locales/de/tabcmd_messages_de.properties (100%) rename {src => tabcmd}/locales/en/LC_MESSAGES/en-US.po (100%) rename {src => tabcmd}/locales/en/LC_MESSAGES/shared_wg_en.po (100%) rename {src => tabcmd}/locales/en/LC_MESSAGES/tabcmd.mo (100%) rename {src => tabcmd}/locales/en/LC_MESSAGES/tabcmd.po (100%) rename {src => tabcmd}/locales/en/LC_MESSAGES/tabcmd_messages_en.po (100%) rename {src => tabcmd}/locales/en/shared_wg_en.properties (100%) rename {src => tabcmd}/locales/en/tabcmd_messages_en.properties (100%) rename {src => tabcmd}/locales/es/LC_MESSAGES/.gitkeep (100%) rename {src => tabcmd}/locales/es/LC_MESSAGES/es.po (100%) rename {src => tabcmd}/locales/es/LC_MESSAGES/shared_wg_es.po (100%) rename {src => tabcmd}/locales/es/LC_MESSAGES/tabcmd.mo (100%) rename {src => tabcmd}/locales/es/LC_MESSAGES/tabcmd.po (100%) rename {src => tabcmd}/locales/es/LC_MESSAGES/tabcmd_messages_es.po (100%) rename {src => tabcmd}/locales/es/shared_wg_es.properties (100%) rename {src => tabcmd}/locales/es/tabcmd_messages_es.properties (100%) rename {src => tabcmd}/locales/fr/LC_MESSAGES/.gitkeep (100%) rename {src => tabcmd}/locales/fr/LC_MESSAGES/fr.po (100%) rename {src => tabcmd}/locales/fr/LC_MESSAGES/shared_wg_fr.po (100%) rename {src => tabcmd}/locales/fr/LC_MESSAGES/tabcmd.mo (100%) rename {src => tabcmd}/locales/fr/LC_MESSAGES/tabcmd.po (100%) rename {src => tabcmd}/locales/fr/LC_MESSAGES/tabcmd_messages_fr.po (100%) rename {src => tabcmd}/locales/fr/shared_wg_fr.properties (100%) rename {src => tabcmd}/locales/fr/tabcmd_messages_fr.properties (100%) rename {src => tabcmd}/locales/ga/LC_MESSAGES/.gitkeep (100%) rename {src => tabcmd}/locales/ga/LC_MESSAGES/ga.po (100%) rename {src => tabcmd}/locales/ga/LC_MESSAGES/shared_wg_ga.po (100%) rename {src => tabcmd}/locales/ga/LC_MESSAGES/tabcmd.mo (100%) rename {src => tabcmd}/locales/ga/LC_MESSAGES/tabcmd.po (100%) rename {src => tabcmd}/locales/ga/LC_MESSAGES/tabcmd_messages_ga.po (100%) rename {src => tabcmd}/locales/ga/shared_wg_ga.properties (100%) rename {src => tabcmd}/locales/ga/tabcmd_messages_ga.properties (100%) rename {src => tabcmd}/locales/it/LC_MESSAGES/.gitkeep (100%) rename {src => tabcmd}/locales/it/LC_MESSAGES/it.po (100%) rename {src => tabcmd}/locales/it/LC_MESSAGES/shared_wg_it.po (100%) rename {src => tabcmd}/locales/it/LC_MESSAGES/tabcmd.mo (100%) rename {src => tabcmd}/locales/it/LC_MESSAGES/tabcmd.po (100%) rename {src => tabcmd}/locales/it/LC_MESSAGES/tabcmd_messages_it.po (100%) rename {src => tabcmd}/locales/it/shared_wg_it.properties (100%) rename {src => tabcmd}/locales/it/tabcmd_messages_it.properties (100%) rename {src => tabcmd}/locales/ja/LC_MESSAGES/.gitkeep (100%) rename {src => tabcmd}/locales/ja/LC_MESSAGES/ja.po (100%) rename {src => tabcmd}/locales/ja/LC_MESSAGES/shared_wg_ja.po (100%) rename {src => tabcmd}/locales/ja/LC_MESSAGES/tabcmd.mo (100%) rename {src => tabcmd}/locales/ja/LC_MESSAGES/tabcmd.po (100%) rename {src => tabcmd}/locales/ja/LC_MESSAGES/tabcmd_messages_ja.po (100%) rename {src => tabcmd}/locales/ja/shared_wg_ja.properties (100%) rename {src => tabcmd}/locales/ja/tabcmd_messages_ja.properties (100%) rename {src => tabcmd}/locales/ko/LC_MESSAGES/.gitkeep (100%) rename {src => tabcmd}/locales/ko/LC_MESSAGES/ko.po (100%) rename {src => tabcmd}/locales/ko/LC_MESSAGES/shared_wg_ko.po (100%) rename {src => tabcmd}/locales/ko/LC_MESSAGES/tabcmd.mo (100%) rename {src => tabcmd}/locales/ko/LC_MESSAGES/tabcmd.po (100%) rename {src => tabcmd}/locales/ko/LC_MESSAGES/tabcmd_messages_ko.po (100%) rename {src => tabcmd}/locales/ko/shared_wg_ko.properties (100%) rename {src => tabcmd}/locales/ko/tabcmd_messages_ko.properties (100%) rename {src => tabcmd}/locales/pt/LC_MESSAGES/.gitkeep (100%) rename {src => tabcmd}/locales/pt/LC_MESSAGES/pt.po (100%) rename {src => tabcmd}/locales/pt/LC_MESSAGES/shared_wg_pt.po (100%) rename {src => tabcmd}/locales/pt/LC_MESSAGES/tabcmd.mo (100%) rename {src => tabcmd}/locales/pt/LC_MESSAGES/tabcmd.po (100%) rename {src => tabcmd}/locales/pt/LC_MESSAGES/tabcmd_messages_pt.po (100%) rename {src => tabcmd}/locales/pt/shared_wg_pt.properties (100%) rename {src => tabcmd}/locales/pt/tabcmd_messages_pt.properties (100%) rename {src => tabcmd}/locales/sv/LC_MESSAGES/.gitkeep (100%) rename {src => tabcmd}/locales/sv/LC_MESSAGES/shared_wg_sv.po (100%) rename {src => tabcmd}/locales/sv/LC_MESSAGES/sv.po (100%) rename {src => tabcmd}/locales/sv/LC_MESSAGES/tabcmd.mo (100%) rename {src => tabcmd}/locales/sv/LC_MESSAGES/tabcmd.po (100%) rename {src => tabcmd}/locales/sv/LC_MESSAGES/tabcmd_messages_sv.po (100%) rename {src => tabcmd}/locales/sv/shared_wg_sv.properties (100%) rename {src => tabcmd}/locales/sv/tabcmd_messages_sv.properties (100%) rename {src => tabcmd}/locales/zh/LC_MESSAGES/.gitkeep (100%) rename {src => tabcmd}/locales/zh/LC_MESSAGES/shared_wg_zh.po (100%) rename {src => tabcmd}/locales/zh/LC_MESSAGES/tabcmd.mo (100%) rename {src => tabcmd}/locales/zh/LC_MESSAGES/tabcmd.po (100%) rename {src => tabcmd}/locales/zh/LC_MESSAGES/tabcmd_messages_zh.po (100%) rename {src => tabcmd}/locales/zh/LC_MESSAGES/zh.po (100%) rename {src => tabcmd}/locales/zh/shared_wg_zh.properties (100%) rename {src => tabcmd}/locales/zh/tabcmd_messages_zh.properties (100%) rename {src => tabcmd}/tabcmd.py (90%) diff --git a/.github/workflows/generate-metadata.yml b/.github/workflows/generate-metadata.yml index 12513ace..213b564a 100644 --- a/.github/workflows/generate-metadata.yml +++ b/.github/workflows/generate-metadata.yml @@ -29,7 +29,7 @@ jobs: run: python bin/license-checker.py - name: Type-check - run: mypy src tests + run: mypy tabcmd tests - name: Check formatting with black run: black . --check diff --git a/README.md b/README.md index 5ec40a8a..c22552aa 100644 --- a/README.md +++ b/README.md @@ -58,13 +58,13 @@ To run tabcmd from your local copy, from a console window in the same directory > coverage run -m pytest && coverage report -m - autoformat your code with black (https://pypi.org/project/black/) -> black --line-length 120 src tests [--check] +> black --line-length 120 tabcmd tests [--check] - type check with mypy -> mypy src tests +> mypy tabcmd tests - packaging is done with pyinstaller. You can only build an executable for the platform you build on. -> pyinstaller src\tabcmd.py --clean --noconfirm +> pyinstaller tabcmd\tabcmd.py --clean --noconfirm produces dist/tabcmd.exe To run tabcmd during development, from a console window in the same directory as the file tabcmd.py: diff --git a/contributing.md b/contributing.md index c7c77036..723e8f86 100644 --- a/contributing.md +++ b/contributing.md @@ -34,7 +34,7 @@ _(note that running mypy and black is required for code being submitted to the r - autoformat your code with black (https://pypi.org/project/black/) > black . - check types -> mypy src tests +> mypy tabcmd tests - do test coverage calculation (https://coverage.readthedocs.io/en/6.3.2) > bin/coverage.sh diff --git a/dodo.py b/dodo.py index 3e4272a5..c24d9506 100644 --- a/dodo.py +++ b/dodo.py @@ -33,7 +33,7 @@ def process_locales(): else: encoding = "cp1252" - for file in glob.glob("src/locales/" + current_locale + "/*.properties"): + for file in glob.glob("tabcmd/locales/" + current_locale + "/*.properties"): basename = os.path.basename(file).split(".")[0] print("transcoding", basename) with open(file, encoding=encoding) as infile: @@ -66,7 +66,7 @@ def task_po(): def process_locales(): for current_locale in LOCALES: - LOC_PATH = "src/locales/" + current_locale + LOC_PATH = "tabcmd/locales/" + current_locale for file in glob.glob(LOC_PATH + "/*.properties"): basename = os.path.basename(file).split(".")[0] print("processing", basename) @@ -98,7 +98,7 @@ def task_clean_all(): def process_locales(): for current_locale in LOCALES: - LOC_PATH = "src/locales/" + current_locale + LOC_PATH = "tabcmd/locales/" + current_locale for file in glob.glob(LOC_PATH + "/*.properties"): basename = os.path.basename(file).split(".")[0] print("deleting", basename + ".*") @@ -130,7 +130,7 @@ def task_merge(): def process_locales(): for current_locale in LOCALES: - LOC_PATH = "src/locales/" + current_locale + "/LC_MESSAGES" + LOC_PATH = "tabcmd/locales/" + current_locale + "/LC_MESSAGES" with open(LOC_PATH + "/tabcmd.po", "w+", encoding="utf-8") as outfile: for file in glob.glob(LOC_PATH + "/*.po"): @@ -157,11 +157,11 @@ def task_mo(): def process_locales(): for current_locale in LOCALES: - LOC_PATH = "src/locales/" + current_locale + "/LC_MESSAGES" + LOC_PATH = "tabcmd/locales/" + current_locale + "/LC_MESSAGES" print("writing final tabcmd.mo file") # build the single binary file from the .po file - result = subprocess.run(["python", "bin/i18n/msgfmt.py", LOC_PATH + "/src"]) + result = subprocess.run(["python", "bin/i18n/msgfmt.py", LOC_PATH + "/tabcmd"]) print("\n", result) # print("stdout:", result.stdout) if not result.returncode == 0: diff --git a/setup.py b/setup.py index 57466d7d..8d834b82 100644 --- a/setup.py +++ b/setup.py @@ -10,9 +10,9 @@ url="https://github.com/tableau/tabcmd", python_requires=">=3.7", packages=find_packages(), - package_data={"tabcmd": ["src.locales/**/*.mo"]}, + package_data={"tabcmd": ["tabcmd.locales/**/*.mo"]}, include_package_data=True, - entry_points={"console_scripts": ["tabcmd = src.tabcmd:main"]}, + entry_points={"console_scripts": ["tabcmd = tabcmd.tabcmd:main"]}, setup_requires=[ # copy of pyproject.toml for back compat "build", diff --git a/src/execution/map_of_commands.py b/src/execution/map_of_commands.py deleted file mode 100644 index 628349d2..00000000 --- a/src/execution/map_of_commands.py +++ /dev/null @@ -1,61 +0,0 @@ -from src.commands.auth.login_command import * -from src.commands.auth.logout_command import * -from src.commands.datasources_and_workbooks.delete_command import * -from src.commands.datasources_and_workbooks.export_command import * -from src.commands.datasources_and_workbooks.get_url_command import * -from src.commands.datasources_and_workbooks.publish_command import * -from src.commands.extracts.create_extracts_command import * -from src.commands.extracts.decrypt_extracts_command import * -from src.commands.extracts.delete_extracts_command import * -from src.commands.extracts.encrypt_extracts_command import * -from src.commands.extracts.reencrypt_extracts_command import * -from src.commands.extracts.refresh_extracts_command import * -from src.commands.group.create_group_command import * -from src.commands.group.delete_group_command import * -from src.commands.help.help_command import * -from src.commands.project.create_project_command import * -from src.commands.project.delete_project_command import * -from src.commands.project.publish_samples_command import * -from src.commands.site.create_site_command import * -from src.commands.site.delete_site_command import * -from src.commands.site.edit_site_command import * -from src.commands.site.list_sites_command import * -from src.commands.user.add_users_command import * -from src.commands.user.create_site_users import * -from src.commands.user.delete_site_users_command import * - -# from src.commands.user.create_users import * -from src.commands.user.remove_users_command import * - - -class CommandsMap: - commands_hash_map = [ - # not yet implemented "createusers": ("createusers", CreateUserCommand, "Create users on the server"), - # run schedule - AddUserCommand, - CreateExtracts, - CreateGroupCommand, - CreateProjectCommand, - CreateSiteCommand, - CreateSiteUsersCommand, - DecryptExtracts, - DeleteCommand, - DeleteExtracts, - DeleteGroupCommand, - DeleteProjectCommand, - DeleteSiteCommand, - DeleteSiteUsersCommand, - EditSiteCommand, - EncryptExtracts, - ExportCommand, - GetUrl, - HelpCommand, - ListSiteCommand, - LoginCommand, - LogoutCommand, - PublishCommand, - PublishSamplesCommand, - ReencryptExtracts, - RefreshExtracts, - RemoveUserCommand, - ] diff --git a/tabcmd-linux.spec b/tabcmd-linux.spec index 530baf62..d71fd83e 100644 --- a/tabcmd-linux.spec +++ b/tabcmd-linux.spec @@ -2,13 +2,13 @@ from PyInstaller.utils.hooks import collect_data_files datas = [] -datas += collect_data_files('src.locales') +datas += collect_data_files('tabcmd.locales') print(datas) block_cipher = None a = Analysis( - ['src\\tabcmd.py'], + ['tabcmd\\tabcmd.py'], pathex=[], binaries=[], datas=datas, diff --git a/tabcmd-mac.spec b/tabcmd-mac.spec index 5f56d46c..fcce4bfe 100644 --- a/tabcmd-mac.spec +++ b/tabcmd-mac.spec @@ -2,14 +2,14 @@ from PyInstaller.utils.hooks import collect_data_files datas = [] -datas += collect_data_files('src.locales') +datas += collect_data_files('tabcmd.locales') block_cipher = None a = Analysis( - ['src/tabcmd.py'], + ['tabcmd/tabcmd.py'], pathex=[], binaries=[], datas=datas, diff --git a/tabcmd-windows.spec b/tabcmd-windows.spec index 530baf62..d71fd83e 100644 --- a/tabcmd-windows.spec +++ b/tabcmd-windows.spec @@ -2,13 +2,13 @@ from PyInstaller.utils.hooks import collect_data_files datas = [] -datas += collect_data_files('src.locales') +datas += collect_data_files('tabcmd.locales') print(datas) block_cipher = None a = Analysis( - ['src\\tabcmd.py'], + ['tabcmd\\tabcmd.py'], pathex=[], binaries=[], datas=datas, diff --git a/tabcmd.spec b/tabcmd.spec index 4475458a..650b541a 100644 --- a/tabcmd.spec +++ b/tabcmd.spec @@ -5,7 +5,7 @@ block_cipher = None a = Analysis( - ['src\\tabcmd.py'], + ['tabcmd\\tabcmd.py'], pathex=[], binaries=[], datas=[], diff --git a/src/__init__.py b/tabcmd/__init__.py similarity index 100% rename from src/__init__.py rename to tabcmd/__init__.py diff --git a/src/__main__.py b/tabcmd/__main__.py similarity index 85% rename from src/__main__.py rename to tabcmd/__main__.py index 29ea8617..29886963 100644 --- a/src/__main__.py +++ b/tabcmd/__main__.py @@ -1,7 +1,7 @@ import sys try: - from src.tabcmd import main + from tabcmd.tabcmd import main except ImportError: print("Tabcmd needs to be run as a module, it cannot be run as a script") print("Try running python -m tabcmd") diff --git a/src/commands/__init__.py b/tabcmd/commands/__init__.py similarity index 100% rename from src/commands/__init__.py rename to tabcmd/commands/__init__.py diff --git a/src/commands/auth/__init__.py b/tabcmd/commands/auth/__init__.py similarity index 100% rename from src/commands/auth/__init__.py rename to tabcmd/commands/auth/__init__.py diff --git a/src/commands/auth/login_command.py b/tabcmd/commands/auth/login_command.py similarity index 79% rename from src/commands/auth/login_command.py rename to tabcmd/commands/auth/login_command.py index fd636369..647fee3f 100644 --- a/src/commands/auth/login_command.py +++ b/tabcmd/commands/auth/login_command.py @@ -1,6 +1,6 @@ -from src.commands.server import Server -from src.execution.localize import _ -from src.execution.logger_config import log +from tabcmd.commands.server import Server +from tabcmd.execution.localize import _ +from tabcmd.execution.logger_config import log from .session import Session diff --git a/src/commands/auth/logout_command.py b/tabcmd/commands/auth/logout_command.py similarity index 72% rename from src/commands/auth/logout_command.py rename to tabcmd/commands/auth/logout_command.py index 49eb72e7..b15f8246 100644 --- a/src/commands/auth/logout_command.py +++ b/tabcmd/commands/auth/logout_command.py @@ -1,7 +1,7 @@ -from src.commands.auth.session import Session -from src.commands.server import Server -from src.execution.localize import _ -from src.execution.logger_config import log +from tabcmd.commands.auth.session import Session +from tabcmd.commands.server import Server +from tabcmd.execution.localize import _ +from tabcmd.execution.logger_config import log class LogoutCommand(Server): diff --git a/src/commands/auth/session.py b/tabcmd/commands/auth/session.py similarity index 99% rename from src/commands/auth/session.py rename to tabcmd/commands/auth/session.py index e224d35d..ba98fd2d 100644 --- a/src/commands/auth/session.py +++ b/tabcmd/commands/auth/session.py @@ -7,9 +7,9 @@ import tableauserverclient.server.endpoint.exceptions from urllib3.exceptions import InsecureRequestWarning -from src.commands.constants import Errors -from src.execution.localize import _ -from src.execution.logger_config import log +from tabcmd.commands.constants import Errors +from tabcmd.execution.localize import _ +from tabcmd.execution.logger_config import log class Session: diff --git a/src/commands/commands.py b/tabcmd/commands/commands.py similarity index 100% rename from src/commands/commands.py rename to tabcmd/commands/commands.py diff --git a/src/commands/constants.py b/tabcmd/commands/constants.py similarity index 97% rename from src/commands/constants.py rename to tabcmd/commands/constants.py index 66cc0db2..fe642bbb 100644 --- a/src/commands/constants.py +++ b/tabcmd/commands/constants.py @@ -1,6 +1,6 @@ import sys -from src.execution.localize import _ +from tabcmd.execution.localize import _ class Constants: diff --git a/src/commands/datasources_and_workbooks/__init__.py b/tabcmd/commands/datasources_and_workbooks/__init__.py similarity index 100% rename from src/commands/datasources_and_workbooks/__init__.py rename to tabcmd/commands/datasources_and_workbooks/__init__.py diff --git a/src/commands/datasources_and_workbooks/datasources_and_workbooks_command.py b/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py similarity index 93% rename from src/commands/datasources_and_workbooks/datasources_and_workbooks_command.py rename to tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py index 1948914b..af54d65a 100644 --- a/src/commands/datasources_and_workbooks/datasources_and_workbooks_command.py +++ b/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py @@ -1,8 +1,8 @@ import tableauserverclient as TSC -from src.commands.constants import Errors -from src.commands.server import Server -from src.execution.localize import _ +from tabcmd.commands.constants import Errors +from tabcmd.commands.server import Server +from tabcmd.execution.localize import _ class DatasourcesAndWorkbooks(Server): diff --git a/src/commands/datasources_and_workbooks/delete_command.py b/tabcmd/commands/datasources_and_workbooks/delete_command.py similarity index 89% rename from src/commands/datasources_and_workbooks/delete_command.py rename to tabcmd/commands/datasources_and_workbooks/delete_command.py index 223eefbd..e9750a93 100644 --- a/src/commands/datasources_and_workbooks/delete_command.py +++ b/tabcmd/commands/datasources_and_workbooks/delete_command.py @@ -1,10 +1,10 @@ import tableauserverclient as TSC -from src.commands.auth.session import Session -from src.commands.constants import Errors -from src.execution.global_options import * -from src.execution.localize import _ -from src.execution.logger_config import log +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 from .datasources_and_workbooks_command import DatasourcesAndWorkbooks diff --git a/src/commands/datasources_and_workbooks/export_command.py b/tabcmd/commands/datasources_and_workbooks/export_command.py similarity index 97% rename from src/commands/datasources_and_workbooks/export_command.py rename to tabcmd/commands/datasources_and_workbooks/export_command.py index ff0b8da9..72f7375a 100644 --- a/src/commands/datasources_and_workbooks/export_command.py +++ b/tabcmd/commands/datasources_and_workbooks/export_command.py @@ -1,9 +1,9 @@ import tableauserverclient as TSC -from src.commands.auth.session import Session -from src.commands.constants import Errors -from src.execution.localize import _ -from src.execution.logger_config import log +from tabcmd.commands.auth.session import Session +from tabcmd.commands.constants import Errors +from tabcmd.execution.localize import _ +from tabcmd.execution.logger_config import log from .datasources_and_workbooks_command import DatasourcesAndWorkbooks diff --git a/src/commands/datasources_and_workbooks/get_url_command.py b/tabcmd/commands/datasources_and_workbooks/get_url_command.py similarity index 97% rename from src/commands/datasources_and_workbooks/get_url_command.py rename to tabcmd/commands/datasources_and_workbooks/get_url_command.py index 643b7e80..5b23dc4e 100644 --- a/src/commands/datasources_and_workbooks/get_url_command.py +++ b/tabcmd/commands/datasources_and_workbooks/get_url_command.py @@ -1,10 +1,10 @@ import tableauserverclient as TSC -from src.commands.auth.session import Session -from src.commands.constants import Errors -from src.execution.global_options import * -from src.execution.localize import _ -from src.execution.logger_config import log +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 from .datasources_and_workbooks_command import DatasourcesAndWorkbooks diff --git a/src/commands/datasources_and_workbooks/publish_command.py b/tabcmd/commands/datasources_and_workbooks/publish_command.py similarity index 91% rename from src/commands/datasources_and_workbooks/publish_command.py rename to tabcmd/commands/datasources_and_workbooks/publish_command.py index b360b775..c81c0091 100644 --- a/src/commands/datasources_and_workbooks/publish_command.py +++ b/tabcmd/commands/datasources_and_workbooks/publish_command.py @@ -1,11 +1,11 @@ import tableauserverclient as TSC -from src.commands.auth.session import Session -from src.commands.constants import Errors -from src.commands.server import Server -from src.execution.global_options import * -from src.execution.localize import _ -from src.execution.logger_config import log +from tabcmd.commands.auth.session import Session +from tabcmd.commands.constants import Errors +from tabcmd.commands.server import Server +from tabcmd.execution.global_options import * +from tabcmd.execution.localize import _ +from tabcmd.execution.logger_config import log from .datasources_and_workbooks_command import DatasourcesAndWorkbooks diff --git a/src/commands/datasources_and_workbooks/runschedule_command.py b/tabcmd/commands/datasources_and_workbooks/runschedule_command.py similarity index 83% rename from src/commands/datasources_and_workbooks/runschedule_command.py rename to tabcmd/commands/datasources_and_workbooks/runschedule_command.py index 0b58e396..f4eceb29 100644 --- a/src/commands/datasources_and_workbooks/runschedule_command.py +++ b/tabcmd/commands/datasources_and_workbooks/runschedule_command.py @@ -1,7 +1,7 @@ -from src.commands.auth.session import Session -from src.commands.constants import Errors -from src.execution.localize import _ -from src.execution.logger_config import log +from tabcmd.commands.auth.session import Session +from tabcmd.commands.constants import Errors +from tabcmd.execution.localize import _ +from tabcmd.execution.logger_config import log from .datasources_and_workbooks_command import DatasourcesAndWorkbooks diff --git a/src/commands/extracts/__init__.py b/tabcmd/commands/extracts/__init__.py similarity index 100% rename from src/commands/extracts/__init__.py rename to tabcmd/commands/extracts/__init__.py diff --git a/src/commands/extracts/create_extracts_command.py b/tabcmd/commands/extracts/create_extracts_command.py similarity index 88% rename from src/commands/extracts/create_extracts_command.py rename to tabcmd/commands/extracts/create_extracts_command.py index da9a0b71..f7f93b72 100644 --- a/src/commands/extracts/create_extracts_command.py +++ b/tabcmd/commands/extracts/create_extracts_command.py @@ -1,11 +1,11 @@ import tableauserverclient as TSC -from src.commands.auth.session import Session -from src.commands.constants import Errors -from src.commands.server import Server -from src.execution.global_options import * -from src.execution.localize import _ -from src.execution.logger_config import log +from tabcmd.commands.auth.session import Session +from tabcmd.commands.constants import Errors +from tabcmd.commands.server import Server +from tabcmd.execution.global_options import * +from tabcmd.execution.localize import _ +from tabcmd.execution.logger_config import log class CreateExtracts(Server): diff --git a/src/commands/extracts/decrypt_extracts_command.py b/tabcmd/commands/extracts/decrypt_extracts_command.py similarity index 84% rename from src/commands/extracts/decrypt_extracts_command.py rename to tabcmd/commands/extracts/decrypt_extracts_command.py index 2f02ec7a..6a983a06 100644 --- a/src/commands/extracts/decrypt_extracts_command.py +++ b/tabcmd/commands/extracts/decrypt_extracts_command.py @@ -1,10 +1,10 @@ import tableauserverclient as TSC -from src.commands.auth.session import Session -from src.commands.constants import Errors -from src.commands.server import Server -from src.execution.localize import _ -from src.execution.logger_config import log +from tabcmd.commands.auth.session import Session +from tabcmd.commands.constants import Errors +from tabcmd.commands.server import Server +from tabcmd.execution.localize import _ +from tabcmd.execution.logger_config import log class DecryptExtracts(Server): diff --git a/src/commands/extracts/delete_extracts_command.py b/tabcmd/commands/extracts/delete_extracts_command.py similarity index 86% rename from src/commands/extracts/delete_extracts_command.py rename to tabcmd/commands/extracts/delete_extracts_command.py index 27a35aa6..400d7c7a 100644 --- a/src/commands/extracts/delete_extracts_command.py +++ b/tabcmd/commands/extracts/delete_extracts_command.py @@ -1,11 +1,11 @@ import tableauserverclient as TSC -from src.commands.auth.session import Session -from src.commands.constants import Errors -from src.commands.server import Server -from src.execution.global_options import * -from src.execution.localize import _ -from src.execution.logger_config import log +from tabcmd.commands.auth.session import Session +from tabcmd.commands.constants import Errors +from tabcmd.commands.server import Server +from tabcmd.execution.global_options import * +from tabcmd.execution.localize import _ +from tabcmd.execution.logger_config import log class DeleteExtracts(Server): diff --git a/src/commands/extracts/encrypt_extracts_command.py b/tabcmd/commands/extracts/encrypt_extracts_command.py similarity index 84% rename from src/commands/extracts/encrypt_extracts_command.py rename to tabcmd/commands/extracts/encrypt_extracts_command.py index 96d18422..2d3b7ae4 100644 --- a/src/commands/extracts/encrypt_extracts_command.py +++ b/tabcmd/commands/extracts/encrypt_extracts_command.py @@ -1,10 +1,10 @@ import tableauserverclient as TSC -from src.commands.auth.session import Session -from src.commands.constants import Errors -from src.commands.server import Server -from src.execution.localize import _ -from src.execution.logger_config import log +from tabcmd.commands.auth.session import Session +from tabcmd.commands.constants import Errors +from tabcmd.commands.server import Server +from tabcmd.execution.localize import _ +from tabcmd.execution.logger_config import log class EncryptExtracts(Server): diff --git a/src/commands/extracts/reencrypt_extracts_command.py b/tabcmd/commands/extracts/reencrypt_extracts_command.py similarity index 84% rename from src/commands/extracts/reencrypt_extracts_command.py rename to tabcmd/commands/extracts/reencrypt_extracts_command.py index 42021a10..66785bfe 100644 --- a/src/commands/extracts/reencrypt_extracts_command.py +++ b/tabcmd/commands/extracts/reencrypt_extracts_command.py @@ -1,10 +1,10 @@ import tableauserverclient as TSC -from src.commands.auth.session import Session -from src.commands.constants import Errors -from src.commands.server import Server -from src.execution.localize import _ -from src.execution.logger_config import log +from tabcmd.commands.auth.session import Session +from tabcmd.commands.constants import Errors +from tabcmd.commands.server import Server +from tabcmd.execution.localize import _ +from tabcmd.execution.logger_config import log class ReencryptExtracts(Server): diff --git a/src/commands/extracts/refresh_extracts_command.py b/tabcmd/commands/extracts/refresh_extracts_command.py similarity index 94% rename from src/commands/extracts/refresh_extracts_command.py rename to tabcmd/commands/extracts/refresh_extracts_command.py index 15245255..562554ba 100644 --- a/src/commands/extracts/refresh_extracts_command.py +++ b/tabcmd/commands/extracts/refresh_extracts_command.py @@ -1,12 +1,12 @@ import polling2 # type: ignore import tableauserverclient as TSC -from src.commands.auth.session import Session -from src.commands.constants import Errors -from src.commands.server import Server -from src.execution.global_options import * -from src.execution.localize import _ -from src.execution.logger_config import log +from tabcmd.commands.auth.session import Session +from tabcmd.commands.constants import Errors +from tabcmd.commands.server import Server +from tabcmd.execution.global_options import * +from tabcmd.execution.localize import _ +from tabcmd.execution.logger_config import log class RefreshExtracts(Server): diff --git a/src/commands/group/__init__.py b/tabcmd/commands/group/__init__.py similarity index 100% rename from src/commands/group/__init__.py rename to tabcmd/commands/group/__init__.py diff --git a/src/commands/group/create_group_command.py b/tabcmd/commands/group/create_group_command.py similarity index 84% rename from src/commands/group/create_group_command.py rename to tabcmd/commands/group/create_group_command.py index 9220b5b5..489bf4db 100644 --- a/src/commands/group/create_group_command.py +++ b/tabcmd/commands/group/create_group_command.py @@ -1,10 +1,10 @@ import tableauserverclient as TSC -from src.commands.auth.session import Session -from src.commands.constants import Errors -from src.commands.server import Server -from src.execution.localize import _ -from src.execution.logger_config import log +from tabcmd.commands.auth.session import Session +from tabcmd.commands.constants import Errors +from tabcmd.commands.server import Server +from tabcmd.execution.localize import _ +from tabcmd.execution.logger_config import log class CreateGroupCommand(Server): diff --git a/src/commands/group/delete_group_command.py b/tabcmd/commands/group/delete_group_command.py similarity index 83% rename from src/commands/group/delete_group_command.py rename to tabcmd/commands/group/delete_group_command.py index ef76584b..01a8a4b5 100644 --- a/src/commands/group/delete_group_command.py +++ b/tabcmd/commands/group/delete_group_command.py @@ -1,10 +1,10 @@ import tableauserverclient as TSC -from src.commands.auth.session import Session -from src.commands.constants import Errors -from src.commands.server import Server -from src.execution.localize import _ -from src.execution.logger_config import log +from tabcmd.commands.auth.session import Session +from tabcmd.commands.constants import Errors +from tabcmd.commands.server import Server +from tabcmd.execution.localize import _ +from tabcmd.execution.logger_config import log class DeleteGroupCommand(Server): diff --git a/src/commands/help/__init__.py b/tabcmd/commands/help/__init__.py similarity index 100% rename from src/commands/help/__init__.py rename to tabcmd/commands/help/__init__.py diff --git a/src/commands/help/help_command.py b/tabcmd/commands/help/help_command.py similarity index 94% rename from src/commands/help/help_command.py rename to tabcmd/commands/help/help_command.py index 2436cd21..6c5409d8 100644 --- a/src/commands/help/help_command.py +++ b/tabcmd/commands/help/help_command.py @@ -1,8 +1,8 @@ import argparse from typing import Any, List -from src.execution.localize import _ -from src.execution.logger_config import log +from tabcmd.execution.localize import _ +from tabcmd.execution.logger_config import log class HelpCommand: @@ -25,7 +25,7 @@ def run_command(args: argparse.Namespace): logger.debug(_("tabcmd.launching")) # delayed import, TODO fix cyclic imports - from src.execution.map_of_commands import CommandsMap + from tabcmd.execution.map_of_commands import CommandsMap all_commands: List[Any] = CommandsMap.commands_hash_map diff --git a/src/commands/project/__init__.py b/tabcmd/commands/project/__init__.py similarity index 100% rename from src/commands/project/__init__.py rename to tabcmd/commands/project/__init__.py diff --git a/src/commands/project/create_project_command.py b/tabcmd/commands/project/create_project_command.py similarity index 89% rename from src/commands/project/create_project_command.py rename to tabcmd/commands/project/create_project_command.py index 09697526..b1838b48 100644 --- a/src/commands/project/create_project_command.py +++ b/tabcmd/commands/project/create_project_command.py @@ -1,11 +1,11 @@ import tableauserverclient as TSC -from src.commands.auth.session import Session -from src.commands.constants import Errors -from src.commands.server import Server -from src.execution.global_options import * -from src.execution.localize import _ -from src.execution.logger_config import log +from tabcmd.commands.auth.session import Session +from tabcmd.commands.constants import Errors +from tabcmd.commands.server import Server +from tabcmd.execution.global_options import * +from tabcmd.execution.localize import _ +from tabcmd.execution.logger_config import log class CreateProjectCommand(Server): diff --git a/src/commands/project/delete_project_command.py b/tabcmd/commands/project/delete_project_command.py similarity index 85% rename from src/commands/project/delete_project_command.py rename to tabcmd/commands/project/delete_project_command.py index 7e05ae29..9ea30d8d 100644 --- a/src/commands/project/delete_project_command.py +++ b/tabcmd/commands/project/delete_project_command.py @@ -1,11 +1,11 @@ import tableauserverclient as TSC -from src.commands.auth.session import Session -from src.commands.constants import Errors -from src.commands.server import Server -from src.execution.global_options import * -from src.execution.localize import _ -from src.execution.logger_config import log +from tabcmd.commands.auth.session import Session +from tabcmd.commands.constants import Errors +from tabcmd.commands.server import Server +from tabcmd.execution.global_options import * +from tabcmd.execution.localize import _ +from tabcmd.execution.logger_config import log class DeleteProjectCommand(Server): diff --git a/src/commands/project/publish_samples_command.py b/tabcmd/commands/project/publish_samples_command.py similarity index 85% rename from src/commands/project/publish_samples_command.py rename to tabcmd/commands/project/publish_samples_command.py index ebf9cf35..907f8a5c 100644 --- a/src/commands/project/publish_samples_command.py +++ b/tabcmd/commands/project/publish_samples_command.py @@ -1,9 +1,9 @@ -from src.commands.auth.session import Session -from src.commands.constants import Errors -from src.commands.server import Server -from src.execution.global_options import * -from src.execution.localize import _ -from src.execution.logger_config import log +from tabcmd.commands.auth.session import Session +from tabcmd.commands.constants import Errors +from tabcmd.commands.server import Server +from tabcmd.execution.global_options import * +from tabcmd.execution.localize import _ +from tabcmd.execution.logger_config import log class PublishSamplesCommand(Server): diff --git a/src/commands/server.py b/tabcmd/commands/server.py similarity index 98% rename from src/commands/server.py rename to tabcmd/commands/server.py index 3bec44ec..38aac8ef 100644 --- a/src/commands/server.py +++ b/tabcmd/commands/server.py @@ -1,8 +1,8 @@ import os import tableauserverclient as TSC -from src.commands.constants import Errors -from src.execution.localize import _ +from tabcmd.commands.constants import Errors +from tabcmd.execution.localize import _ class Server: diff --git a/src/commands/site/__init__.py b/tabcmd/commands/site/__init__.py similarity index 100% rename from src/commands/site/__init__.py rename to tabcmd/commands/site/__init__.py diff --git a/src/commands/site/create_site_command.py b/tabcmd/commands/site/create_site_command.py similarity index 87% rename from src/commands/site/create_site_command.py rename to tabcmd/commands/site/create_site_command.py index ef44b56d..e8ca8c03 100644 --- a/src/commands/site/create_site_command.py +++ b/tabcmd/commands/site/create_site_command.py @@ -1,11 +1,11 @@ import tableauserverclient as TSC -from src.commands.auth.session import Session -from src.commands.constants import Errors -from src.commands.server import Server -from src.execution.global_options import * -from src.execution.localize import _ -from src.execution.logger_config import log +from tabcmd.commands.auth.session import Session +from tabcmd.commands.constants import Errors +from tabcmd.commands.server import Server +from tabcmd.execution.global_options import * +from tabcmd.execution.localize import _ +from tabcmd.execution.logger_config import log class CreateSiteCommand(Server): diff --git a/src/commands/site/delete_site_command.py b/tabcmd/commands/site/delete_site_command.py similarity index 80% rename from src/commands/site/delete_site_command.py rename to tabcmd/commands/site/delete_site_command.py index 2f9ea640..2d210c86 100644 --- a/src/commands/site/delete_site_command.py +++ b/tabcmd/commands/site/delete_site_command.py @@ -1,10 +1,10 @@ import tableauserverclient as TSC -from src.commands.auth.session import Session -from src.commands.constants import Errors -from src.commands.server import Server -from src.execution.localize import _ -from src.execution.logger_config import log +from tabcmd.commands.auth.session import Session +from tabcmd.commands.constants import Errors +from tabcmd.commands.server import Server +from tabcmd.execution.localize import _ +from tabcmd.execution.logger_config import log class DeleteSiteCommand(Server): diff --git a/src/commands/site/edit_site_command.py b/tabcmd/commands/site/edit_site_command.py similarity index 86% rename from src/commands/site/edit_site_command.py rename to tabcmd/commands/site/edit_site_command.py index a055a45f..cbd58f31 100644 --- a/src/commands/site/edit_site_command.py +++ b/tabcmd/commands/site/edit_site_command.py @@ -1,11 +1,11 @@ import tableauserverclient as TSC -from src.commands.auth.session import Session -from src.commands.constants import Errors -from src.commands.server import Server -from src.execution.global_options import * -from src.execution.localize import _ -from src.execution.logger_config import log +from tabcmd.commands.auth.session import Session +from tabcmd.commands.constants import Errors +from tabcmd.commands.server import Server +from tabcmd.execution.global_options import * +from tabcmd.execution.localize import _ +from tabcmd.execution.logger_config import log class EditSiteCommand(Server): diff --git a/src/commands/site/list_sites_command.py b/tabcmd/commands/site/list_sites_command.py similarity index 80% rename from src/commands/site/list_sites_command.py rename to tabcmd/commands/site/list_sites_command.py index 6709c488..418f6caf 100644 --- a/src/commands/site/list_sites_command.py +++ b/tabcmd/commands/site/list_sites_command.py @@ -1,11 +1,11 @@ import tableauserverclient as TSC -from src.commands.auth.session import Session -from src.commands.constants import Errors -from src.commands.server import Server -from src.execution.global_options import * -from src.execution.localize import _ -from src.execution.logger_config import log +from tabcmd.commands.auth.session import Session +from tabcmd.commands.constants import Errors +from tabcmd.commands.server import Server +from tabcmd.execution.global_options import * +from tabcmd.execution.localize import _ +from tabcmd.execution.logger_config import log class ListSiteCommand(Server): diff --git a/src/commands/user/__init__.py b/tabcmd/commands/user/__init__.py similarity index 100% rename from src/commands/user/__init__.py rename to tabcmd/commands/user/__init__.py diff --git a/src/commands/user/add_users_command.py b/tabcmd/commands/user/add_users_command.py similarity index 82% rename from src/commands/user/add_users_command.py rename to tabcmd/commands/user/add_users_command.py index ac2431d7..2d580ac3 100644 --- a/src/commands/user/add_users_command.py +++ b/tabcmd/commands/user/add_users_command.py @@ -1,7 +1,7 @@ -from src.commands.auth.session import Session -from src.execution.global_options import * -from src.execution.localize import _ -from src.execution.logger_config import log +from tabcmd.commands.auth.session import Session +from tabcmd.execution.global_options import * +from tabcmd.execution.localize import _ +from tabcmd.execution.logger_config import log from .user_data import UserCommand diff --git a/src/commands/user/create_site_users.py b/tabcmd/commands/user/create_site_users.py similarity index 92% rename from src/commands/user/create_site_users.py rename to tabcmd/commands/user/create_site_users.py index b068da51..37b7eaaa 100644 --- a/src/commands/user/create_site_users.py +++ b/tabcmd/commands/user/create_site_users.py @@ -1,9 +1,9 @@ import tableauserverclient as TSC -from src.commands.auth.session import Session -from src.execution.global_options import * -from src.execution.localize import _ -from src.execution.logger_config import log +from tabcmd.commands.auth.session import Session +from tabcmd.execution.global_options import * +from tabcmd.execution.localize import _ +from tabcmd.execution.logger_config import log from .user_data import UserCommand diff --git a/src/commands/user/create_users_command.py b/tabcmd/commands/user/create_users_command.py similarity index 91% rename from src/commands/user/create_users_command.py rename to tabcmd/commands/user/create_users_command.py index 8de37544..2a44854f 100644 --- a/src/commands/user/create_users_command.py +++ b/tabcmd/commands/user/create_users_command.py @@ -1,10 +1,10 @@ import tableauserverclient as TSC -from src.commands.auth.session import Session -from src.commands.constants import Errors -from src.execution.global_options import * -from src.execution.localize import _ -from src.execution.logger_config import log +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 from .user_data import UserCommand diff --git a/src/commands/user/delete_site_users_command.py b/tabcmd/commands/user/delete_site_users_command.py similarity index 86% rename from src/commands/user/delete_site_users_command.py rename to tabcmd/commands/user/delete_site_users_command.py index 4ef94bad..7f46a311 100644 --- a/src/commands/user/delete_site_users_command.py +++ b/tabcmd/commands/user/delete_site_users_command.py @@ -1,10 +1,10 @@ -from src.commands.auth.session import Session -from src.commands.constants import Errors -from src.commands.server import Server -from src.commands.user.user_data import UserCommand -from src.execution.global_options import * -from src.execution.localize import _ -from src.execution.logger_config import log +from tabcmd.commands.auth.session import Session +from tabcmd.commands.constants import Errors +from tabcmd.commands.server import Server +from tabcmd.commands.user.user_data import UserCommand +from tabcmd.execution.global_options import * +from tabcmd.execution.localize import _ +from tabcmd.execution.logger_config import log class DeleteSiteUsersCommand(Server): diff --git a/src/commands/user/remove_users_command.py b/tabcmd/commands/user/remove_users_command.py similarity index 83% rename from src/commands/user/remove_users_command.py rename to tabcmd/commands/user/remove_users_command.py index b4f102c2..bedbd394 100644 --- a/src/commands/user/remove_users_command.py +++ b/tabcmd/commands/user/remove_users_command.py @@ -1,7 +1,7 @@ -from src.commands.auth.session import Session -from src.execution.global_options import * -from src.execution.localize import _ -from src.execution.logger_config import log +from tabcmd.commands.auth.session import Session +from tabcmd.execution.global_options import * +from tabcmd.execution.localize import _ +from tabcmd.execution.logger_config import log from .user_data import UserCommand diff --git a/src/commands/user/user_data.py b/tabcmd/commands/user/user_data.py similarity index 98% rename from src/commands/user/user_data.py rename to tabcmd/commands/user/user_data.py index dd5d9c53..ed7082c4 100644 --- a/src/commands/user/user_data.py +++ b/tabcmd/commands/user/user_data.py @@ -6,9 +6,9 @@ import tableauserverclient as TSC -from src.commands.constants import Errors -from src.commands.server import Server -from src.execution.localize import _ +from tabcmd.commands.constants import Errors +from tabcmd.commands.server import Server +from tabcmd.execution.localize import _ class Userdata: diff --git a/src/execution/__init__.py b/tabcmd/execution/__init__.py similarity index 100% rename from src/execution/__init__.py rename to tabcmd/execution/__init__.py diff --git a/src/execution/_version.py b/tabcmd/execution/_version.py similarity index 100% rename from src/execution/_version.py rename to tabcmd/execution/_version.py diff --git a/src/execution/global_options.py b/tabcmd/execution/global_options.py similarity index 100% rename from src/execution/global_options.py rename to tabcmd/execution/global_options.py diff --git a/src/execution/localize.py b/tabcmd/execution/localize.py similarity index 100% rename from src/execution/localize.py rename to tabcmd/execution/localize.py diff --git a/src/execution/logger_config.py b/tabcmd/execution/logger_config.py similarity index 100% rename from src/execution/logger_config.py rename to tabcmd/execution/logger_config.py diff --git a/tabcmd/execution/map_of_commands.py b/tabcmd/execution/map_of_commands.py new file mode 100644 index 00000000..87ff0a1d --- /dev/null +++ b/tabcmd/execution/map_of_commands.py @@ -0,0 +1,61 @@ +from tabcmd.commands.auth.login_command import * +from tabcmd.commands.auth.logout_command import * +from tabcmd.commands.datasources_and_workbooks.delete_command import * +from tabcmd.commands.datasources_and_workbooks.export_command import * +from tabcmd.commands.datasources_and_workbooks.get_url_command import * +from tabcmd.commands.datasources_and_workbooks.publish_command import * +from tabcmd.commands.extracts.create_extracts_command import * +from tabcmd.commands.extracts.decrypt_extracts_command import * +from tabcmd.commands.extracts.delete_extracts_command import * +from tabcmd.commands.extracts.encrypt_extracts_command import * +from tabcmd.commands.extracts.reencrypt_extracts_command import * +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 * +from tabcmd.commands.site.create_site_command import * +from tabcmd.commands.site.delete_site_command import * +from tabcmd.commands.site.edit_site_command import * +from tabcmd.commands.site.list_sites_command import * +from tabcmd.commands.user.add_users_command import * +from tabcmd.commands.user.create_site_users import * +from tabcmd.commands.user.delete_site_users_command import * + +# from tabcmd.commands.user.create_users import * +from tabcmd.commands.user.remove_users_command import * + + +class CommandsMap: + commands_hash_map = [ + # not yet implemented "createusers": ("createusers", CreateUserCommand, "Create users on the server"), + # run schedule + AddUserCommand, + CreateExtracts, + CreateGroupCommand, + CreateProjectCommand, + CreateSiteCommand, + CreateSiteUsersCommand, + DecryptExtracts, + DeleteCommand, + DeleteExtracts, + DeleteGroupCommand, + DeleteProjectCommand, + DeleteSiteCommand, + DeleteSiteUsersCommand, + EditSiteCommand, + EncryptExtracts, + ExportCommand, + GetUrl, + HelpCommand, + ListSiteCommand, + LoginCommand, + LogoutCommand, + PublishCommand, + PublishSamplesCommand, + ReencryptExtracts, + RefreshExtracts, + RemoveUserCommand, + ] diff --git a/src/execution/parent_parser.py b/tabcmd/execution/parent_parser.py similarity index 100% rename from src/execution/parent_parser.py rename to tabcmd/execution/parent_parser.py diff --git a/src/execution/tabcmd_controller.py b/tabcmd/execution/tabcmd_controller.py similarity index 100% rename from src/execution/tabcmd_controller.py rename to tabcmd/execution/tabcmd_controller.py diff --git a/src/locales/de/LC_MESSAGES/.gitkeep b/tabcmd/locales/de/LC_MESSAGES/.gitkeep similarity index 100% rename from src/locales/de/LC_MESSAGES/.gitkeep rename to tabcmd/locales/de/LC_MESSAGES/.gitkeep diff --git a/src/locales/de/LC_MESSAGES/de.po b/tabcmd/locales/de/LC_MESSAGES/de.po similarity index 100% rename from src/locales/de/LC_MESSAGES/de.po rename to tabcmd/locales/de/LC_MESSAGES/de.po diff --git a/src/locales/de/LC_MESSAGES/shared_wg_de.po b/tabcmd/locales/de/LC_MESSAGES/shared_wg_de.po similarity index 100% rename from src/locales/de/LC_MESSAGES/shared_wg_de.po rename to tabcmd/locales/de/LC_MESSAGES/shared_wg_de.po diff --git a/src/locales/de/LC_MESSAGES/tabcmd.mo b/tabcmd/locales/de/LC_MESSAGES/tabcmd.mo similarity index 100% rename from src/locales/de/LC_MESSAGES/tabcmd.mo rename to tabcmd/locales/de/LC_MESSAGES/tabcmd.mo diff --git a/src/locales/de/LC_MESSAGES/tabcmd.po b/tabcmd/locales/de/LC_MESSAGES/tabcmd.po similarity index 100% rename from src/locales/de/LC_MESSAGES/tabcmd.po rename to tabcmd/locales/de/LC_MESSAGES/tabcmd.po diff --git a/src/locales/de/LC_MESSAGES/tabcmd_messages_de.po b/tabcmd/locales/de/LC_MESSAGES/tabcmd_messages_de.po similarity index 100% rename from src/locales/de/LC_MESSAGES/tabcmd_messages_de.po rename to tabcmd/locales/de/LC_MESSAGES/tabcmd_messages_de.po diff --git a/src/locales/de/shared_wg_de.properties b/tabcmd/locales/de/shared_wg_de.properties similarity index 100% rename from src/locales/de/shared_wg_de.properties rename to tabcmd/locales/de/shared_wg_de.properties diff --git a/src/locales/de/tabcmd_messages_de.properties b/tabcmd/locales/de/tabcmd_messages_de.properties similarity index 100% rename from src/locales/de/tabcmd_messages_de.properties rename to tabcmd/locales/de/tabcmd_messages_de.properties diff --git a/src/locales/en/LC_MESSAGES/en-US.po b/tabcmd/locales/en/LC_MESSAGES/en-US.po similarity index 100% rename from src/locales/en/LC_MESSAGES/en-US.po rename to tabcmd/locales/en/LC_MESSAGES/en-US.po diff --git a/src/locales/en/LC_MESSAGES/shared_wg_en.po b/tabcmd/locales/en/LC_MESSAGES/shared_wg_en.po similarity index 100% rename from src/locales/en/LC_MESSAGES/shared_wg_en.po rename to tabcmd/locales/en/LC_MESSAGES/shared_wg_en.po diff --git a/src/locales/en/LC_MESSAGES/tabcmd.mo b/tabcmd/locales/en/LC_MESSAGES/tabcmd.mo similarity index 100% rename from src/locales/en/LC_MESSAGES/tabcmd.mo rename to tabcmd/locales/en/LC_MESSAGES/tabcmd.mo diff --git a/src/locales/en/LC_MESSAGES/tabcmd.po b/tabcmd/locales/en/LC_MESSAGES/tabcmd.po similarity index 100% rename from src/locales/en/LC_MESSAGES/tabcmd.po rename to tabcmd/locales/en/LC_MESSAGES/tabcmd.po diff --git a/src/locales/en/LC_MESSAGES/tabcmd_messages_en.po b/tabcmd/locales/en/LC_MESSAGES/tabcmd_messages_en.po similarity index 100% rename from src/locales/en/LC_MESSAGES/tabcmd_messages_en.po rename to tabcmd/locales/en/LC_MESSAGES/tabcmd_messages_en.po diff --git a/src/locales/en/shared_wg_en.properties b/tabcmd/locales/en/shared_wg_en.properties similarity index 100% rename from src/locales/en/shared_wg_en.properties rename to tabcmd/locales/en/shared_wg_en.properties diff --git a/src/locales/en/tabcmd_messages_en.properties b/tabcmd/locales/en/tabcmd_messages_en.properties similarity index 100% rename from src/locales/en/tabcmd_messages_en.properties rename to tabcmd/locales/en/tabcmd_messages_en.properties diff --git a/src/locales/es/LC_MESSAGES/.gitkeep b/tabcmd/locales/es/LC_MESSAGES/.gitkeep similarity index 100% rename from src/locales/es/LC_MESSAGES/.gitkeep rename to tabcmd/locales/es/LC_MESSAGES/.gitkeep diff --git a/src/locales/es/LC_MESSAGES/es.po b/tabcmd/locales/es/LC_MESSAGES/es.po similarity index 100% rename from src/locales/es/LC_MESSAGES/es.po rename to tabcmd/locales/es/LC_MESSAGES/es.po diff --git a/src/locales/es/LC_MESSAGES/shared_wg_es.po b/tabcmd/locales/es/LC_MESSAGES/shared_wg_es.po similarity index 100% rename from src/locales/es/LC_MESSAGES/shared_wg_es.po rename to tabcmd/locales/es/LC_MESSAGES/shared_wg_es.po diff --git a/src/locales/es/LC_MESSAGES/tabcmd.mo b/tabcmd/locales/es/LC_MESSAGES/tabcmd.mo similarity index 100% rename from src/locales/es/LC_MESSAGES/tabcmd.mo rename to tabcmd/locales/es/LC_MESSAGES/tabcmd.mo diff --git a/src/locales/es/LC_MESSAGES/tabcmd.po b/tabcmd/locales/es/LC_MESSAGES/tabcmd.po similarity index 100% rename from src/locales/es/LC_MESSAGES/tabcmd.po rename to tabcmd/locales/es/LC_MESSAGES/tabcmd.po diff --git a/src/locales/es/LC_MESSAGES/tabcmd_messages_es.po b/tabcmd/locales/es/LC_MESSAGES/tabcmd_messages_es.po similarity index 100% rename from src/locales/es/LC_MESSAGES/tabcmd_messages_es.po rename to tabcmd/locales/es/LC_MESSAGES/tabcmd_messages_es.po diff --git a/src/locales/es/shared_wg_es.properties b/tabcmd/locales/es/shared_wg_es.properties similarity index 100% rename from src/locales/es/shared_wg_es.properties rename to tabcmd/locales/es/shared_wg_es.properties diff --git a/src/locales/es/tabcmd_messages_es.properties b/tabcmd/locales/es/tabcmd_messages_es.properties similarity index 100% rename from src/locales/es/tabcmd_messages_es.properties rename to tabcmd/locales/es/tabcmd_messages_es.properties diff --git a/src/locales/fr/LC_MESSAGES/.gitkeep b/tabcmd/locales/fr/LC_MESSAGES/.gitkeep similarity index 100% rename from src/locales/fr/LC_MESSAGES/.gitkeep rename to tabcmd/locales/fr/LC_MESSAGES/.gitkeep diff --git a/src/locales/fr/LC_MESSAGES/fr.po b/tabcmd/locales/fr/LC_MESSAGES/fr.po similarity index 100% rename from src/locales/fr/LC_MESSAGES/fr.po rename to tabcmd/locales/fr/LC_MESSAGES/fr.po diff --git a/src/locales/fr/LC_MESSAGES/shared_wg_fr.po b/tabcmd/locales/fr/LC_MESSAGES/shared_wg_fr.po similarity index 100% rename from src/locales/fr/LC_MESSAGES/shared_wg_fr.po rename to tabcmd/locales/fr/LC_MESSAGES/shared_wg_fr.po diff --git a/src/locales/fr/LC_MESSAGES/tabcmd.mo b/tabcmd/locales/fr/LC_MESSAGES/tabcmd.mo similarity index 100% rename from src/locales/fr/LC_MESSAGES/tabcmd.mo rename to tabcmd/locales/fr/LC_MESSAGES/tabcmd.mo diff --git a/src/locales/fr/LC_MESSAGES/tabcmd.po b/tabcmd/locales/fr/LC_MESSAGES/tabcmd.po similarity index 100% rename from src/locales/fr/LC_MESSAGES/tabcmd.po rename to tabcmd/locales/fr/LC_MESSAGES/tabcmd.po diff --git a/src/locales/fr/LC_MESSAGES/tabcmd_messages_fr.po b/tabcmd/locales/fr/LC_MESSAGES/tabcmd_messages_fr.po similarity index 100% rename from src/locales/fr/LC_MESSAGES/tabcmd_messages_fr.po rename to tabcmd/locales/fr/LC_MESSAGES/tabcmd_messages_fr.po diff --git a/src/locales/fr/shared_wg_fr.properties b/tabcmd/locales/fr/shared_wg_fr.properties similarity index 100% rename from src/locales/fr/shared_wg_fr.properties rename to tabcmd/locales/fr/shared_wg_fr.properties diff --git a/src/locales/fr/tabcmd_messages_fr.properties b/tabcmd/locales/fr/tabcmd_messages_fr.properties similarity index 100% rename from src/locales/fr/tabcmd_messages_fr.properties rename to tabcmd/locales/fr/tabcmd_messages_fr.properties diff --git a/src/locales/ga/LC_MESSAGES/.gitkeep b/tabcmd/locales/ga/LC_MESSAGES/.gitkeep similarity index 100% rename from src/locales/ga/LC_MESSAGES/.gitkeep rename to tabcmd/locales/ga/LC_MESSAGES/.gitkeep diff --git a/src/locales/ga/LC_MESSAGES/ga.po b/tabcmd/locales/ga/LC_MESSAGES/ga.po similarity index 100% rename from src/locales/ga/LC_MESSAGES/ga.po rename to tabcmd/locales/ga/LC_MESSAGES/ga.po diff --git a/src/locales/ga/LC_MESSAGES/shared_wg_ga.po b/tabcmd/locales/ga/LC_MESSAGES/shared_wg_ga.po similarity index 100% rename from src/locales/ga/LC_MESSAGES/shared_wg_ga.po rename to tabcmd/locales/ga/LC_MESSAGES/shared_wg_ga.po diff --git a/src/locales/ga/LC_MESSAGES/tabcmd.mo b/tabcmd/locales/ga/LC_MESSAGES/tabcmd.mo similarity index 100% rename from src/locales/ga/LC_MESSAGES/tabcmd.mo rename to tabcmd/locales/ga/LC_MESSAGES/tabcmd.mo diff --git a/src/locales/ga/LC_MESSAGES/tabcmd.po b/tabcmd/locales/ga/LC_MESSAGES/tabcmd.po similarity index 100% rename from src/locales/ga/LC_MESSAGES/tabcmd.po rename to tabcmd/locales/ga/LC_MESSAGES/tabcmd.po diff --git a/src/locales/ga/LC_MESSAGES/tabcmd_messages_ga.po b/tabcmd/locales/ga/LC_MESSAGES/tabcmd_messages_ga.po similarity index 100% rename from src/locales/ga/LC_MESSAGES/tabcmd_messages_ga.po rename to tabcmd/locales/ga/LC_MESSAGES/tabcmd_messages_ga.po diff --git a/src/locales/ga/shared_wg_ga.properties b/tabcmd/locales/ga/shared_wg_ga.properties similarity index 100% rename from src/locales/ga/shared_wg_ga.properties rename to tabcmd/locales/ga/shared_wg_ga.properties diff --git a/src/locales/ga/tabcmd_messages_ga.properties b/tabcmd/locales/ga/tabcmd_messages_ga.properties similarity index 100% rename from src/locales/ga/tabcmd_messages_ga.properties rename to tabcmd/locales/ga/tabcmd_messages_ga.properties diff --git a/src/locales/it/LC_MESSAGES/.gitkeep b/tabcmd/locales/it/LC_MESSAGES/.gitkeep similarity index 100% rename from src/locales/it/LC_MESSAGES/.gitkeep rename to tabcmd/locales/it/LC_MESSAGES/.gitkeep diff --git a/src/locales/it/LC_MESSAGES/it.po b/tabcmd/locales/it/LC_MESSAGES/it.po similarity index 100% rename from src/locales/it/LC_MESSAGES/it.po rename to tabcmd/locales/it/LC_MESSAGES/it.po diff --git a/src/locales/it/LC_MESSAGES/shared_wg_it.po b/tabcmd/locales/it/LC_MESSAGES/shared_wg_it.po similarity index 100% rename from src/locales/it/LC_MESSAGES/shared_wg_it.po rename to tabcmd/locales/it/LC_MESSAGES/shared_wg_it.po diff --git a/src/locales/it/LC_MESSAGES/tabcmd.mo b/tabcmd/locales/it/LC_MESSAGES/tabcmd.mo similarity index 100% rename from src/locales/it/LC_MESSAGES/tabcmd.mo rename to tabcmd/locales/it/LC_MESSAGES/tabcmd.mo diff --git a/src/locales/it/LC_MESSAGES/tabcmd.po b/tabcmd/locales/it/LC_MESSAGES/tabcmd.po similarity index 100% rename from src/locales/it/LC_MESSAGES/tabcmd.po rename to tabcmd/locales/it/LC_MESSAGES/tabcmd.po diff --git a/src/locales/it/LC_MESSAGES/tabcmd_messages_it.po b/tabcmd/locales/it/LC_MESSAGES/tabcmd_messages_it.po similarity index 100% rename from src/locales/it/LC_MESSAGES/tabcmd_messages_it.po rename to tabcmd/locales/it/LC_MESSAGES/tabcmd_messages_it.po diff --git a/src/locales/it/shared_wg_it.properties b/tabcmd/locales/it/shared_wg_it.properties similarity index 100% rename from src/locales/it/shared_wg_it.properties rename to tabcmd/locales/it/shared_wg_it.properties diff --git a/src/locales/it/tabcmd_messages_it.properties b/tabcmd/locales/it/tabcmd_messages_it.properties similarity index 100% rename from src/locales/it/tabcmd_messages_it.properties rename to tabcmd/locales/it/tabcmd_messages_it.properties diff --git a/src/locales/ja/LC_MESSAGES/.gitkeep b/tabcmd/locales/ja/LC_MESSAGES/.gitkeep similarity index 100% rename from src/locales/ja/LC_MESSAGES/.gitkeep rename to tabcmd/locales/ja/LC_MESSAGES/.gitkeep diff --git a/src/locales/ja/LC_MESSAGES/ja.po b/tabcmd/locales/ja/LC_MESSAGES/ja.po similarity index 100% rename from src/locales/ja/LC_MESSAGES/ja.po rename to tabcmd/locales/ja/LC_MESSAGES/ja.po diff --git a/src/locales/ja/LC_MESSAGES/shared_wg_ja.po b/tabcmd/locales/ja/LC_MESSAGES/shared_wg_ja.po similarity index 100% rename from src/locales/ja/LC_MESSAGES/shared_wg_ja.po rename to tabcmd/locales/ja/LC_MESSAGES/shared_wg_ja.po diff --git a/src/locales/ja/LC_MESSAGES/tabcmd.mo b/tabcmd/locales/ja/LC_MESSAGES/tabcmd.mo similarity index 100% rename from src/locales/ja/LC_MESSAGES/tabcmd.mo rename to tabcmd/locales/ja/LC_MESSAGES/tabcmd.mo diff --git a/src/locales/ja/LC_MESSAGES/tabcmd.po b/tabcmd/locales/ja/LC_MESSAGES/tabcmd.po similarity index 100% rename from src/locales/ja/LC_MESSAGES/tabcmd.po rename to tabcmd/locales/ja/LC_MESSAGES/tabcmd.po diff --git a/src/locales/ja/LC_MESSAGES/tabcmd_messages_ja.po b/tabcmd/locales/ja/LC_MESSAGES/tabcmd_messages_ja.po similarity index 100% rename from src/locales/ja/LC_MESSAGES/tabcmd_messages_ja.po rename to tabcmd/locales/ja/LC_MESSAGES/tabcmd_messages_ja.po diff --git a/src/locales/ja/shared_wg_ja.properties b/tabcmd/locales/ja/shared_wg_ja.properties similarity index 100% rename from src/locales/ja/shared_wg_ja.properties rename to tabcmd/locales/ja/shared_wg_ja.properties diff --git a/src/locales/ja/tabcmd_messages_ja.properties b/tabcmd/locales/ja/tabcmd_messages_ja.properties similarity index 100% rename from src/locales/ja/tabcmd_messages_ja.properties rename to tabcmd/locales/ja/tabcmd_messages_ja.properties diff --git a/src/locales/ko/LC_MESSAGES/.gitkeep b/tabcmd/locales/ko/LC_MESSAGES/.gitkeep similarity index 100% rename from src/locales/ko/LC_MESSAGES/.gitkeep rename to tabcmd/locales/ko/LC_MESSAGES/.gitkeep diff --git a/src/locales/ko/LC_MESSAGES/ko.po b/tabcmd/locales/ko/LC_MESSAGES/ko.po similarity index 100% rename from src/locales/ko/LC_MESSAGES/ko.po rename to tabcmd/locales/ko/LC_MESSAGES/ko.po diff --git a/src/locales/ko/LC_MESSAGES/shared_wg_ko.po b/tabcmd/locales/ko/LC_MESSAGES/shared_wg_ko.po similarity index 100% rename from src/locales/ko/LC_MESSAGES/shared_wg_ko.po rename to tabcmd/locales/ko/LC_MESSAGES/shared_wg_ko.po diff --git a/src/locales/ko/LC_MESSAGES/tabcmd.mo b/tabcmd/locales/ko/LC_MESSAGES/tabcmd.mo similarity index 100% rename from src/locales/ko/LC_MESSAGES/tabcmd.mo rename to tabcmd/locales/ko/LC_MESSAGES/tabcmd.mo diff --git a/src/locales/ko/LC_MESSAGES/tabcmd.po b/tabcmd/locales/ko/LC_MESSAGES/tabcmd.po similarity index 100% rename from src/locales/ko/LC_MESSAGES/tabcmd.po rename to tabcmd/locales/ko/LC_MESSAGES/tabcmd.po diff --git a/src/locales/ko/LC_MESSAGES/tabcmd_messages_ko.po b/tabcmd/locales/ko/LC_MESSAGES/tabcmd_messages_ko.po similarity index 100% rename from src/locales/ko/LC_MESSAGES/tabcmd_messages_ko.po rename to tabcmd/locales/ko/LC_MESSAGES/tabcmd_messages_ko.po diff --git a/src/locales/ko/shared_wg_ko.properties b/tabcmd/locales/ko/shared_wg_ko.properties similarity index 100% rename from src/locales/ko/shared_wg_ko.properties rename to tabcmd/locales/ko/shared_wg_ko.properties diff --git a/src/locales/ko/tabcmd_messages_ko.properties b/tabcmd/locales/ko/tabcmd_messages_ko.properties similarity index 100% rename from src/locales/ko/tabcmd_messages_ko.properties rename to tabcmd/locales/ko/tabcmd_messages_ko.properties diff --git a/src/locales/pt/LC_MESSAGES/.gitkeep b/tabcmd/locales/pt/LC_MESSAGES/.gitkeep similarity index 100% rename from src/locales/pt/LC_MESSAGES/.gitkeep rename to tabcmd/locales/pt/LC_MESSAGES/.gitkeep diff --git a/src/locales/pt/LC_MESSAGES/pt.po b/tabcmd/locales/pt/LC_MESSAGES/pt.po similarity index 100% rename from src/locales/pt/LC_MESSAGES/pt.po rename to tabcmd/locales/pt/LC_MESSAGES/pt.po diff --git a/src/locales/pt/LC_MESSAGES/shared_wg_pt.po b/tabcmd/locales/pt/LC_MESSAGES/shared_wg_pt.po similarity index 100% rename from src/locales/pt/LC_MESSAGES/shared_wg_pt.po rename to tabcmd/locales/pt/LC_MESSAGES/shared_wg_pt.po diff --git a/src/locales/pt/LC_MESSAGES/tabcmd.mo b/tabcmd/locales/pt/LC_MESSAGES/tabcmd.mo similarity index 100% rename from src/locales/pt/LC_MESSAGES/tabcmd.mo rename to tabcmd/locales/pt/LC_MESSAGES/tabcmd.mo diff --git a/src/locales/pt/LC_MESSAGES/tabcmd.po b/tabcmd/locales/pt/LC_MESSAGES/tabcmd.po similarity index 100% rename from src/locales/pt/LC_MESSAGES/tabcmd.po rename to tabcmd/locales/pt/LC_MESSAGES/tabcmd.po diff --git a/src/locales/pt/LC_MESSAGES/tabcmd_messages_pt.po b/tabcmd/locales/pt/LC_MESSAGES/tabcmd_messages_pt.po similarity index 100% rename from src/locales/pt/LC_MESSAGES/tabcmd_messages_pt.po rename to tabcmd/locales/pt/LC_MESSAGES/tabcmd_messages_pt.po diff --git a/src/locales/pt/shared_wg_pt.properties b/tabcmd/locales/pt/shared_wg_pt.properties similarity index 100% rename from src/locales/pt/shared_wg_pt.properties rename to tabcmd/locales/pt/shared_wg_pt.properties diff --git a/src/locales/pt/tabcmd_messages_pt.properties b/tabcmd/locales/pt/tabcmd_messages_pt.properties similarity index 100% rename from src/locales/pt/tabcmd_messages_pt.properties rename to tabcmd/locales/pt/tabcmd_messages_pt.properties diff --git a/src/locales/sv/LC_MESSAGES/.gitkeep b/tabcmd/locales/sv/LC_MESSAGES/.gitkeep similarity index 100% rename from src/locales/sv/LC_MESSAGES/.gitkeep rename to tabcmd/locales/sv/LC_MESSAGES/.gitkeep diff --git a/src/locales/sv/LC_MESSAGES/shared_wg_sv.po b/tabcmd/locales/sv/LC_MESSAGES/shared_wg_sv.po similarity index 100% rename from src/locales/sv/LC_MESSAGES/shared_wg_sv.po rename to tabcmd/locales/sv/LC_MESSAGES/shared_wg_sv.po diff --git a/src/locales/sv/LC_MESSAGES/sv.po b/tabcmd/locales/sv/LC_MESSAGES/sv.po similarity index 100% rename from src/locales/sv/LC_MESSAGES/sv.po rename to tabcmd/locales/sv/LC_MESSAGES/sv.po diff --git a/src/locales/sv/LC_MESSAGES/tabcmd.mo b/tabcmd/locales/sv/LC_MESSAGES/tabcmd.mo similarity index 100% rename from src/locales/sv/LC_MESSAGES/tabcmd.mo rename to tabcmd/locales/sv/LC_MESSAGES/tabcmd.mo diff --git a/src/locales/sv/LC_MESSAGES/tabcmd.po b/tabcmd/locales/sv/LC_MESSAGES/tabcmd.po similarity index 100% rename from src/locales/sv/LC_MESSAGES/tabcmd.po rename to tabcmd/locales/sv/LC_MESSAGES/tabcmd.po diff --git a/src/locales/sv/LC_MESSAGES/tabcmd_messages_sv.po b/tabcmd/locales/sv/LC_MESSAGES/tabcmd_messages_sv.po similarity index 100% rename from src/locales/sv/LC_MESSAGES/tabcmd_messages_sv.po rename to tabcmd/locales/sv/LC_MESSAGES/tabcmd_messages_sv.po diff --git a/src/locales/sv/shared_wg_sv.properties b/tabcmd/locales/sv/shared_wg_sv.properties similarity index 100% rename from src/locales/sv/shared_wg_sv.properties rename to tabcmd/locales/sv/shared_wg_sv.properties diff --git a/src/locales/sv/tabcmd_messages_sv.properties b/tabcmd/locales/sv/tabcmd_messages_sv.properties similarity index 100% rename from src/locales/sv/tabcmd_messages_sv.properties rename to tabcmd/locales/sv/tabcmd_messages_sv.properties diff --git a/src/locales/zh/LC_MESSAGES/.gitkeep b/tabcmd/locales/zh/LC_MESSAGES/.gitkeep similarity index 100% rename from src/locales/zh/LC_MESSAGES/.gitkeep rename to tabcmd/locales/zh/LC_MESSAGES/.gitkeep diff --git a/src/locales/zh/LC_MESSAGES/shared_wg_zh.po b/tabcmd/locales/zh/LC_MESSAGES/shared_wg_zh.po similarity index 100% rename from src/locales/zh/LC_MESSAGES/shared_wg_zh.po rename to tabcmd/locales/zh/LC_MESSAGES/shared_wg_zh.po diff --git a/src/locales/zh/LC_MESSAGES/tabcmd.mo b/tabcmd/locales/zh/LC_MESSAGES/tabcmd.mo similarity index 100% rename from src/locales/zh/LC_MESSAGES/tabcmd.mo rename to tabcmd/locales/zh/LC_MESSAGES/tabcmd.mo diff --git a/src/locales/zh/LC_MESSAGES/tabcmd.po b/tabcmd/locales/zh/LC_MESSAGES/tabcmd.po similarity index 100% rename from src/locales/zh/LC_MESSAGES/tabcmd.po rename to tabcmd/locales/zh/LC_MESSAGES/tabcmd.po diff --git a/src/locales/zh/LC_MESSAGES/tabcmd_messages_zh.po b/tabcmd/locales/zh/LC_MESSAGES/tabcmd_messages_zh.po similarity index 100% rename from src/locales/zh/LC_MESSAGES/tabcmd_messages_zh.po rename to tabcmd/locales/zh/LC_MESSAGES/tabcmd_messages_zh.po diff --git a/src/locales/zh/LC_MESSAGES/zh.po b/tabcmd/locales/zh/LC_MESSAGES/zh.po similarity index 100% rename from src/locales/zh/LC_MESSAGES/zh.po rename to tabcmd/locales/zh/LC_MESSAGES/zh.po diff --git a/src/locales/zh/shared_wg_zh.properties b/tabcmd/locales/zh/shared_wg_zh.properties similarity index 100% rename from src/locales/zh/shared_wg_zh.properties rename to tabcmd/locales/zh/shared_wg_zh.properties diff --git a/src/locales/zh/tabcmd_messages_zh.properties b/tabcmd/locales/zh/tabcmd_messages_zh.properties similarity index 100% rename from src/locales/zh/tabcmd_messages_zh.properties rename to tabcmd/locales/zh/tabcmd_messages_zh.properties diff --git a/src/tabcmd.py b/tabcmd/tabcmd.py similarity index 90% rename from src/tabcmd.py rename to tabcmd/tabcmd.py index 72e53d40..75951897 100644 --- a/src/tabcmd.py +++ b/tabcmd/tabcmd.py @@ -1,6 +1,6 @@ import sys -from src.execution.tabcmd_controller import TabcmdController +from tabcmd.execution.tabcmd_controller import TabcmdController def main(): diff --git a/tests/commands/test_execution.py b/tests/commands/test_execution.py index 7ed589f2..f737829c 100644 --- a/tests/commands/test_execution.py +++ b/tests/commands/test_execution.py @@ -2,8 +2,8 @@ import sys import unittest import mock -from src.execution.logger_config import * -from src.execution.tabcmd_controller import TabcmdController +from tabcmd.execution.logger_config import * +from tabcmd.execution.tabcmd_controller import TabcmdController class ExecutionTests(unittest.TestCase): diff --git a/tests/commands/test_geturl_utils.py b/tests/commands/test_geturl_utils.py index 3f006aa7..2430bf28 100644 --- a/tests/commands/test_geturl_utils.py +++ b/tests/commands/test_geturl_utils.py @@ -1,8 +1,8 @@ import unittest from unittest import mock -from src.commands.datasources_and_workbooks.get_url_command import * -from src.commands.datasources_and_workbooks.export_command import * -from src.commands.server import Server +from tabcmd.commands.datasources_and_workbooks.get_url_command import * +from tabcmd.commands.datasources_and_workbooks.export_command import * +from tabcmd.commands.server import Server mock_logger = mock.MagicMock() diff --git a/tests/commands/test_localize.py b/tests/commands/test_localize.py index 2e316ed3..90bb4851 100644 --- a/tests/commands/test_localize.py +++ b/tests/commands/test_localize.py @@ -1,6 +1,6 @@ import gettext import unittest -from src.execution.localize import set_client_locale +from tabcmd.execution.localize import set_client_locale class LocaleTests(unittest.TestCase): diff --git a/tests/commands/test_projects_utils.py b/tests/commands/test_projects_utils.py index 3a36ff3d..61799bd7 100644 --- a/tests/commands/test_projects_utils.py +++ b/tests/commands/test_projects_utils.py @@ -1,8 +1,8 @@ import unittest from unittest import mock -from src.commands.server import Server -from src.execution.logger_config import log +from tabcmd.commands.server import Server +from tabcmd.execution.logger_config import log fake_item = mock.MagicMock() fake_item.name = "fake-name" diff --git a/tests/commands/test_run_commands.py b/tests/commands/test_run_commands.py index f654598b..4ab980c3 100644 --- a/tests/commands/test_run_commands.py +++ b/tests/commands/test_run_commands.py @@ -3,15 +3,15 @@ from unittest.mock import * import tableauserverclient as TSC -from src.commands.auth import login_command, logout_command -from src.commands.datasources_and_workbooks import ( +from tabcmd.commands.auth import login_command, logout_command +from tabcmd.commands.datasources_and_workbooks import ( delete_command, export_command, get_url_command, publish_command, runschedule_command, ) -from src.commands.extracts import ( +from tabcmd.commands.extracts import ( create_extracts_command, delete_extracts_command, decrypt_extracts_command, @@ -19,16 +19,16 @@ reencrypt_extracts_command, refresh_extracts_command, ) -from src.commands.group import create_group_command, delete_group_command -from src.commands.help import help_command -from src.commands.project import create_project_command, delete_project_command, publish_samples_command -from src.commands.site import ( +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, delete_site_command, edit_site_command, list_sites_command, ) -from src.commands.user import ( +from tabcmd.commands.user import ( add_users_command, create_site_users, create_users_command, @@ -63,7 +63,7 @@ @patch("tableauserverclient.Server") -@patch("src.commands.auth.session.Session.create_session") +@patch("tabcmd.commands.auth.session.Session.create_session") class RunCommandsTest(unittest.TestCase): @staticmethod def _set_up_session(mock_session, mock_server): @@ -79,7 +79,7 @@ def test_login(self, mock_session, mock_server): login_command.LoginCommand.run_command(mock_args) mock_session.assert_called_with(mock_args) - @patch("src.commands.auth.session.Session.end_session_and_clear_data") + @patch("tabcmd.commands.auth.session.Session.end_session_and_clear_data") def test_logout(self, mock_end_session, mock_create_session, mock_server): logout_command.LogoutCommand.run_command(mock_args) mock_create_session.assert_not_called() diff --git a/tests/commands/test_server_handler.py b/tests/commands/test_server_handler.py index b7ca74c5..5b4088e4 100644 --- a/tests/commands/test_server_handler.py +++ b/tests/commands/test_server_handler.py @@ -1,6 +1,6 @@ import unittest import logging -from src.commands.constants import Errors +from tabcmd.commands.constants import Errors # TODO add checks that the logger was called? diff --git a/tests/commands/test_session.py b/tests/commands/test_session.py index 64ea26a9..ebcf770b 100644 --- a/tests/commands/test_session.py +++ b/tests/commands/test_session.py @@ -3,7 +3,7 @@ from unittest import mock from unittest.mock import patch, mock_open -from src.commands.auth.session import Session +from tabcmd.commands.auth.session import Session import os args_to_mock = Namespace( diff --git a/tests/commands/test_user_utils.py b/tests/commands/test_user_utils.py index c6f7e76e..b22f7ff1 100644 --- a/tests/commands/test_user_utils.py +++ b/tests/commands/test_user_utils.py @@ -1,7 +1,7 @@ import unittest from unittest.mock import * -from src.commands.user.user_data import UserCommand, Userdata -from src.execution.logger_config import log +from tabcmd.commands.user.user_data import UserCommand, Userdata +from tabcmd.execution.logger_config import log from typing import List import io diff --git a/tests/e2e/tests_integration.py b/tests/e2e/tests_integration.py index 8ad6ee3e..d41070a1 100644 --- a/tests/e2e/tests_integration.py +++ b/tests/e2e/tests_integration.py @@ -2,9 +2,9 @@ import logging import pytest import unittest -from src.commands.auth.session import Session -from src.commands.server import Server -from src.execution.logger_config import log +from tabcmd.commands.auth.session import Session +from tabcmd.commands.server import Server +from tabcmd.execution.logger_config import log try: diff --git a/tests/parsers/common_setup.py b/tests/parsers/common_setup.py index 64a7d6a8..6530998b 100644 --- a/tests/parsers/common_setup.py +++ b/tests/parsers/common_setup.py @@ -1,4 +1,4 @@ -from src.execution import parent_parser +from tabcmd.execution import parent_parser from collections import namedtuple diff --git a/tests/parsers/test_login_parser.py b/tests/parsers/test_login_parser.py index 7369ed37..2276ef45 100644 --- a/tests/parsers/test_login_parser.py +++ b/tests/parsers/test_login_parser.py @@ -1,7 +1,7 @@ import unittest from unittest import mock -from src.commands.auth.login_command import LoginCommand +from tabcmd.commands.auth.login_command import LoginCommand from .common_setup import * commandname = "login" diff --git a/tests/parsers/test_logout_parser.py b/tests/parsers/test_logout_parser.py index 7e41fba3..5d59eab7 100644 --- a/tests/parsers/test_logout_parser.py +++ b/tests/parsers/test_logout_parser.py @@ -2,7 +2,7 @@ import unittest from unittest import mock -from src.commands.auth.logout_command import LogoutCommand +from tabcmd.commands.auth.logout_command import LogoutCommand from .common_setup import * commandname = "logout" diff --git a/tests/parsers/test_parser_add_user.py b/tests/parsers/test_parser_add_user.py index 62e1902e..fe229bce 100644 --- a/tests/parsers/test_parser_add_user.py +++ b/tests/parsers/test_parser_add_user.py @@ -1,7 +1,7 @@ import unittest from unittest import mock -from src.commands.user.add_users_command import AddUserCommand +from tabcmd.commands.user.add_users_command import AddUserCommand from .common_setup import * commandname = "addusers" diff --git a/tests/parsers/test_parser_create_extracts.py b/tests/parsers/test_parser_create_extracts.py index 6f7988f8..8a6d3f49 100644 --- a/tests/parsers/test_parser_create_extracts.py +++ b/tests/parsers/test_parser_create_extracts.py @@ -1,6 +1,6 @@ import unittest -from src.commands.extracts.create_extracts_command import CreateExtracts +from tabcmd.commands.extracts.create_extracts_command import CreateExtracts from .common_setup import * commandname = "createextracts" diff --git a/tests/parsers/test_parser_create_group.py b/tests/parsers/test_parser_create_group.py index 6790aaf2..de05934d 100644 --- a/tests/parsers/test_parser_create_group.py +++ b/tests/parsers/test_parser_create_group.py @@ -1,6 +1,6 @@ import unittest -from src.commands.group.create_group_command import CreateGroupCommand +from tabcmd.commands.group.create_group_command import CreateGroupCommand from .common_setup import * commandname = "creategroup" diff --git a/tests/parsers/test_parser_create_project.py b/tests/parsers/test_parser_create_project.py index e89ac0d2..b02319c6 100644 --- a/tests/parsers/test_parser_create_project.py +++ b/tests/parsers/test_parser_create_project.py @@ -1,6 +1,6 @@ import unittest -from src.commands.project.create_project_command import CreateProjectCommand +from tabcmd.commands.project.create_project_command import CreateProjectCommand from .common_setup import * commandname = "createproject" diff --git a/tests/parsers/test_parser_create_site.py b/tests/parsers/test_parser_create_site.py index 68f40b7d..ebe08813 100644 --- a/tests/parsers/test_parser_create_site.py +++ b/tests/parsers/test_parser_create_site.py @@ -1,6 +1,6 @@ import unittest -from src.commands.site.create_site_command import CreateSiteCommand +from tabcmd.commands.site.create_site_command import CreateSiteCommand from .common_setup import * commandname = "createsite" diff --git a/tests/parsers/test_parser_create_site_users.py b/tests/parsers/test_parser_create_site_users.py index 67e10fcb..ea157a98 100644 --- a/tests/parsers/test_parser_create_site_users.py +++ b/tests/parsers/test_parser_create_site_users.py @@ -1,7 +1,7 @@ import unittest from unittest import mock -from src.commands.user.create_site_users import CreateSiteUsersCommand +from tabcmd.commands.user.create_site_users import CreateSiteUsersCommand from .common_setup import * commandname = "createsiteusers" diff --git a/tests/parsers/test_parser_create_user.py b/tests/parsers/test_parser_create_user.py index 70bbe660..f93ea58f 100644 --- a/tests/parsers/test_parser_create_user.py +++ b/tests/parsers/test_parser_create_user.py @@ -1,7 +1,7 @@ import unittest from unittest import mock -from src.commands.user.create_users_command import CreateUsersCommand +from tabcmd.commands.user.create_users_command import CreateUsersCommand from .common_setup import * commandname = "createusers" diff --git a/tests/parsers/test_parser_decrypt_extracts.py b/tests/parsers/test_parser_decrypt_extracts.py index 4e327fa5..1fdd5dc2 100644 --- a/tests/parsers/test_parser_decrypt_extracts.py +++ b/tests/parsers/test_parser_decrypt_extracts.py @@ -1,6 +1,6 @@ import unittest -from src.commands.extracts.decrypt_extracts_command import DecryptExtracts +from tabcmd.commands.extracts.decrypt_extracts_command import DecryptExtracts from .common_setup import * commandname = "decryptextracts" diff --git a/tests/parsers/test_parser_delete.py b/tests/parsers/test_parser_delete.py index b2d1b508..9d6900d0 100644 --- a/tests/parsers/test_parser_delete.py +++ b/tests/parsers/test_parser_delete.py @@ -1,6 +1,6 @@ import unittest -from src.commands.datasources_and_workbooks.delete_command import DeleteCommand +from tabcmd.commands.datasources_and_workbooks.delete_command import DeleteCommand from .common_setup import * commandname = "delete" diff --git a/tests/parsers/test_parser_delete_extracts.py b/tests/parsers/test_parser_delete_extracts.py index c2e13b24..1d71d187 100644 --- a/tests/parsers/test_parser_delete_extracts.py +++ b/tests/parsers/test_parser_delete_extracts.py @@ -1,6 +1,6 @@ import unittest -from src.commands.extracts.delete_extracts_command import DeleteExtracts +from tabcmd.commands.extracts.delete_extracts_command import DeleteExtracts from .common_setup import * commandname = "deleteextracts" diff --git a/tests/parsers/test_parser_delete_group.py b/tests/parsers/test_parser_delete_group.py index 8269eda7..368e3cce 100644 --- a/tests/parsers/test_parser_delete_group.py +++ b/tests/parsers/test_parser_delete_group.py @@ -1,6 +1,6 @@ import unittest -from src.commands.group.delete_group_command import DeleteGroupCommand +from tabcmd.commands.group.delete_group_command import DeleteGroupCommand from .common_setup import * commandname = "deletegroup" diff --git a/tests/parsers/test_parser_delete_project.py b/tests/parsers/test_parser_delete_project.py index e4b2d1c7..5ecf43bc 100644 --- a/tests/parsers/test_parser_delete_project.py +++ b/tests/parsers/test_parser_delete_project.py @@ -1,6 +1,6 @@ import unittest -from src.commands.project.delete_project_command import DeleteProjectCommand +from tabcmd.commands.project.delete_project_command import DeleteProjectCommand from .common_setup import * commandname = "deleteproject" diff --git a/tests/parsers/test_parser_delete_site.py b/tests/parsers/test_parser_delete_site.py index 843e9c82..2d9562f7 100644 --- a/tests/parsers/test_parser_delete_site.py +++ b/tests/parsers/test_parser_delete_site.py @@ -1,6 +1,6 @@ import unittest -from src.commands.site.delete_site_command import DeleteSiteCommand +from tabcmd.commands.site.delete_site_command import DeleteSiteCommand from .common_setup import * commandname = "deletesite" diff --git a/tests/parsers/test_parser_delete_site_user.py b/tests/parsers/test_parser_delete_site_user.py index 8728a2b5..df66af66 100644 --- a/tests/parsers/test_parser_delete_site_user.py +++ b/tests/parsers/test_parser_delete_site_user.py @@ -1,7 +1,7 @@ import unittest from unittest import mock -from src.commands.user.delete_site_users_command import DeleteSiteUsersCommand +from tabcmd.commands.user.delete_site_users_command import DeleteSiteUsersCommand from .common_setup import * commandname = "deletesiteusers" diff --git a/tests/parsers/test_parser_edit_site.py b/tests/parsers/test_parser_edit_site.py index 19298a5a..447deecf 100644 --- a/tests/parsers/test_parser_edit_site.py +++ b/tests/parsers/test_parser_edit_site.py @@ -1,6 +1,6 @@ import unittest -from src.commands.site.edit_site_command import EditSiteCommand +from tabcmd.commands.site.edit_site_command import EditSiteCommand from .common_setup import * commandname = "editsites" diff --git a/tests/parsers/test_parser_encrypt_extracts.py b/tests/parsers/test_parser_encrypt_extracts.py index 683801ce..e87f8f2a 100644 --- a/tests/parsers/test_parser_encrypt_extracts.py +++ b/tests/parsers/test_parser_encrypt_extracts.py @@ -1,6 +1,6 @@ import unittest -from src.commands.extracts.encrypt_extracts_command import EncryptExtracts +from tabcmd.commands.extracts.encrypt_extracts_command import EncryptExtracts from .common_setup import * commandname = "encryptextracts" diff --git a/tests/parsers/test_parser_export.py b/tests/parsers/test_parser_export.py index ada43078..f90999a9 100644 --- a/tests/parsers/test_parser_export.py +++ b/tests/parsers/test_parser_export.py @@ -1,6 +1,6 @@ import unittest -from src.commands.datasources_and_workbooks.export_command import ExportCommand +from tabcmd.commands.datasources_and_workbooks.export_command import ExportCommand from .common_setup import * commandname = "export" diff --git a/tests/parsers/test_parser_get_url.py b/tests/parsers/test_parser_get_url.py index dcc601f1..5467e236 100644 --- a/tests/parsers/test_parser_get_url.py +++ b/tests/parsers/test_parser_get_url.py @@ -1,6 +1,6 @@ import unittest import argparse -from src.commands.datasources_and_workbooks.get_url_command import GetUrl +from tabcmd.commands.datasources_and_workbooks.get_url_command import GetUrl from .common_setup import * commandname = "listsites" diff --git a/tests/parsers/test_parser_list_sites.py b/tests/parsers/test_parser_list_sites.py index e1b025ef..71b01234 100644 --- a/tests/parsers/test_parser_list_sites.py +++ b/tests/parsers/test_parser_list_sites.py @@ -1,6 +1,6 @@ import unittest -from src.commands.site.list_sites_command import ListSiteCommand +from tabcmd.commands.site.list_sites_command import ListSiteCommand from .common_setup import * commandname = "listsites" diff --git a/tests/parsers/test_parser_publish.py b/tests/parsers/test_parser_publish.py index 356bfe60..261aeb6e 100644 --- a/tests/parsers/test_parser_publish.py +++ b/tests/parsers/test_parser_publish.py @@ -1,6 +1,6 @@ import unittest -from src.commands.datasources_and_workbooks.publish_command import PublishCommand +from tabcmd.commands.datasources_and_workbooks.publish_command import PublishCommand from .common_setup import * commandname = "Publish" diff --git a/tests/parsers/test_parser_publish_samples.py b/tests/parsers/test_parser_publish_samples.py index 52e04c6d..43c4062a 100644 --- a/tests/parsers/test_parser_publish_samples.py +++ b/tests/parsers/test_parser_publish_samples.py @@ -1,6 +1,6 @@ import unittest -from src.commands.project.publish_samples_command import PublishSamplesCommand +from tabcmd.commands.project.publish_samples_command import PublishSamplesCommand from .common_setup import * commandname = "publishsamples" diff --git a/tests/parsers/test_parser_reencrypt_extracts.py b/tests/parsers/test_parser_reencrypt_extracts.py index a465937c..79d82cda 100644 --- a/tests/parsers/test_parser_reencrypt_extracts.py +++ b/tests/parsers/test_parser_reencrypt_extracts.py @@ -1,6 +1,6 @@ import unittest -from src.commands.extracts.reencrypt_extracts_command import ReencryptExtracts +from tabcmd.commands.extracts.reencrypt_extracts_command import ReencryptExtracts from .common_setup import * commandname = "reencryptextracts" diff --git a/tests/parsers/test_parser_refresh_extracts.py b/tests/parsers/test_parser_refresh_extracts.py index 942182d3..a74ac760 100644 --- a/tests/parsers/test_parser_refresh_extracts.py +++ b/tests/parsers/test_parser_refresh_extracts.py @@ -1,6 +1,6 @@ import unittest -from src.commands.extracts.refresh_extracts_command import RefreshExtracts +from tabcmd.commands.extracts.refresh_extracts_command import RefreshExtracts from .common_setup import * commandname = "refreshextracts" diff --git a/tests/parsers/test_parser_remove_user.py b/tests/parsers/test_parser_remove_user.py index d77d00e1..83562c97 100644 --- a/tests/parsers/test_parser_remove_user.py +++ b/tests/parsers/test_parser_remove_user.py @@ -1,7 +1,7 @@ import unittest from unittest import mock -from src.commands.user.remove_users_command import RemoveUserCommand +from tabcmd.commands.user.remove_users_command import RemoveUserCommand from .common_setup import * commandname = "removeusers" diff --git a/tests/parsers/test_parser_runschedule.py b/tests/parsers/test_parser_runschedule.py index 48c71d37..445dfb58 100644 --- a/tests/parsers/test_parser_runschedule.py +++ b/tests/parsers/test_parser_runschedule.py @@ -1,6 +1,6 @@ import unittest -from src.commands.datasources_and_workbooks.runschedule_command import RunSchedule +from tabcmd.commands.datasources_and_workbooks.runschedule_command import RunSchedule from .common_setup import * commandname = "runschedule" From d000dc4a0e76717d1a1200fbcd5af19302bf722e Mon Sep 17 00:00:00 2001 From: Jac Date: Mon, 25 Jul 2022 17:42:36 -0700 Subject: [PATCH 12/37] Jac/publish to folder (#145) Defect 1428581: [Tabcmd WAM] Publishing to projects other than default throws an error --- .../datasources_and_workbooks/publish_command.py | 1 + tabcmd/commands/server.py | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/tabcmd/commands/datasources_and_workbooks/publish_command.py b/tabcmd/commands/datasources_and_workbooks/publish_command.py index c81c0091..ec607755 100644 --- a/tabcmd/commands/datasources_and_workbooks/publish_command.py +++ b/tabcmd/commands/datasources_and_workbooks/publish_command.py @@ -43,6 +43,7 @@ def run_command(args): ) project_id = dest_project.id except Exception as exc: + logger.error(exc.__str__()) Errors.exit_with_error(logger, _("publish.errors.server_resource_not_found"), exc) else: project_id = "" diff --git a/tabcmd/commands/server.py b/tabcmd/commands/server.py index 38aac8ef..fd866055 100644 --- a/tabcmd/commands/server.py +++ b/tabcmd/commands/server.py @@ -106,11 +106,13 @@ def get_filename_extension_if_tableau_type(logger, filename): @staticmethod def get_project_by_name_and_parent_path(logger, server, project_name, parent_path): - logger.debug(_("content_type.project") + ":{0}, {1}".format(parent_path, project_name)) - project_tree = Server._parse_project_path_to_list(parent_path) if not project_name: - project = Server._get_parent_project_from_tree(logger, server, project_tree) + project_name = "Default" + if not parent_path: + project = Server._get_project_by_name_and_parent(logger, server, project_name, None) else: + logger.debug("Finding project within the given parent") + project_tree = Server._parse_project_path_to_list(parent_path) parent = Server._get_parent_project_from_tree(logger, server, project_tree) project = Server._get_project_by_name_and_parent(logger, server, project_name, parent) if not project: @@ -137,7 +139,7 @@ def _get_project_by_name_and_parent(logger, server, project_name, parent): @staticmethod def _get_parent_project_from_tree(logger, server, hierarchy): - # logger.debug("get parent project from tree: {0}".format(hierarchy)) + logger.debug("get parent project from tree: {0}".format(hierarchy)) tree_height = len(hierarchy) if tree_height == 0: return None From fdcb18df31b9892a830f34419fdfeaa7ef2c9760 Mon Sep 17 00:00:00 2001 From: Jac Date: Mon, 1 Aug 2022 15:58:47 -0700 Subject: [PATCH 13/37] Jac/delete command (#144) * filter for items in project * separate the use of ds/wb as flags vs as argument names * update publish_samples to call shared project logic --- .../delete_command.py | 45 +++++++---- .../project/publish_samples_command.py | 6 +- tabcmd/commands/server.py | 75 +++++++++++------- tabcmd/commands/site/list_sites_command.py | 2 +- tabcmd/execution/global_options.py | 78 ++++++++++--------- tests/commands/test_projects_utils.py | 2 +- tests/commands/test_run_commands.py | 3 + 7 files changed, 125 insertions(+), 86 deletions(-) diff --git a/tabcmd/commands/datasources_and_workbooks/delete_command.py b/tabcmd/commands/datasources_and_workbooks/delete_command.py index e9750a93..8e68cb41 100644 --- a/tabcmd/commands/datasources_and_workbooks/delete_command.py +++ b/tabcmd/commands/datasources_and_workbooks/delete_command.py @@ -21,7 +21,8 @@ class DeleteCommand(DatasourcesAndWorkbooks): @staticmethod def define_args(delete_parser): - delete_parser.add_argument("name", help=_("content_type.workbook") + _("content_type.datasource")) + 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) @@ -31,26 +32,42 @@ def run_command(args): logger.debug(_("tabcmd.launching")) session = Session() server = session.create_session(args) + content_type: str = "" + if args.workbook: + content_type = "workbook" + elif args.datasource: + content_type = "datasource" - logger.info(_("delete.status").format(args.name, "")) + container: TSC.ProjectItem = DeleteCommand.get_project_by_name_and_parent_path( + logger, server, args.project_name, args.parent_project_path + ) + if container: + item_name = (args.parent_project_path or "") + "/" + (args.project_name or "default") + "/" + args.name + else: + Errors.exit_with_error(logger, "Containing project could not be found") + logger.info(_("delete.status").format(content_type, item_name or args.name)) error = None - try: - item_to_delete = DeleteCommand.get_workbook_item(logger, server, args.name) - item_type = "workbook" - except TSC.ServerResponseError as workbook_error: - error = workbook_error - try: - item_to_delete = DeleteCommand.get_data_source_item(logger, server, args.name) - item_type = "datasource" - except TSC.ServerResponseError as ds_error: - error = ds_error - if not item_type: + if args.workbook or not content_type: + logger.debug("Attempt as workbook") + try: + item_to_delete = DeleteCommand.get_workbook_item(logger, server, args.name, container) + content_type = "workbook" + except TSC.ServerResponseError as error: + logger.debug(error) + if args.datasource or not content_type: + logger.debug("Attempt as datasource") + try: + item_to_delete = DeleteCommand.get_data_source_item(logger, server, args.name, container) + content_type = "datasource" + except TSC.ServerResponseError as error: + logger.debug(error) + if not content_type or not item_to_delete: logger.debug(error) Errors.exit_with_error(logger, _("delete.errors.requires_workbook_datasource")) try: - if item_type == "workbook": + if content_type == "workbook": server.workbooks.delete(item_to_delete.id) else: server.datasources.delete(item_to_delete.id) diff --git a/tabcmd/commands/project/publish_samples_command.py b/tabcmd/commands/project/publish_samples_command.py index 907f8a5c..65f47358 100644 --- a/tabcmd/commands/project/publish_samples_command.py +++ b/tabcmd/commands/project/publish_samples_command.py @@ -31,13 +31,9 @@ def run_command(args): logger.debug(_("tabcmd.launching")) session = Session() server = session.create_session(args) - if args.parent_project_path is not None: - project_path = Server.get_project_by_name_and_parent_path(logger, server, None, args.parent_project_path) - else: - project_path = None try: project = PublishSamplesCommand.get_project_by_name_and_parent_path( - logger, server, args.project_name, project_path + logger, server, args.project_name, args.parent_project_path ) except Exception as e: Errors.exit_with_error(logger, _("tabcmd.report.error.publish_samples.expected_project"), exception=e) diff --git a/tabcmd/commands/server.py b/tabcmd/commands/server.py index fd866055..09fa965c 100644 --- a/tabcmd/commands/server.py +++ b/tabcmd/commands/server.py @@ -1,4 +1,6 @@ import os +from typing import List, Optional + import tableauserverclient as TSC from tabcmd.commands.constants import Errors @@ -10,24 +12,24 @@ class Server: @staticmethod def get_workbook_item(logger, server, workbook_name, container=None): try: - return Server.get_items_by_name(logger, server.workbooks, workbook_name, container=None)[0] + return Server.get_items_by_name(logger, server.workbooks, workbook_name, container)[0] except Exception as e: Errors.exit_with_error(logger, exception=e) @staticmethod def get_workbook_id(logger, server, workbook_name, container=None): - return Server.get_workbook_item(logger, server, workbook_name, container=None).id + return Server.get_workbook_item(logger, server, workbook_name, container).id @staticmethod def get_data_source_item(logger, server, data_source_name, container=None): try: - return Server.get_items_by_name(logger, server.datasources, data_source_name, container=None)[0] + return Server.get_items_by_name(logger, server.datasources, data_source_name, container)[0] except Exception as e: Errors.exit_with_error(logger, exception=e) @staticmethod def get_data_source_id(logger, server, data_source_name, container=None): - return Server.get_data_source_item(logger, server, data_source_name, container=None).id + return Server.get_data_source_item(logger, server, data_source_name, container).id @staticmethod def find_group(logger, server, group_name): @@ -48,28 +50,36 @@ def find_user_id(logger, server, username): Errors.exit_with_error(logger, exception=e) @staticmethod - def get_items_by_name(logger, item_endpoint, item_name, container=None): + def get_items_by_name(logger, item_endpoint, item_name: str, container: TSC.ProjectItem = None): item_type = type(item_endpoint).__name__ - item_log_name = item_name + item_log_name: str = "[" + item_type + "] " + item_name if container: - item_log_name = container + "/" + item_log_name - item_log_name = "[" + item_type + "] " + item_log_name + item_log_name = str(container) + "/" + item_log_name logger.debug(_("export.status").format(item_log_name)) req_option = TSC.RequestOptions() req_option.filter.add(TSC.Filter(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Operator.Equals, item_name)) - if container: - logger.debug("Searching in project {}".format(container)) - req_option.filter.add( - TSC.Filter(TSC.RequestOptions.Field.ParentProjectId, TSC.RequestOptions.Operator.Equals, container) - ) + all_items, pagination_item = item_endpoint.get(req_option) if all_items is None or all_items == []: raise ValueError("[" + item_type + "] " + _("errors.xmlapi.not_found")) + if len(all_items) == 1: + logger.debug("Exactly one result found") + result = all_items if len(all_items) > 1: - logger.debug("{}+ items of this name were found. Returning first page.".format(len(all_items))) - logger.debug(all_items[0].name + ", " + all_items[1].name + ", " + all_items[2].name) + logger.debug( + "{}+ items of this name were found: {}".format( + len(all_items), all_items[0].name + ", " + all_items[1].name + ", ..." + ) + ) + + if container: + container_id = container.id + logger.debug("Filtering to items in project {}".format(container.id)) + result = list(filter(lambda item: item.project_id == container_id, all_items)) + else: + result = all_items - return all_items + return result # Get site by name or get currently logged in site @staticmethod @@ -105,28 +115,35 @@ def get_filename_extension_if_tableau_type(logger, filename): ) @staticmethod - def get_project_by_name_and_parent_path(logger, server, project_name, parent_path): - if not project_name: - project_name = "Default" + def get_project_by_name_and_parent_path(logger, server, project_name: str, parent_path: str) -> TSC.ProjectItem: + logger.debug(_("content_type.project") + ":{0}, {1}".format(parent_path, project_name)) if not parent_path: - project = Server._get_project_by_name_and_parent(logger, server, project_name, None) - else: - logger.debug("Finding project within the given parent") - project_tree = Server._parse_project_path_to_list(parent_path) - parent = Server._get_parent_project_from_tree(logger, server, project_tree) - project = Server._get_project_by_name_and_parent(logger, server, project_name, parent) + if not project_name: + project_name = "Default" + project: TSC.ProjectItem = Server.get_items_by_name(logger, server.projects, project_name, None) + return project + + project_tree: List[str] = Server._parse_project_path_to_list(parent_path) + if not project_name: + project = Server._get_parent_project_from_tree(logger, server, project_tree) + return project + + parent = Server._get_parent_project_from_tree(logger, server, project_tree) + project = Server._get_project_by_name_and_parent(logger, server, project_name, parent) if not project: Errors.exit_with_error(logger, message=_("publish.errors.server_resource_not_found")) return project @staticmethod - def _parse_project_path_to_list(project_path): - if project_path is None: + def _parse_project_path_to_list(project_path: str): + if project_path is None or project_path == "": return [] + if project_path.find("/") == -1: + return [project_path] return project_path.split("/") @staticmethod - def _get_project_by_name_and_parent(logger, server, project_name, parent): + def _get_project_by_name_and_parent(logger, server, project_name: str, parent: Optional[TSC.ProjectItem]): # logger.debug("get by name and parent: {0}, {1}".format(project_name, parent)) # get by name to narrow down the list projects = Server.get_items_by_name(logger, server.projects, project_name) @@ -138,7 +155,7 @@ def _get_project_by_name_and_parent(logger, server, project_name, parent): return projects[0] @staticmethod - def _get_parent_project_from_tree(logger, server, hierarchy): + def _get_parent_project_from_tree(logger, server, hierarchy: List[str]): logger.debug("get parent project from tree: {0}".format(hierarchy)) tree_height = len(hierarchy) if tree_height == 0: diff --git a/tabcmd/commands/site/list_sites_command.py b/tabcmd/commands/site/list_sites_command.py index 418f6caf..15d353bf 100644 --- a/tabcmd/commands/site/list_sites_command.py +++ b/tabcmd/commands/site/list_sites_command.py @@ -18,7 +18,7 @@ class ListSiteCommand(Server): @staticmethod def define_args(list_site_parser): - set_view_site_encryption(list_site_parser) + set_site_detail_option(list_site_parser) @staticmethod def run_command(args): diff --git a/tabcmd/execution/global_options.py b/tabcmd/execution/global_options.py index 8c259028..34672806 100644 --- a/tabcmd/execution/global_options.py +++ b/tabcmd/execution/global_options.py @@ -172,21 +172,19 @@ def set_project_arg(parser): return parser -def set_datasource_arg(parser, action="store_true"): - parser.add_argument("-d", "--datasource", help="The name of the target data source.", action=action) - return parser - - def set_site_url_arg(parser): parser.add_argument("--url", help="The canonical name for the resource as it appears in the URL") return parser -def set_workbook_arg(parser, action="store_true"): # true if the user adds --workbook - parser.add_argument("-w", "--workbook", help="The name of the target workbook.", action=action) +def set_ds_xor_wb_options(parser): + target_type_group = parser.add_mutually_exclusive_group(required=False) + target_type_group.add_argument("-d", "--datasource", action="store_true", help="The name of the target datasource.") + target_type_group.add_argument("-w", "--workbook", action="store_true", help="The name of the target workbook.") return parser +# pass arguments for either --datasource or --workbook def set_ds_xor_wb_args(parser): target_type_group = parser.add_mutually_exclusive_group(required=True) target_type_group.add_argument("-d", "--datasource", help="The name of the target datasource.") @@ -212,7 +210,7 @@ def set_site_status_arg(parser): # create-site/update-site - lots of these options are never used elsewhere # mismatched arguments: createsite says --url, editsite says --site-id # just let both commands use either of them -def set_site_id_options(parser): +def set_site_id_args(parser): site_id = parser.add_mutually_exclusive_group() site_id.add_argument("--site-id", help="Used in the URL to uniquely identify the site.") site_id.add_argument( @@ -226,24 +224,11 @@ def set_site_id_options(parser): # these options are all shared in create-site and edit-site def set_common_site_args(parser): - parser = set_site_id_options(parser) + parser = set_site_id_args(parser) parser.add_argument("--user-quota", type=int, help="Maximum number of users that can be added to the site.") - site_help = "Allows or denies site administrators the ability to add users to or remove users from the site." - site_group = parser.add_mutually_exclusive_group() - site_group.add_argument( - "--site-mode", - dest="site_admin_user_management", - action="store_true", - help=site_help, - ) - site_group.add_argument( - "--no-site-mode", - dest="site_admin_user_management", - action="store_false", - help=site_help, - ) + set_site_mode_option(parser) parser.add_argument( "--storage-quota", @@ -265,8 +250,25 @@ def set_common_site_args(parser): return parser +def set_site_mode_option(parser): + site_help = "Allows or denies site administrators the ability to add users to or remove users from the site." + site_group = parser.add_mutually_exclusive_group() + site_group.add_argument( + "--site-mode", + dest="site_admin_user_management", + action="store_true", + help=site_help, + ) + site_group.add_argument( + "--no-site-mode", + dest="site_admin_user_management", + action="store_false", + help=site_help, + ) + + # this option is only used by listsites -def set_view_site_encryption(parser): +def set_site_detail_option(parser): parser.add_argument( "--get-extract-encryption-mode", action="store_true", @@ -282,18 +284,7 @@ def set_filename_arg(parser, description=_("get.options.file")): def set_publish_args(parser): parser.add_argument("-n", "--name", help="Name to publish the new datasource or workbook by.") - append_group = parser.add_mutually_exclusive_group() - append_group.add_argument( - "-o", - "--overwrite", - action="store_true", - help="Overwrites the workbook, data source, or data extract if it already exists on the server.", - ) - append_group.add_argument( - "--append", - action="store_true", - help="Append the extract file to the existing data source.", - ) + set_overwrite_option(parser) parser.add_argument( "--db-username", help="Use this option to publish a database user name with the workbook, data source, or data extract.", @@ -329,6 +320,21 @@ def set_publish_args(parser): parser.add_argument("--thumbnail-group", help="Not yet implemented") # not implemented in the REST API +def set_overwrite_option(parser): + append_group = parser.add_mutually_exclusive_group() + append_group.add_argument( + "-o", + "--overwrite", + action="store_true", + help="Overwrites the workbook, data source, or data extract if it already exists on the server.", + ) + append_group.add_argument( + "--append", + action="store_true", + help="Append the extract file to the existing data source.", + ) + + # refresh-extracts def set_incremental_options(parser): sync_group = parser.add_mutually_exclusive_group() diff --git a/tests/commands/test_projects_utils.py b/tests/commands/test_projects_utils.py index 61799bd7..9ba3c09d 100644 --- a/tests/commands/test_projects_utils.py +++ b/tests/commands/test_projects_utils.py @@ -16,7 +16,7 @@ class ProjectsTest(unittest.TestCase): @staticmethod def test_parent_path_to_list(): assert Server._parse_project_path_to_list(None) == [] - assert Server._parse_project_path_to_list("") == [""] + assert Server._parse_project_path_to_list("") == [] assert Server._parse_project_path_to_list("parent") == ["parent"] assert Server._parse_project_path_to_list("parent/child") == ["parent", "child"] diff --git a/tests/commands/test_run_commands.py b/tests/commands/test_run_commands.py index 4ab980c3..7e7e3d45 100644 --- a/tests/commands/test_run_commands.py +++ b/tests/commands/test_run_commands.py @@ -89,8 +89,11 @@ def test_delete(self, mock_session, mock_server): RunCommandsTest._set_up_session(mock_session, mock_server) mock_server.workbooks = getter mock_server.datasources = getter + mock_server.projects = getter mock_args.workbook = True mock_args.datasource = False + mock_args.project_name = None + mock_args.parent_project_path = None mock_args.name = "name for on server" delete_command.DeleteCommand.run_command(mock_args) From cc4ee24c1418812856e7607c7e235e9c42727865 Mon Sep 17 00:00:00 2001 From: Jac Date: Wed, 3 Aug 2022 01:01:11 -0700 Subject: [PATCH 14/37] Jac/e2e test workflow (#154) * Create run-e2-tests.yml --- .github/workflows/run-e2-tests.yml | 45 +++++++++++++++++++++++ tests/e2e/online_tests.py | 59 ++++++++++++++++++++++-------- 2 files changed, 89 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/run-e2-tests.yml diff --git a/.github/workflows/run-e2-tests.yml b/.github/workflows/run-e2-tests.yml new file mode 100644 index 00000000..371577f5 --- /dev/null +++ b/.github/workflows/run-e2-tests.yml @@ -0,0 +1,45 @@ +name: Python tests + +on: + workflow_dispatch: + inputs: + server: + required: true + site: + required: true + patname: + required: true + pat: + required: true + +jobs: + build: + strategy: + fail-fast: true + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ['3.7', '3.8', '3.9', '3.10', '3'] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python --version + python -m pip install --upgrade pip + pip install -e .[build] + pip install -e .[test] + doit version + python setup.py build + + - name: Run e2e tests + run: | + python -m tabcmd login --server "${{ github.event.inputs.server }}" --site "${{ github.event.inputs.site }}" --token-name "${{ github.event.inputs.patname }}" --token-value "${{ github.event.inputs.pat }}" + pytest -q tests\e2e\online_tests.py -r pfE diff --git a/tests/e2e/online_tests.py b/tests/e2e/online_tests.py index 7aa69610..b1e6afbb 100644 --- a/tests/e2e/online_tests.py +++ b/tests/e2e/online_tests.py @@ -18,6 +18,14 @@ # you can either run setup with a stored credentials file, or simply log in # before running the suite so a session is active +# alpodev +parent_location = "WAM" +project_name = "Developer Platform" + +server_admin = False +site_admin = True +project_admin = True + def _test_command(test_args: list[str]): # this will raise an exception if it gets a non-zero return code @@ -43,18 +51,18 @@ def setup_class(cls): def _create_project(self, project_name, parent_path=None): command = "createproject" arguments = [command, "--name", project_name] - if parent_path: + if parent_path or parent_location: arguments.append("--parent-project-path") - arguments.append(parent_path) + arguments.append(parent_path or parent_location) print(arguments) _test_command(arguments) def _delete_project(self, project_name, parent_path=None): command = "deleteproject" arguments = [command, project_name] - if parent_path: + if parent_path or parent_location: arguments.append("--parent-project-path") - arguments.append(parent_path) + arguments.append(parent_path or parent_location) _test_command(arguments) def _publish_samples(self, project_name): @@ -135,6 +143,8 @@ def test_help(self): @pytest.mark.order(2) def test_create_site_users(self): + if not server_admin and not site_admin: + pytest.skip("Must be server or site administrator to create site users") command = "createsiteusers" users = os.path.join("tests", "assets", "detailed_users.csv") arguments = [command, users, "--role", "Publisher"] @@ -142,6 +152,8 @@ def test_create_site_users(self): @pytest.mark.order(3) def test_creategroup(self): + if not server_admin and not site_admin: + pytest.skip("Must be server or site administrator to create groups") groupname = group_name command = "creategroup" arguments = [command, groupname] @@ -149,6 +161,9 @@ def test_creategroup(self): @pytest.mark.order(4) def test_add_users_to_group(self): + if not server_admin and not site_admin: + pytest.skip("Must be server or site administrator to add to groups") + groupname = group_name command = "addusers" filename = os.path.join("tests", "assets", "usernames.csv") @@ -157,6 +172,9 @@ def test_add_users_to_group(self): @pytest.mark.order(5) def test_remove_users_to_group(self): + if not server_admin and not site_admin: + pytest.skip("Must be server or site administrator to remove from groups") + groupname = group_name command = "removeusers" filename = os.path.join("tests", "assets", "usernames.csv") @@ -165,22 +183,19 @@ def test_remove_users_to_group(self): @pytest.mark.order(6) def test_deletegroup(self): + if not server_admin and not site_admin: + pytest.skip("Must be server or site administrator to delete groups") + groupname = group_name command = "deletegroup" arguments = [command, groupname] _test_command(arguments) - @pytest.mark.order(7) - def test_publish_samples(self): - project_name = "sample-proj" - self._create_project(project_name) - time.sleep(indexing_sleep_time) - - self._publish_samples(project_name) - self._delete_project(project_name) - @pytest.mark.order(8) def test_create_projects(self): + + if not project_admin: + pytest.skip("Must be project administrator to create projects") # project 1 self._create_project(project_name) time.sleep(indexing_sleep_time) @@ -194,12 +209,14 @@ def test_create_projects(self): @pytest.mark.order(9) def test_delete_projects(self): + if not project_admin: + pytest.skip("Must be project administrator to create projects") self._delete_project("project_name_2", project_name) # project 2 self._delete_project(project_name) @pytest.mark.order(9) def test_publish_samples(self): - self._publish_samples("Default") + self._publish_samples(project_name) @pytest.mark.order(10) def test_publish(self): @@ -256,6 +273,7 @@ def test_create_extract(self): @pytest.mark.order(14) def test_refresh_extract(self): + # must be a datasource owned by the test user name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME self._refresh_extract(name_on_server) self._delete_wb(name_on_server) @@ -292,11 +310,22 @@ def test_export_view(self): @pytest.mark.order(16) def test_delete_site_users(self): + if not server_admin and not site_admin: + pytest.skip("Must be server or site administrator to delete site users") + command = "deletesiteusers" users = os.path.join("tests", "assets", "usernames.csv") _test_command([command, users]) @pytest.mark.order(20) def test_list_sites(self): + if not server_admin: + pytest.skip("Must be server administrator to list sites") + command = "listsites" - _test_command([command]) + try: + _test_command([command]) + except Exception as E: + print("yay") + result = True + assert result From 7fb7b9cb41368ce7123206773785cb270ab155bd Mon Sep 17 00:00:00 2001 From: Jac Date: Fri, 5 Aug 2022 13:56:27 -0700 Subject: [PATCH 15/37] Add command to list items of a given content type (#153) * new command: list content --- tabcmd.py | 2 +- tabcmd/commands/site/list_command.py | 45 ++++++++++++++++++++++ tabcmd/commands/site/list_sites_command.py | 5 +-- tabcmd/execution/map_of_commands.py | 2 + tests/e2e/online_tests.py | 9 +++++ 5 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 tabcmd/commands/site/list_command.py diff --git a/tabcmd.py b/tabcmd.py index 1c987d62..55e7e634 100644 --- a/tabcmd.py +++ b/tabcmd.py @@ -1,4 +1,4 @@ -from src import tabcmd +from tabcmd import tabcmd if __name__ == "__main__": tabcmd.main() diff --git a/tabcmd/commands/site/list_command.py b/tabcmd/commands/site/list_command.py new file mode 100644 index 00000000..2eff8ec5 --- /dev/null +++ b/tabcmd/commands/site/list_command.py @@ -0,0 +1,45 @@ +import tableauserverclient as TSC + +from tabcmd.commands.auth.session import Session +from tabcmd.commands.constants import Errors +from tabcmd.commands.server import Server +from tabcmd.execution.global_options import * +from tabcmd.execution.localize import _ +from tabcmd.execution.logger_config import log + + +class ListCommand(Server): + """ + Command to return a list of content the user can access + """ + + name: str = "list" + description: str = "List content items of a specified type" + + @staticmethod + def define_args(list_parser): + list_parser.add_argument("content", choices=["projects", "workbooks", "datasources"], help="View content") + + @staticmethod + def run_command(args): + logger = log(__name__, args.logging_level) + logger.debug(_("tabcmd.launching")) + session = Session() + server = session.create_session(args) + content_type = args.content + + try: + if content_type == "projects": + items = server.projects.all() + elif content_type == "workbooks": + items = server.workbooks.all() + elif content_type == "datasources": + items = server.datasources.all() + + logger.info("===== Listing {0} content for user {1}...".format(content_type, session.username)) + for item in items: + print("NAME:".rjust(10), item.name) + print("ID:".rjust(10), item.id) + + except TSC.ServerResponseError as e: + Errors.exit_with_error(logger, e) diff --git a/tabcmd/commands/site/list_sites_command.py b/tabcmd/commands/site/list_sites_command.py index 15d353bf..55e69f85 100644 --- a/tabcmd/commands/site/list_sites_command.py +++ b/tabcmd/commands/site/list_sites_command.py @@ -30,10 +30,9 @@ def run_command(args): sites, pagination = server.sites.get() logger.info(_("listsites.status").format(session.username)) for site in sites: - print("NAME:", site.name) - print("SITEID:", site.content_url) + print("NAME:".rjust(10), site.name) + print("SITEID:".rjust(10), site.content_url) if args.get_extract_encryption_mode: print("EXTRACTENCRYPTION:", site.extract_encryption_mode) - print("") except TSC.ServerResponseError as e: Errors.exit_with_error(logger, e) diff --git a/tabcmd/execution/map_of_commands.py b/tabcmd/execution/map_of_commands.py index 87ff0a1d..c6f7a679 100644 --- a/tabcmd/execution/map_of_commands.py +++ b/tabcmd/execution/map_of_commands.py @@ -23,6 +23,7 @@ from tabcmd.commands.user.add_users_command import * from tabcmd.commands.user.create_site_users import * from tabcmd.commands.user.delete_site_users_command import * +from tabcmd.commands.site.list_command import * # from tabcmd.commands.user.create_users import * from tabcmd.commands.user.remove_users_command import * @@ -51,6 +52,7 @@ class CommandsMap: GetUrl, HelpCommand, ListSiteCommand, + ListCommand, LoginCommand, LogoutCommand, PublishCommand, diff --git a/tests/e2e/online_tests.py b/tests/e2e/online_tests.py index b1e6afbb..cb391e29 100644 --- a/tests/e2e/online_tests.py +++ b/tests/e2e/online_tests.py @@ -114,6 +114,11 @@ def _delete_extract(self, wb_name): arguments = [command, "-w", wb_name] _test_command(arguments) + def _list(self, item_type: str): + command = "list" + arguments = [command, item_type] + _test_command(arguments) + # actual tests TWBX_FILE_WITH_EXTRACT = "extract-data-access.twbx" TWBX_WITH_EXTRACT_NAME = "WorkbookWithExtract" @@ -207,6 +212,10 @@ def test_create_projects(self): self._create_project(project_name, parent_path) time.sleep(indexing_sleep_time) + @pytest.mark.order(8) + def test_list_projects(self): + self._list("projects") + @pytest.mark.order(9) def test_delete_projects(self): if not project_admin: From 6722d613445e2335c902e99cc40393469dba7401 Mon Sep 17 00:00:00 2001 From: Jac Date: Thu, 1 Sep 2022 11:33:49 -0700 Subject: [PATCH 16/37] make export use filters, add tests (#164) --- .../export_command.py | 87 ++++++++++++------- tabcmd/execution/logger_config.py | 2 + tests/commands/test_geturl_utils.py | 80 ++++++++++++++++- tests/commands/test_run_commands.py | 9 ++ tests/e2e/online_tests.py | 2 +- 5 files changed, 147 insertions(+), 33 deletions(-) diff --git a/tabcmd/commands/datasources_and_workbooks/export_command.py b/tabcmd/commands/datasources_and_workbooks/export_command.py index 72f7375a..11cf2b82 100644 --- a/tabcmd/commands/datasources_and_workbooks/export_command.py +++ b/tabcmd/commands/datasources_and_workbooks/export_command.py @@ -38,25 +38,6 @@ def define_args(export_parser): help="View filter to apply to the view", ) - # TODO: ARGUMENT --COMPLETE - - @staticmethod - def get_content_url_for_workbook(url): - # check the size of list - separated_list = url.split("/") - reversed_list = separated_list[::-1] - return reversed_list[1] - - @staticmethod - def get_content_url_for_view(url): - # check the size of list - separated_list = url.split("/") - if len(separated_list) > 2: - print("error") - workbook_name = separated_list[0] - view_name = separated_list[1] - return DatasourcesAndWorkbooks.get_view_url_from_names(workbook_name, view_name) - """ Command to Export a view_name or workbook from Tableau Server and save it to a file. This command can also export just the data used for a view_name @@ -73,24 +54,25 @@ def run_command(args): if not view_content_url and not wb_content_url: Errors.exit_with_error(logger, _("export.errors.requires_workbook_view_param").format(ExportCommand)) - try: + logger.debug(args.pagelayout, args.pagesize, args.filename, args.width, args.height, args.filter) + try: if args.fullpdf: # it's a workbook workbook_item = ExportCommand.get_wb_by_content_url(logger, server, wb_content_url) - output = ExportCommand.download_wb_pdf(server, workbook_item) + output = ExportCommand.download_wb_pdf(server, workbook_item, args.url, logger) default_filename = "{}.pdf".format(workbook_item.name) elif args.pdf or args.png or args.csv: # it's a view view_item = ExportCommand.get_view_by_content_url(logger, server, view_content_url) if args.pdf: - output = ExportCommand.download_view_pdf(server, view_item) + output = ExportCommand.download_view_pdf(server, view_item, args.url, logger) default_filename = "{}.pdf".format(view_item.name) elif args.csv: - output = ExportCommand.download_csv(server, view_item) + output = ExportCommand.download_csv(server, view_item, args.url, logger) default_filename = "{}.csv".format(view_item.name) elif args.png: - output = ExportCommand.download_png(server, view_item) + output = ExportCommand.download_png(server, view_item, args.url, logger) default_filename = "{}.png".format(view_item.name) except Exception as e: @@ -98,32 +80,65 @@ def run_command(args): try: save_name = args.filename or default_filename - ExportCommand.save_to_file(logger, output, save_name) + if args.csv: + ExportCommand.save_to_data_file(logger, output, save_name) + else: + ExportCommand.save_to_file(logger, output, save_name) except Exception as e: Errors.exit_with_error(logger, "Error saving to file", e) @staticmethod - def download_wb_pdf(server, workbook_item): + def extract_filter_values_from_url_params(request_options: TSC.PDFRequestOptions, url, logger=None) -> None: + try: + # todo make logging better + logger = logger or log(ExportCommand.__class__.__name__, "DEBUG") + logger.debug(url) + + if "?" in url: + query = url.split("?")[1] + else: + return + + params = query.split("&") + logger.trace(params) + for value in params: + data_filter = value.split("=") + request_options.vf(data_filter[0], data_filter[1]) + except BaseException as e: + logger.error("Error building filter params", e) + ExportCommand.log_stack(logger) # type: ignore + + @staticmethod + def download_wb_pdf(server, workbook_item, url, logger): + logger.trace(url) pdf = TSC.PDFRequestOptions(maxage=1) + ExportCommand.extract_filter_values_from_url_params(pdf, url) server.workbooks.populate_pdf(workbook_item, pdf) return workbook_item.pdf @staticmethod - def download_view_pdf(server, view_item): + def download_view_pdf(server, view_item, url, logger): + logger.trace(url) pdf = TSC.PDFRequestOptions(maxage=1) + ExportCommand.extract_filter_values_from_url_params(pdf, url) + logger.trace(pdf.view_filters) server.views.populate_pdf(view_item, pdf) return view_item.pdf @staticmethod - def download_csv(server, view_item): + def download_csv(server, view_item, url, logger): + logger.trace(url) csv = TSC.CSVRequestOptions(maxage=1) + ExportCommand.extract_filter_values_from_url_params(csv, url) server.views.populate_csv(view_item, csv) return view_item.csv @staticmethod - def download_png(server, view_item): + def download_png(server, view_item, url, logger): + logger.trace(url) req_option_image = TSC.ImageRequestOptions(maxage=1) + ExportCommand.extract_filter_values_from_url_params(req_option_image, url) server.views.populate_image(view_item, req_option_image) return view_item.png @@ -132,7 +147,10 @@ def parse_export_url_to_workbook_and_view(logger, url): logger.info(_("export.status").format(url)) if " " in url: Errors.exit_with_error(logger, _("export.errors.white_space_workbook_view")) - # input should be workbook_name/view_name + if "?" in url: + url = url.split("?")[0] + # input should be workbook_name/view_name or /workbook_name/view_name + url = url.lstrip("/") # strip opening / if present if not url.find("/"): return None, None name_parts = url.split("/") @@ -142,9 +160,16 @@ def parse_export_url_to_workbook_and_view(logger, url): view = "{}/sheets/{}".format(workbook, name_parts[1]) return view, workbook + @staticmethod + def save_to_data_file(logger, output, filename): + logger.info(_("httputils.found_attachment").format(filename)) + with open(filename, "wb") as f: + f.writelines(output) + logger.info(_("export.success").format("", filename)) + @staticmethod def save_to_file(logger, output, filename): logger.info(_("httputils.found_attachment").format(filename)) with open(filename, "wb") as f: f.write(output) - logger.info(_("export.success").format(filename, "")) + logger.info(_("export.success").format("", filename)) diff --git a/tabcmd/execution/logger_config.py b/tabcmd/execution/logger_config.py index 1837e951..b478909e 100644 --- a/tabcmd/execution/logger_config.py +++ b/tabcmd/execution/logger_config.py @@ -29,4 +29,6 @@ def configure_log(name: str, logging_level_input: str): def log(file_name, logging_level): logger = configure_log(file_name, logging_level) + if not hasattr(logger, "trace"): + logger.trace = logger.debug return logger diff --git a/tests/commands/test_geturl_utils.py b/tests/commands/test_geturl_utils.py index 2430bf28..0bdb4555 100644 --- a/tests/commands/test_geturl_utils.py +++ b/tests/commands/test_geturl_utils.py @@ -1,11 +1,19 @@ import unittest +from typing import Iterator from unittest import mock + +import tableauserverclient + from tabcmd.commands.datasources_and_workbooks.get_url_command import * from tabcmd.commands.datasources_and_workbooks.export_command import * from tabcmd.commands.server import Server mock_logger = mock.MagicMock() +fake_item = mock.MagicMock(TSC.ViewItem) +fake_item.name = "fake-name" +fake_item.id = "fake-id" + class GeturlTests(unittest.TestCase): def test_evaluate_file_name_pdf(self): @@ -59,9 +67,79 @@ def test_view_name(self): """ +@mock.patch("tableauserverclient.ViewItem", fake_item) class ExportTests(unittest.TestCase): - def test_parse_export_url_to_workbook(self): + + mock_logger = mock.MagicMock("logger") + fake_item.csv = mock.MagicMock("bytes[]") + fake_item.pdf = mock.MagicMock("bytes") + fake_item.png = mock.MagicMock("bytes") + + def test_parse_export_url_to_workbook_and_view(self): wb_url = "wb-name/view-name" view, wb = ExportCommand.parse_export_url_to_workbook_and_view(mock_logger, wb_url) assert view == "wb-name/sheets/view-name" assert wb == "wb-name" + + def test_parse_export_url_to_workbook_and_view_with_start_slash(self): + wb_url = "/wb-name/view-name" + view, wb = ExportCommand.parse_export_url_to_workbook_and_view(mock_logger, wb_url) + assert view == "wb-name/sheets/view-name" + assert wb == "wb-name" + + def test_parse_export_url_to_workbook_and_view_bad_url(self): + wb_url = "wb-name/view-name/kitty" + view, wb = ExportCommand.parse_export_url_to_workbook_and_view(mock_logger, wb_url) + assert view is None + assert wb is None + + def test_extract_query_params(self): + url = "wb-name/view-name?param1=value1" + options = TSC.PDFRequestOptions() + assert options.view_filters is not None + assert len(options.view_filters) is 0 + ExportCommand.extract_filter_values_from_url_params(options, url) + assert len(options.view_filters) == 1 + assert options.view_filters[0] == ("param1", "value1") + + @mock.patch("tableauserverclient.Server") + def test_download_csv(self, mock_server): + mock_server.views = mock.MagicMock() + mock_server.views.csv = mock.MagicMock() + mock_view = tableauserverclient.ViewItem() + url = "wb-name/view-name?param1=value1" + ExportCommand.download_csv(mock_server, mock_view, url, mock_logger) + + @mock.patch("tableauserverclient.Server") + def test_download_image(self, mock_server): + mock_server.views = mock.MagicMock() + mock_server.views.png = mock.MagicMock() + mock_view = tableauserverclient.ViewItem() + url = "wb-name/view-name?param1=value1" + ExportCommand.download_png(mock_server, mock_view, url, mock_logger) + + @mock.patch("tableauserverclient.Server") + def test_download_view_pdf(self, mock_server): + mock_server.views = mock.MagicMock() + mock_server.views.pdf = mock.MagicMock() + mock_view = tableauserverclient.ViewItem() + url = "wb-name/view-name?param1=value1" + ExportCommand.download_view_pdf(mock_server, mock_view, url, mock_logger) + + @mock.patch("tableauserverclient.Server") + def test_download_wb_pdf(self, mock_server): + mock_server.workbooks = mock.MagicMock() + mock_server.workbooks.pdf = mock.MagicMock() + mock_view = tableauserverclient.ViewItem() + url = "wb-name/view-name?param1=value1" + ExportCommand.download_wb_pdf(mock_server, mock_view, url, mock_logger) + + def test_save_to_binary_file(self): + mock_content = bytes() + filename = "test_out.pdf" + ExportCommand.save_to_file(mock_logger, mock_content, filename) + + def test_save_to_data_file(self): + mock_content = mock.MagicMock() + filename = "test_out.csv" + ExportCommand.save_to_data_file(mock_logger, mock_content, filename) diff --git a/tests/commands/test_run_commands.py b/tests/commands/test_run_commands.py index 7e7e3d45..22b9dda0 100644 --- a/tests/commands/test_run_commands.py +++ b/tests/commands/test_run_commands.py @@ -103,6 +103,15 @@ def test_export(self, mock_session, mock_server): mock_args.fullpdf = True mock_args.filename = "filename.pdf" mock_args.url = "workbook-name/view-name" + mock_args.csv = None + mock_args.image = None + mock_args.pdf = None + mock_args.pagelayout = None + mock_args.pagesize = None + mock_args.size = None + mock_args.height = None + mock_args.width = None + mock_args.filter = None export_command.ExportCommand.run_command(mock_args) mock_session.assert_called() diff --git a/tests/e2e/online_tests.py b/tests/e2e/online_tests.py index cb391e29..73a0e5dc 100644 --- a/tests/e2e/online_tests.py +++ b/tests/e2e/online_tests.py @@ -313,7 +313,7 @@ def test_export_view(self): file = os.path.join("tests", "assets", OnlineCommandTest.TWBX_FILE_WITH_EXTRACT) self._publish_wb(file, name_on_server) command = "export" - friendly_name = name_on_server + "/" + OnlineCommandTest.TWBX_WITH_EXTRACT_SHEET + friendly_name = name_on_server + "/" + OnlineCommandTest.TWBX_WITH_EXTRACT_SHEET + "?param1=3" arguments = [command, friendly_name, "--pdf", "-f", "exported_view.pdf"] _test_command(arguments) From c8451dbe758f270159093f8c740559022bfb8806 Mon Sep 17 00:00:00 2001 From: Jac Date: Wed, 7 Sep 2022 23:05:28 -0700 Subject: [PATCH 17/37] Fix some e2e tests (#166) * Fix some e2e tests --- tabcmd/commands/group/delete_group_command.py | 2 +- tabcmd/commands/server.py | 25 +++--- .../user/delete_site_users_command.py | 2 +- tabcmd/commands/user/user_data.py | 31 +++++-- tests/e2e/online_tests.py | 82 +++++++++---------- 5 files changed, 75 insertions(+), 67 deletions(-) diff --git a/tabcmd/commands/group/delete_group_command.py b/tabcmd/commands/group/delete_group_command.py index 01a8a4b5..6d2741f1 100644 --- a/tabcmd/commands/group/delete_group_command.py +++ b/tabcmd/commands/group/delete_group_command.py @@ -27,7 +27,7 @@ def run_command(args): server = session.create_session(args) try: logger.info(_("tabcmd.find.group").format(args.name)) - group_id = Server.find_group_id(logger, server, args.name) + group_id = Server.find_group(logger, server, args.name).id logger.info(_("deletegroup.status").format(group_id)) server.groups.delete(group_id) logger.info(_("common.output.succeeded")) diff --git a/tabcmd/commands/server.py b/tabcmd/commands/server.py index 09fa965c..cf3700cf 100644 --- a/tabcmd/commands/server.py +++ b/tabcmd/commands/server.py @@ -39,18 +39,12 @@ def find_group(logger, server, group_name): Errors.exit_with_error(logger, exception=e) @staticmethod - def find_group_id(logger, server, group_name): - return Server.find_group(logger, server, group_name).id + def find_user(logger, server, username): + return Server.get_items_by_name(logger, server.users, username)[0] @staticmethod - def find_user_id(logger, server, username): - try: - return Server.get_items_by_name(logger, server.users, username)[0].id - except Exception as e: - Errors.exit_with_error(logger, exception=e) - - @staticmethod - def get_items_by_name(logger, item_endpoint, item_name: str, container: TSC.ProjectItem = None): + def get_items_by_name(logger, item_endpoint, item_name: str, container: TSC.ProjectItem = None) -> List: + # TODO: typing should reflect that this returns TSC.TableauItem and item_endpoint is of type TSC.QuerysetEndpoint[same] item_type = type(item_endpoint).__name__ item_log_name: str = "[" + item_type + "] " + item_name if container: @@ -58,10 +52,13 @@ def get_items_by_name(logger, item_endpoint, item_name: str, container: TSC.Proj logger.debug(_("export.status").format(item_log_name)) req_option = TSC.RequestOptions() req_option.filter.add(TSC.Filter(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Operator.Equals, item_name)) - all_items, pagination_item = item_endpoint.get(req_option) if all_items is None or all_items == []: - raise ValueError("[" + item_type + "] " + _("errors.xmlapi.not_found")) + raise TSC.ServerResponseError( + code=404, + summary=_("errors.xmlapi.not_found"), + detail=_("errors.xmlapi.not_found") + ": " + item_log_name, + ) if len(all_items) == 1: logger.debug("Exactly one result found") result = all_items @@ -120,7 +117,7 @@ def get_project_by_name_and_parent_path(logger, server, project_name: str, paren if not parent_path: if not project_name: project_name = "Default" - project: TSC.ProjectItem = Server.get_items_by_name(logger, server.projects, project_name, None) + project: TSC.ProjectItem = Server.get_items_by_name(logger, server.projects, project_name, None)[0] return project project_tree: List[str] = Server._parse_project_path_to_list(parent_path) @@ -129,7 +126,9 @@ def get_project_by_name_and_parent_path(logger, server, project_name: str, paren return project parent = Server._get_parent_project_from_tree(logger, server, project_tree) + logger.debug(parent) project = Server._get_project_by_name_and_parent(logger, server, project_name, parent) + logger.debug(project) if not project: Errors.exit_with_error(logger, message=_("publish.errors.server_resource_not_found")) return project diff --git a/tabcmd/commands/user/delete_site_users_command.py b/tabcmd/commands/user/delete_site_users_command.py index 7f46a311..aa749a5a 100644 --- a/tabcmd/commands/user/delete_site_users_command.py +++ b/tabcmd/commands/user/delete_site_users_command.py @@ -42,7 +42,7 @@ def run_command(args): for user_obj in user_obj_list: logger.info(_("importcsvsummary.line.processed").format(number_of_users_deleted)) try: - user_id = UserCommand.find_user_id(logger, server, user_obj.name) + user_id = UserCommand.find_user(logger, server, user_obj.name).id server.users.remove(user_id) logger.debug(_("tabcmd.result.success.delete_user").format(user_obj.name, user_id)) number_of_users_deleted += 1 diff --git a/tabcmd/commands/user/user_data.py b/tabcmd/commands/user/user_data.py index ed7082c4..f8c088f7 100644 --- a/tabcmd/commands/user/user_data.py +++ b/tabcmd/commands/user/user_data.py @@ -229,17 +229,25 @@ def act_on_users( logger.debug(_("importcsvsummary.line.processed").format(n_users_listed)) error_list = [] + line_no = 0 user_obj_list: List[TSC.UserItem] = UserCommand.get_users_from_file(args.users) logger.debug(_("tabcmd.result.success.parsed_users").format(len(user_obj_list))) for user_obj in user_obj_list: - username: str = user_obj.name or "unknown user" + line_no += 1 + if not user_obj.name: + number_of_errors += 1 + error_list.append(_("importcsvsummary.error.line").format(line_no, "No username", "")) + continue + try: - user_id: str = UserCommand.find_user_id(logger, server, username) + username: str = user_obj.name + user_id: str = UserCommand.find_user(logger, server, username).id logger.debug("{} user {} ({})".format(action_name, username, user_id)) except TSC.ServerResponseError as e: - Errors.check_common_error_codes_and_explain(logger, e) number_of_errors += 1 - error_list.append(e) + error_list.append( + _("importcsvsummary.error.line").format(line_no, username, "{}: {}".format(e.code, e.detail)) + ) logger.debug(_("tabcmd.result.failure.user").format(username)) continue @@ -248,11 +256,20 @@ def act_on_users( n_users_handled += 1 logger.info(_("tabcmd.result.success.user_actions").format(action_name, username, group)) except TSC.ServerResponseError as e: - Errors.check_common_error_codes_and_explain(logger, e) number_of_errors += 1 - error_list.append(e) + error_list.append( + _("importcsvsummary.error.line").format(line_no, username, "{}: {}".format(e.code, e.detail)) + ) logger.info(_("session.monitorjob.percent_complete").format(100)) logger.info(_("importcsvsummary.errors.count").format(number_of_errors)) if number_of_errors > 0: - logger.info(_("importcsvsummary.error.details").format(error_list)) + i = 0 + max_printing = 5 + logger.info(_("importcsvsummary.error.details")) + while i < number_of_errors and i < max_printing: + logger.info(error_list[i]) + i += 1 + if number_of_errors > max_printing: + logger.info(_("importcsvsummary.error.too_many_errors")) + logger.info(_("importcsvsummary.remainingerrors")) diff --git a/tests/e2e/online_tests.py b/tests/e2e/online_tests.py index 73a0e5dc..bede34d0 100644 --- a/tests/e2e/online_tests.py +++ b/tests/e2e/online_tests.py @@ -51,10 +51,9 @@ def setup_class(cls): def _create_project(self, project_name, parent_path=None): command = "createproject" arguments = [command, "--name", project_name] - if parent_path or parent_location: + if parent_path: arguments.append("--parent-project-path") - arguments.append(parent_path or parent_location) - print(arguments) + arguments.append(parent_path) _test_command(arguments) def _delete_project(self, project_name, parent_path=None): @@ -136,7 +135,7 @@ def test_login(self): @pytest.mark.order(1) def test_version(self): - command = "" + command = "-v" arguments = [command] _test_command(arguments) @@ -147,7 +146,7 @@ def test_help(self): _test_command(arguments) @pytest.mark.order(2) - def test_create_site_users(self): + def test_users_create_site_users(self): if not server_admin and not site_admin: pytest.skip("Must be server or site administrator to create site users") command = "createsiteusers" @@ -156,7 +155,7 @@ def test_create_site_users(self): _test_command(arguments) @pytest.mark.order(3) - def test_creategroup(self): + def test_group_creategroup(self): if not server_admin and not site_admin: pytest.skip("Must be server or site administrator to create groups") groupname = group_name @@ -165,7 +164,7 @@ def test_creategroup(self): _test_command(arguments) @pytest.mark.order(4) - def test_add_users_to_group(self): + def test_users_add_to_group(self): if not server_admin and not site_admin: pytest.skip("Must be server or site administrator to add to groups") @@ -176,7 +175,7 @@ def test_add_users_to_group(self): _test_command(arguments) @pytest.mark.order(5) - def test_remove_users_to_group(self): + def test_users_remove_from_group(self): if not server_admin and not site_admin: pytest.skip("Must be server or site administrator to remove from groups") @@ -187,7 +186,7 @@ def test_remove_users_to_group(self): _test_command(arguments) @pytest.mark.order(6) - def test_deletegroup(self): + def test_group_deletegroup(self): if not server_admin and not site_admin: pytest.skip("Must be server or site administrator to delete groups") @@ -198,9 +197,12 @@ def test_deletegroup(self): @pytest.mark.order(8) def test_create_projects(self): - if not project_admin: pytest.skip("Must be project administrator to create projects") + + # project 1 + self._create_project(parent_location) + time.sleep(indexing_sleep_time) # project 1 self._create_project(project_name) time.sleep(indexing_sleep_time) @@ -216,58 +218,54 @@ def test_create_projects(self): def test_list_projects(self): self._list("projects") - @pytest.mark.order(9) + """ + @pytest.mark.order(9) + def test_publish_samples(self): + self._publish_samples(project_name) + """ + + @pytest.mark.order(10) def test_delete_projects(self): if not project_admin: pytest.skip("Must be project administrator to create projects") self._delete_project("project_name_2", project_name) # project 2 self._delete_project(project_name) - @pytest.mark.order(9) - def test_publish_samples(self): - self._publish_samples(project_name) - @pytest.mark.order(10) - def test_publish(self): + def test_wb_publish(self): name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME file = os.path.join("tests", "assets", OnlineCommandTest.TWBX_FILE_WITH_EXTRACT) self._publish_wb(file, name_on_server) @pytest.mark.order(10) - def test__get_wb(self): - wb_name_on_server = OnlineCommandTest.TWBX_WITHOUT_EXTRACT_NAME - self._get_workbook(wb_name_on_server + ".twbx") + def test_wb_get(self): + self._get_workbook(OnlineCommandTest.TWBX_WITH_EXTRACT_NAME + ".twbx") @pytest.mark.order(10) - def test__get_wb(self): + def test_view_get_pdf(self): wb_name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME - self._get_workbook(wb_name_on_server + ".twbx") - - @pytest.mark.order(10) - def test__get_view(self): - wb_name_on_server = OnlineCommandTest.TWBX_WITHOUT_EXTRACT_NAME - sheet_name = OnlineCommandTest.TWBX_WITHOUT_EXTRACT_SHEET + sheet_name = OnlineCommandTest.TWBX_WITH_EXTRACT_SHEET self._get_view(wb_name_on_server, sheet_name + ".pdf") @pytest.mark.order(10) - def test__get_view_csv(self): + def test_view_get_csv(self): wb_name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME sheet_name = OnlineCommandTest.TWBX_WITH_EXTRACT_SHEET self._get_view(wb_name_on_server, sheet_name + ".csv") @pytest.mark.order(10) - def test__get_view_png(self): + def test_view_get_png(self): wb_name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME sheet_name = OnlineCommandTest.TWBX_WITH_EXTRACT_SHEET self._get_view(wb_name_on_server, sheet_name + ".png") @pytest.mark.order(11) - def test__delete_wb(self): + def test_wb_delete(self): name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME self._delete_wb(name_on_server) @pytest.mark.order(12) - def test_delete_extract(self): + def test_extract_delete(self): # fails because the extract has a bad data connection :/ name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME file = os.path.join("tests", "assets", OnlineCommandTest.TWBX_FILE_WITH_EXTRACT) @@ -275,30 +273,24 @@ def test_delete_extract(self): self._delete_extract(name_on_server) @pytest.mark.order(13) - def test_create_extract(self): + def test_extract_create(self): # Fails because it 'already has an extract' :/ name_on_server = OnlineCommandTest.TWBX_WITHOUT_EXTRACT_NAME self._create_extract(name_on_server) @pytest.mark.order(14) - def test_refresh_extract(self): + def test_extract_refresh(self): # must be a datasource owned by the test user + name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME + file = os.path.join("tests", "assets", OnlineCommandTest.TWBX_FILE_WITH_EXTRACT) + self._publish_wb(file, name_on_server) + name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME self._refresh_extract(name_on_server) self._delete_wb(name_on_server) - def test_version(self): - _test_command(["-v"]) - - def test_help(self): - _test_command(["help"]) - - # this just gets in the way :( - # def test_logout(self): - # _test_command(["logout"]) - @pytest.mark.order(15) - def test_export_wb(self): + def test_export_wb_pdf(self): name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME file = os.path.join("tests", "assets", OnlineCommandTest.TWBX_FILE_WITH_EXTRACT) self._publish_wb(file, name_on_server) @@ -308,7 +300,7 @@ def test_export_wb(self): _test_command(arguments) @pytest.mark.order(15) - def test_export_view(self): + def test_export_view_pdf(self): name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME file = os.path.join("tests", "assets", OnlineCommandTest.TWBX_FILE_WITH_EXTRACT) self._publish_wb(file, name_on_server) @@ -318,7 +310,7 @@ def test_export_view(self): _test_command(arguments) @pytest.mark.order(16) - def test_delete_site_users(self): + def test_users_delete_site_users(self): if not server_admin and not site_admin: pytest.skip("Must be server or site administrator to delete site users") From ed3efeadd31cf72c8ed29ca4a52a84b48a3fb33d Mon Sep 17 00:00:00 2001 From: Jac Date: Wed, 7 Sep 2022 23:06:57 -0700 Subject: [PATCH 18/37] Trace logging and bug fix (#165) * Add trace logging details and error stack * handle url input more robustly Co-authored-by: Brian Cantoni --- tabcmd/commands/auth/session.py | 5 +-- tabcmd/commands/constants.py | 22 ++++++++++ .../get_url_command.py | 10 ++++- tabcmd/execution/__init__.py | 3 ++ tabcmd/execution/logger_config.py | 40 ++++++++++++++++--- tabcmd/execution/parent_parser.py | 2 +- tabcmd/execution/tabcmd_controller.py | 7 +++- 7 files changed, 76 insertions(+), 13 deletions(-) diff --git a/tabcmd/commands/auth/session.py b/tabcmd/commands/auth/session.py index ba98fd2d..866efbc8 100644 --- a/tabcmd/commands/auth/session.py +++ b/tabcmd/commands/auth/session.py @@ -4,7 +4,6 @@ import requests import tableauserverclient as TSC -import tableauserverclient.server.endpoint.exceptions from urllib3.exceptions import InsecureRequestWarning from tabcmd.commands.constants import Errors @@ -45,8 +44,8 @@ def __init__(self): self.timeout = None self.logging_level = "info" - self.logger = log(__class__.__name__, self.logging_level) self._read_from_json() + self.logger = log(__name__, self.logging_level) # instantiate here mostly for tests self.tableau_server = None # this one is an object that doesn't get persisted in the file # called before we connect to the server @@ -55,7 +54,6 @@ def _update_session_data(self, args): # user id and site id are never passed in as args # last_login_using and tableau_server are internal data # self.command = args.??? - # TODO: if server/username/token are changed, clear others self.username = args.username or self.username self.site_name = args.site_name or self.site_name or "" if self.site_name == "default": @@ -216,6 +214,7 @@ def create_session(self, args): self._read_existing_state() self._update_session_data(args) self.logging_level = args.logging_level or self.logging_level + self.logger = log(__name__, self.logging_level) credentials = None if args.password: diff --git a/tabcmd/commands/constants.py b/tabcmd/commands/constants.py index fe642bbb..0eaa9b3c 100644 --- a/tabcmd/commands/constants.py +++ b/tabcmd/commands/constants.py @@ -1,3 +1,4 @@ +import inspect import sys from tabcmd.execution.localize import _ @@ -29,9 +30,30 @@ def is_login_error(error): if hasattr(error, "code"): return error.code == Constants.login_error + # https://gist.github.com/FredLoney/5454553 + @staticmethod + def log_stack(logger): + try: + """The log header message formatter.""" + HEADER_FMT = "Printing Call Stack at %s::%s" + """The log stack message formatter.""" + STACK_FMT = "%s, line %d in function %s." + stack = inspect.stack() + here = stack[0] + file, line, func = here[1:4] + start = 0 + n_lines = 5 + logger.trace(HEADER_FMT % (file, func)) + for frame in stack[start + 1 : n_lines]: + file, line, func = frame[1:4] + logger.trace(STACK_FMT % (file, line, func)) + except BaseException as e: + logger.info("Error printing stack trace:", e) + @staticmethod def exit_with_error(logger, message=None, exception=None): try: + Errors.log_stack(logger) if message and not exception: logger.error(message) if exception: diff --git a/tabcmd/commands/datasources_and_workbooks/get_url_command.py b/tabcmd/commands/datasources_and_workbooks/get_url_command.py index 5b23dc4e..1e6010d9 100644 --- a/tabcmd/commands/datasources_and_workbooks/get_url_command.py +++ b/tabcmd/commands/datasources_and_workbooks/get_url_command.py @@ -37,6 +37,10 @@ def run_command(args): if " " in args.url: Errors.exit_with_error(logger, _("export.errors.white_space_workbook_view")) + if not args.url.startswith("/"): + args.url = "/" + args.url + logger.trace("helpfully fix format of url: " + args.url) + file_type = GetUrl.get_file_type_from_filename(logger, args.filename, args.url) content_type = GetUrl.evaluate_content_type(logger, args.url) if content_type == "workbook": @@ -65,9 +69,11 @@ def evaluate_content_type(logger, url): elif url.find("/workbooks/") == 0: return "workbook" else: - Errors.exit_with_error( - logger, message=_("export.errors.requires_workbook_view_param").format(__class__.__name__) + view_example = "/views//." + message = "{} [{}]".format( + _("export.errors.requires_workbook_view_param").format(__class__.__name__), view_example ) + Errors.exit_with_error(logger, message) @staticmethod def get_file_type_from_filename(logger, file_name, url): diff --git a/tabcmd/execution/__init__.py b/tabcmd/execution/__init__.py index e69de29b..26cf34a5 100644 --- a/tabcmd/execution/__init__.py +++ b/tabcmd/execution/__init__.py @@ -0,0 +1,3 @@ +from tabcmd.execution.logger_config import add_trace_level + +add_trace_level() diff --git a/tabcmd/execution/logger_config.py b/tabcmd/execution/logger_config.py index b478909e..215b3ee6 100644 --- a/tabcmd/execution/logger_config.py +++ b/tabcmd/execution/logger_config.py @@ -4,14 +4,42 @@ path = os.path.dirname(os.path.abspath(__file__)) FORMATS = { - logging.ERROR: "ERROR: %(name)-10s: %(lineno)d: %(message)s", - logging.WARN: "WARN: %(message)s", - logging.DEBUG: "DEBUG: %(name)-10s: %(lineno)d: %(message)-10s", - logging.INFO: "%(message)s", - "TRACE": "TRACE: %(asctime)-12s %(name)-10s: %(lineno)d: %(message)-10s", - "DEFAULT": "%(message)s", + logging.ERROR: "%(asctime)s %(levelname)-5s:(%(name)-10s %(filename)-10s: %(lineno)d): %(message)-30s", + logging.WARN: "%(asctime)s %(levelname)s : (%(name)-10s %(filename)-10s: %(lineno)d): %(message)-30s", + logging.INFO: "%(message)-30s", + logging.DEBUG: "%(asctime)s %(levelname)s : (%(name)-10s %(filename)-10s: %(lineno)d): %(message)-30s", } +# https://stackoverflow.com/questions/2183233/how-to-add-a-custom-loglevel-to-pythons-logging-facility +def add_log_level(level_name, level_num, method_name=None): + if not method_name: + method_name = level_name.lower() + + if hasattr(logging, level_name): + raise AttributeError("{} already defined in logging module".format(level_name)) + if hasattr(logging, method_name): + raise AttributeError("{} already defined in logging module".format(method_name)) + if hasattr(logging.getLoggerClass(), method_name): + raise AttributeError("{} already defined in logger class".format(method_name)) + + def logForLevel(self, message, *args, **kwargs): + if self.isEnabledFor(level_num): + self._log(level_num, message, args, **kwargs) + + def logToRoot(message, *args, **kwargs): + logging.log(level_num, message, *args, **kwargs) + + logging.addLevelName(level_num, level_name) + setattr(logging, level_name, level_num) + setattr(logging.getLoggerClass(), method_name, logForLevel) + setattr(logging, method_name, logToRoot) + + +def add_trace_level(): + trace_level: int = logging.DEBUG - 5 + add_log_level("TRACE", trace_level) + FORMATS[trace_level] = FORMATS[logging.ERROR] + def configure_log(name: str, logging_level_input: str): """function for logging statements to console and logfile""" diff --git a/tabcmd/execution/parent_parser.py b/tabcmd/execution/parent_parser.py index 82261f28..cb1a68d0 100644 --- a/tabcmd/execution/parent_parser.py +++ b/tabcmd/execution/parent_parser.py @@ -65,7 +65,7 @@ def parent_parser_with_global_options(self): parser.add_argument( "-l", "--logging-level", - choices=["DEBUG", "INFO", "ERROR"], + 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.", diff --git a/tabcmd/execution/tabcmd_controller.py b/tabcmd/execution/tabcmd_controller.py index f54f6b02..0dee0ad0 100644 --- a/tabcmd/execution/tabcmd_controller.py +++ b/tabcmd/execution/tabcmd_controller.py @@ -25,9 +25,14 @@ def run(parser, user_input=None): sys.exit(0) user_input = user_input or sys.argv[1:] namespace = parser.parse_args(user_input) + if namespace.logging_level: + print("logging:", namespace.logging_level) logger = log(__name__, namespace.logging_level or logging.INFO) - # logger.debug(namespace) + if namespace.password: + logger.trace(namespace.func) + else: + logger.trace(namespace) if namespace.language: set_client_locale(namespace.language, logger) From a12ee64d85e71a94aeaef97075cb308b00f4bfc2 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Thu, 8 Sep 2022 00:20:30 -0700 Subject: [PATCH 19/37] Merge branch 'main' into development --- .github/workflows/run-e2-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-e2-tests.yml b/.github/workflows/run-e2-tests.yml index 371577f5..a3601d94 100644 --- a/.github/workflows/run-e2-tests.yml +++ b/.github/workflows/run-e2-tests.yml @@ -1,4 +1,4 @@ -name: Python tests +name: e2e tests on: workflow_dispatch: @@ -42,4 +42,4 @@ jobs: - name: Run e2e tests run: | python -m tabcmd login --server "${{ github.event.inputs.server }}" --site "${{ github.event.inputs.site }}" --token-name "${{ github.event.inputs.patname }}" --token-value "${{ github.event.inputs.pat }}" - pytest -q tests\e2e\online_tests.py -r pfE + pytest -q tests/e2e/online_tests.py -r pfE From 5eb3d7c4e68bc4c7a01ef108afdb1f10d16aef27 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Thu, 8 Sep 2022 00:21:39 -0700 Subject: [PATCH 20/37] Update run-e2-tests.yml remove 3.7 and 3.8 to get past a typing bug in those versions --- .github/workflows/run-e2-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-e2-tests.yml b/.github/workflows/run-e2-tests.yml index a3601d94..26f2222c 100644 --- a/.github/workflows/run-e2-tests.yml +++ b/.github/workflows/run-e2-tests.yml @@ -18,7 +18,7 @@ jobs: fail-fast: true matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ['3.7', '3.8', '3.9', '3.10', '3'] + python-version: ['3.9', '3.10', '3'] runs-on: ${{ matrix.os }} From 9aea40f2b9f81b1885ec45953c5ab1e36aac5ee2 Mon Sep 17 00:00:00 2001 From: Jac Date: Mon, 12 Sep 2022 16:05:30 -0700 Subject: [PATCH 21/37] Implement command line and url parameters for get/export (#175) * Get-Url and Export use same url parameters * some logging cleanup * Fixes https://github.com/tableau/tabcmd/issues/173 * Partial fix for https://github.com/tableau/tabcmd/issues/169 * refactor to re-use option and parameter parsing, save_to_file methods --- tabcmd/commands/auth/session.py | 4 +- .../datasources_and_workbooks_command.py | 75 ++++++++++- .../export_command.py | 122 ++++++++---------- .../get_url_command.py | 89 ++++++------- tabcmd/commands/user/user_data.py | 1 - tabcmd/execution/logger_config.py | 5 +- tests/commands/test_geturl_utils.py | 79 +++++++++--- tests/e2e/online_tests.py | 6 +- 8 files changed, 243 insertions(+), 138 deletions(-) diff --git a/tabcmd/commands/auth/session.py b/tabcmd/commands/auth/session.py index 866efbc8..a380ab73 100644 --- a/tabcmd/commands/auth/session.py +++ b/tabcmd/commands/auth/session.py @@ -45,7 +45,7 @@ def __init__(self): self.logging_level = "info" self._read_from_json() - self.logger = log(__name__, self.logging_level) # instantiate here mostly for tests + self.logger = log(__class__.__name__, self.logging_level) # instantiate here mostly for tests self.tableau_server = None # this one is an object that doesn't get persisted in the file # called before we connect to the server @@ -214,7 +214,7 @@ def create_session(self, args): self._read_existing_state() self._update_session_data(args) self.logging_level = args.logging_level or self.logging_level - self.logger = log(__name__, self.logging_level) + self.logger = log(__class__.__name__, self.logging_level) credentials = None if args.password: diff --git a/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py b/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py index af54d65a..86536550 100644 --- a/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py +++ b/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py @@ -25,7 +25,7 @@ def get_view_by_content_url(logger, server, view_content_url) -> TSC.ViewItem: req_option.filter.add(TSC.Filter("contentUrl", TSC.RequestOptions.Operator.Equals, view_content_url)) matching_views, paging = server.views.get(req_option) except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response").format("")) + Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response").format(e)) if len(matching_views) < 1: Errors.exit_with_error(logger, message=_("errors.xmlapi.not_found")) return matching_views[0] @@ -42,3 +42,76 @@ def get_wb_by_content_url(logger, server, workbook_content_url) -> TSC.WorkbookI if len(matching_workbooks) < 1: Errors.exit_with_error(logger, message=_("dataalerts.failure.error.workbookNotFound")) return matching_workbooks[0] + + @staticmethod + def apply_values_from_url_params(request_options: TSC.PDFRequestOptions, url, logger) -> None: + # should be able to replace this with request_options._append_view_filters(params) + logger.debug(url) + try: + if "?" in url: + query = url.split("?")[1] + logger.trace("Query parameters: {}".format(query)) + else: + logger.debug("No query parameters present in url") + return + + params = query.split("&") + logger.trace(params) + for value in params: + if value.startswith(":"): + DatasourcesAndWorkbooks.apply_option_value(request_options, value, logger) + else: # it must be a filter + DatasourcesAndWorkbooks.apply_filter_value(request_options, value, logger) + + except BaseException as e: + logger.warn("Error building filter params", e) + # ExportCommand.log_stack(logger) # type: ignore + + @staticmethod + def apply_filter_value(request_options: TSC.PDFRequestOptions, value: str, logger) -> None: + # todo: do we need to strip Parameters.x -> x? + logger.trace("handling filter param {}".format(value)) + data_filter = value.split("=") + request_options.vf(data_filter[0], data_filter[1]) + + @staticmethod + def apply_option_value(request_options: TSC.PDFRequestOptions, value: str, logger) -> None: + logger.trace("handling url option {}".format(value)) + setting = value.split("=") + if ":iid" == setting[0]: + logger.debug(":iid value ignored in url") + elif ":refresh" == setting[0] and DatasourcesAndWorkbooks.is_truthy(setting[1]): + # mypy is worried that this is readonly + request_options.max_age = 0 # type:ignore + logger.debug("Set max age to {} from {}".format(request_options.max_age, value)) + elif ":size" == setting[0]: + height, width = setting[1].split(",") + logger.warn("Height/weight parameters not yet implemented ({})".format(value)) + else: + logger.debug("Parameter[s] not recognized: {}".format(value)) + + @staticmethod + def is_truthy(value: str): + return value.lower() in ["yes", "y", "1", "true"] + + @staticmethod + def apply_png_options(request_options: TSC.ImageRequestOptions, args, logger): + if args.height or args.width: + # only applicable for png + logger.warn("Height/width arguments not yet implemented in export") + if args.image_resolution: + request_options.image_resolution = args.image_resolution + + @staticmethod + def save_to_data_file(logger, output, filename): + logger.info(_("httputils.found_attachment").format(filename)) + with open(filename, "wb") as f: + f.writelines(output) + logger.info(_("export.success").format("", filename)) + + @staticmethod + def save_to_file(logger, output, filename): + logger.info(_("httputils.found_attachment").format(filename)) + with open(filename, "wb") as f: + f.write(output) + logger.info(_("export.success").format("", filename)) diff --git a/tabcmd/commands/datasources_and_workbooks/export_command.py b/tabcmd/commands/datasources_and_workbooks/export_command.py index f05a88ce..2e714543 100644 --- a/tabcmd/commands/datasources_and_workbooks/export_command.py +++ b/tabcmd/commands/datasources_and_workbooks/export_command.py @@ -28,12 +28,17 @@ def define_args(export_parser): help="page orientation (landscape or portrait) of the exported PDF", ) export_parser.add_argument("--pagesize", default="letter", help="Set the page size of the exported PDF") - export_parser.add_argument("--width", default=800, help="Set the width in pixels. Default is 800 px") + export_parser.add_argument( + "--image-resolution", default="High", help="Set the image resolution of the exported image" + ) + + export_parser.add_argument( + "--width", default=800, help="Set the width of the image in pixels. Default is 800 px" + ) export_parser.add_argument("--filename", "-f", help="filename to store the exported data") export_parser.add_argument("--height", default=600, help=_("export.options.height")) export_parser.add_argument( "--filter", - "-vf", metavar="COLUMN:VALUE", help="View filter to apply to the view", ) @@ -52,29 +57,29 @@ def run_command(args): view_content_url, wb_content_url = ExportCommand.parse_export_url_to_workbook_and_view(logger, args.url) logger.debug([view_content_url, wb_content_url]) if not view_content_url and not wb_content_url: - Errors.exit_with_error(logger, _("export.errors.requires_workbook_view_param").format(ExportCommand)) - - logger.debug(args.pagelayout, args.pagesize, args.filename, args.width, args.height, args.filter) + view_example = "/workbook_name/view_name" + message = "{} [{}]".format( + _("export.errors.requires_workbook_view_param").format(__class__.__name__), view_example + ) + Errors.exit_with_error(logger, message) try: if args.fullpdf: # it's a workbook workbook_item = ExportCommand.get_wb_by_content_url(logger, server, wb_content_url) - output = ExportCommand.download_wb_pdf(server, workbook_item, args.url, logger) - + output = ExportCommand.download_wb_pdf(server, workbook_item, args, logger) default_filename = "{}.pdf".format(workbook_item.name) elif args.pdf or args.png or args.csv: # it's a view view_item = ExportCommand.get_view_by_content_url(logger, server, view_content_url) if args.pdf: - output = ExportCommand.download_view_pdf(server, view_item, args.url, logger) + output = ExportCommand.download_view_pdf(server, view_item, args, logger) default_filename = "{}.pdf".format(view_item.name) elif args.csv: - output = ExportCommand.download_csv(server, view_item, args.url, logger) + output = ExportCommand.download_csv(server, view_item, args, logger) default_filename = "{}.csv".format(view_item.name) elif args.png: - output = ExportCommand.download_png(server, view_item, args.url, logger) - + output = ExportCommand.download_png(server, view_item, args, logger) default_filename = "{}.png".format(view_item.name) except Exception as e: @@ -87,63 +92,62 @@ def run_command(args): else: ExportCommand.save_to_file(logger, output, save_name) - except Exception as e: Errors.exit_with_error(logger, "Error saving to file", e) @staticmethod - def extract_filter_values_from_url_params(request_options: TSC.PDFRequestOptions, url, logger=None) -> None: - try: - # todo make logging better - logger = logger or log(ExportCommand.__class__.__name__, "DEBUG") - logger.debug(url) - - if "?" in url: - query = url.split("?")[1] - else: - return - - params = query.split("&") - logger.trace(params) + def apply_values_from_args(request_options: TSC.PDFRequestOptions, args, logger=None) -> None: + logger.debug( + "Args: {}, {}, {}, {}, {}".format(args.pagelayout, args.pagesize, args.width, args.height, args.filter) + ) + if args.pagelayout: + request_options.orientation = args.pagelayout + if args.pagesize: + request_options.page_type = args.pagesize + if args.filter: + params = args.filter.split("&") for value in params: - data_filter = value.split("=") - request_options.vf(data_filter[0], data_filter[1]) - except BaseException as e: - logger.error("Error building filter params", e) - ExportCommand.log_stack(logger) # type: ignore + ExportCommand.apply_filter_value(request_options, value, logger) @staticmethod - def download_wb_pdf(server, workbook_item, url, logger): - logger.trace(url) - pdf = TSC.PDFRequestOptions(maxage=1) - ExportCommand.extract_filter_values_from_url_params(pdf, url) - - server.workbooks.populate_pdf(workbook_item, pdf) + def download_wb_pdf(server, workbook_item, args, logger): + logger.trace(args.url) + pdf_options = TSC.PDFRequestOptions(maxage=1) + ExportCommand.apply_values_from_url_params(pdf_options, args.url, logger) + ExportCommand.apply_values_from_args(pdf_options, args, logger) + logger.trace(pdf_options) + server.workbooks.populate_pdf(workbook_item, pdf_options) return workbook_item.pdf @staticmethod - def download_view_pdf(server, view_item, url, logger): - logger.trace(url) - pdf = TSC.PDFRequestOptions(maxage=1) - ExportCommand.extract_filter_values_from_url_params(pdf, url) - logger.trace(pdf.view_filters) - server.views.populate_pdf(view_item, pdf) + def download_view_pdf(server, view_item, args, logger): + logger.trace(args.url) + pdf_options = TSC.PDFRequestOptions(maxage=1) + ExportCommand.apply_values_from_url_params(pdf_options, args.url, logger) + ExportCommand.apply_values_from_args(pdf_options, args, logger) + logger.trace(pdf_options) + server.views.populate_pdf(view_item, pdf_options) return view_item.pdf @staticmethod - def download_csv(server, view_item, url, logger): - logger.trace(url) - csv = TSC.CSVRequestOptions(maxage=1) - ExportCommand.extract_filter_values_from_url_params(csv, url) - server.views.populate_csv(view_item, csv) + def download_csv(server, view_item, args, logger): + logger.trace(args.url) + csv_options = TSC.CSVRequestOptions(maxage=1) + ExportCommand.apply_values_from_url_params(csv_options, args.url, logger) + ExportCommand.apply_values_from_args(csv_options, args, logger) + logger.trace(csv_options) + server.views.populate_csv(view_item, csv_options) return view_item.csv @staticmethod - def download_png(server, view_item, url, logger): - logger.trace(url) - req_option_image = TSC.ImageRequestOptions(maxage=1) - ExportCommand.extract_filter_values_from_url_params(req_option_image, url) - server.views.populate_image(view_item, req_option_image) + def download_png(server, view_item, args, logger): + logger.trace(args.url) + image_options = TSC.ImageRequestOptions(maxage=1) + ExportCommand.apply_values_from_url_params(image_options, args.url, logger) + ExportCommand.apply_values_from_args(image_options, args, logger) + DatasourcesAndWorkbooks.apply_png_options(image_options, args, logger) + logger.trace(image_options) + server.views.populate_image(view_item, image_options) return view_item.png @staticmethod @@ -163,17 +167,3 @@ def parse_export_url_to_workbook_and_view(logger, url): workbook = name_parts[0] view = "{}/sheets/{}".format(workbook, name_parts[1]) return view, workbook - - @staticmethod - def save_to_data_file(logger, output, filename): - logger.info(_("httputils.found_attachment").format(filename)) - with open(filename, "wb") as f: - f.writelines(output) - logger.info(_("export.success").format("", filename)) - - @staticmethod - def save_to_file(logger, output, filename): - logger.info(_("httputils.found_attachment").format(filename)) - with open(filename, "wb") as f: - f.write(output) - logger.info(_("export.success").format("", filename)) diff --git a/tabcmd/commands/datasources_and_workbooks/get_url_command.py b/tabcmd/commands/datasources_and_workbooks/get_url_command.py index 4e1ba4b8..96d9cee1 100644 --- a/tabcmd/commands/datasources_and_workbooks/get_url_command.py +++ b/tabcmd/commands/datasources_and_workbooks/get_url_command.py @@ -37,48 +37,49 @@ def run_command(args): if " " in args.url: Errors.exit_with_error(logger, _("export.errors.white_space_workbook_view")) - if not args.url.startswith("/"): - args.url = "/" + args.url - logger.trace("helpfully fix format of url: " + args.url) + url = args.url.lstrip("/") # strip opening / if present - file_type = GetUrl.get_file_type_from_filename(logger, args.filename, args.url) - content_type = GetUrl.evaluate_content_type(logger, args.url) + file_type = GetUrl.get_file_type_from_filename(logger, args.filename, url) + content_type = GetUrl.evaluate_content_type(logger, url) if content_type == "workbook": - if file_type == "twbx" or file_type == "twb": - GetUrl.generate_twb(logger, server, args, file_type) + if file_type in ["twbx", "twb"]: + GetUrl.generate_twb(logger, server, args, file_type, url) else: Errors.exit_with_error( logger, message=_("publish.errors.mutually_exclusive_option").format("twb", "twbx") ) else: # content type = view + view_url = GetUrl.get_view_url(url, logger) if file_type == "pdf": - GetUrl.generate_pdf(logger, server, args) + GetUrl.generate_pdf(logger, server, args, view_url) elif file_type == "png": - GetUrl.generate_png(logger, server, args) + GetUrl.generate_png(logger, server, args, view_url) elif file_type == "csv": - GetUrl.generate_csv(logger, server, args) + GetUrl.generate_csv(logger, server, args, view_url) else: Errors.exit_with_error(logger, message=_("tabcmd.get.extension.not_found")) @staticmethod def evaluate_content_type(logger, url): - # specify a view to get using "/views//." - # specify a workbook to get using "/workbooks/.". - if url.find("/views/") == 0: + # specify a view to get using "views//." + # specify a workbook to get using "workbooks/.". + if url.startswith("views/"): return "view" - elif url.find("/workbooks/") == 0: + elif url.startswith("workbooks/"): return "workbook" else: - view_example = "/views//." - message = "{} [{}]".format( - _("export.errors.requires_workbook_view_param").format(__class__.__name__), view_example - ) - Errors.exit_with_error(logger, message) + GetUrl.explain_expected_url(logger) + @staticmethod + def explain_expected_url(logger): + view_example = "/views//." + message = "{} [{}]".format( + _("export.errors.requires_workbook_view_param").format(__class__.__name__), view_example + ) + Errors.exit_with_error(logger, message) @staticmethod def get_file_type_from_filename(logger, file_name, url): - type_of_file = None file_name = file_name or url logger.debug(_("get.options.file") + ": {}".format(file_name)) type_of_file = GetUrl.get_file_extension(file_name) @@ -114,24 +115,24 @@ def get_name_without_possible_extension(filename): return filename @staticmethod - def get_workbook_name(logger, url): # /workbooks/wb-name" -> "wb-name" + def get_workbook_name(url, logger): # workbooks/wb-name" -> "wb-name" name_parts = url.split("/") - if len(name_parts) != 3: - raise ValueError(_("export.errors.requires_workbook_view_param").format(GetUrl.name)) + if len(name_parts) != 2: + GetUrl.explain_expected_url(logger) workbook_name = name_parts[::-1][0] # last part workbook_name = GetUrl.strip_query_params(workbook_name) workbook_name = GetUrl.get_name_without_possible_extension(workbook_name) return workbook_name @staticmethod - def get_view_url(url): # "/views/wb-name/view-name" -> wb-name/sheets/view-name - name_parts = url.split("/") # ['', 'views', 'wb-name', 'view-name'] - if len(name_parts) != 4: - raise ValueError(_("export.errors.requires_workbook_view_param").format(GetUrl.name)) + def get_view_url(url, logger): # "views/wb-name/view-name" -> wb-name/sheets/view-name + name_parts = url.split("/") # ['views', 'wb-name', 'view-name'] + if len(name_parts) != 3: + GetUrl.explain_expected_url(logger) + workbook_name = name_parts[1] view_name = name_parts[::-1][0] view_name = GetUrl.strip_query_params(view_name) view_name = GetUrl.get_name_without_possible_extension(view_name) - workbook_name = name_parts[2] return DatasourcesAndWorkbooks.get_view_url_from_names(workbook_name, view_name) @staticmethod @@ -141,55 +142,49 @@ def filename_from_args(file_argument, item_name, filetype): return file_argument @staticmethod - def generate_pdf(logger, server, args): - view_url = GetUrl.get_view_url(args.url) + def generate_pdf(logger, server, args, view_url): try: view_item: TSC.ViewItem = GetUrl.get_view_by_content_url(logger, server, view_url) logger.debug(_("content_type.view") + ": {}".format(view_item.name)) req_option_pdf = TSC.PDFRequestOptions(maxage=1) + DatasourcesAndWorkbooks.apply_values_from_url_params(req_option_pdf, args.url, logger) server.views.populate_pdf(view_item, req_option_pdf) filename = GetUrl.filename_from_args(args.filename, view_item.name, "pdf") - with open(filename, "wb") as f: - f.write(view_item.pdf) - logger.info(_("export.success").format(view_item.name, filename)) + DatasourcesAndWorkbooks.save_to_file(logger, view_item.pdf, filename) except TSC.ServerResponseError as e: Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) @staticmethod - def generate_png(logger, server, args): - view = GetUrl.get_view_url(args.url) + def generate_png(logger, server, args, view_url): try: - view_item: TSC.ViewItem = GetUrl.get_view_by_content_url(logger, server, view) + view_item: TSC.ViewItem = GetUrl.get_view_by_content_url(logger, server, view_url) logger.debug(_("content_type.view") + ": {}".format(view_item.name)) - req_option_csv = TSC.CSVRequestOptions(maxage=1) # same as png + req_option_csv = TSC.ImageRequestOptions(maxage=1) + DatasourcesAndWorkbooks.apply_values_from_url_params(req_option_csv, args.url, logger) server.views.populate_image(view_item, req_option_csv) filename = GetUrl.filename_from_args(args.filename, view_item.name, "png") - with open(filename, "wb") as f: - f.write(view_item.image) - logger.info(_("export.success").format(view_item.name, filename)) + DatasourcesAndWorkbooks.save_to_file(logger, view_item.image, filename) except TSC.ServerResponseError as e: Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) @staticmethod - def generate_csv(logger, server, args): - view_url = GetUrl.get_view_url(args.url) + def generate_csv(logger, server, args, view_url): try: view_item: TSC.ViewItem = GetUrl.get_view_by_content_url(logger, server, view_url) logger.debug(_("content_type.view") + ": {}".format(view_item.name)) req_option_csv = TSC.CSVRequestOptions(maxage=1) + DatasourcesAndWorkbooks.apply_values_from_url_params(req_option_csv, args.url, logger) server.views.populate_csv(view_item, req_option_csv) file_name_with_path = GetUrl.filename_from_args(args.filename, view_item.name, "csv") - with open(file_name_with_path, "wb") as f: - f.writelines(view_item.csv) - logger.info(_("export.success").format(view_item.name, file_name_with_path)) + DatasourcesAndWorkbooks.save_to_data_file(logger, view_item.csv, file_name_with_path) except TSC.ServerResponseError as e: Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) except Exception as e: Errors.exit_with_error(logger, exception=e) @staticmethod - def generate_twb(logger, server, args, file_extension): - workbook_name = GetUrl.get_workbook_name(logger, args.url) + def generate_twb(logger, server, args, file_extension, url): + workbook_name = GetUrl.get_workbook_name(url, logger) try: target_workbook = GetUrl.get_wb_by_content_url(logger, server, workbook_name) diff --git a/tabcmd/commands/user/user_data.py b/tabcmd/commands/user/user_data.py index 8e4caaeb..f8c088f7 100644 --- a/tabcmd/commands/user/user_data.py +++ b/tabcmd/commands/user/user_data.py @@ -273,4 +273,3 @@ def act_on_users( if number_of_errors > max_printing: logger.info(_("importcsvsummary.error.too_many_errors")) logger.info(_("importcsvsummary.remainingerrors")) - diff --git a/tabcmd/execution/logger_config.py b/tabcmd/execution/logger_config.py index c57de868..96f7f067 100644 --- a/tabcmd/execution/logger_config.py +++ b/tabcmd/execution/logger_config.py @@ -5,9 +5,9 @@ FORMATS = { logging.ERROR: "%(asctime)s %(levelname)-5s:(%(name)-10s %(filename)-10s: %(lineno)d): %(message)-30s", - logging.WARN: "%(asctime)s %(levelname)s : (%(name)-10s %(filename)-10s: %(lineno)d): %(message)-30s", + logging.WARN: "%(asctime)s %(levelname)-5s: (%(name)-10s %(filename)-10s: %(lineno)d): %(message)-30s", logging.INFO: "%(message)-30s", - logging.DEBUG: "%(asctime)s %(levelname)s : (%(name)-10s %(filename)-10s: %(lineno)d): %(message)-30s", + logging.DEBUG: "%(asctime)s %(levelname)-5s: (%(name)-10s %(filename)-10s: %(lineno)d): %(message)-30s", } # https://stackoverflow.com/questions/2183233/how-to-add-a-custom-loglevel-to-pythons-logging-facility @@ -41,7 +41,6 @@ def add_trace_level(): FORMATS[trace_level] = FORMATS[logging.ERROR] - 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()) diff --git a/tests/commands/test_geturl_utils.py b/tests/commands/test_geturl_utils.py index 0bdb4555..77d21309 100644 --- a/tests/commands/test_geturl_utils.py +++ b/tests/commands/test_geturl_utils.py @@ -8,6 +8,15 @@ from tabcmd.commands.datasources_and_workbooks.export_command import * from tabcmd.commands.server import Server +mock_args = argparse.Namespace() +mock_args.pagelayout = None +mock_args.pagesize = None +mock_args.image_resolution = None +mock_args.width = None +mock_args.height = None +mock_args.filename = None +mock_args.filter = None + mock_logger = mock.MagicMock() fake_item = mock.MagicMock(TSC.ViewItem) @@ -51,10 +60,13 @@ def test_get_view_without_extension_that_doesnt_have_one(self): assert GetUrl.get_name_without_possible_extension(filename) == filename def test_get_workbook_name(self): - assert GetUrl.get_workbook_name(mock_logger, "/workbooks/wbname") == "wbname" + assert GetUrl.get_workbook_name("workbooks/wbname", mock_logger) == "wbname" def test_view_name(self): - assert GetUrl.get_view_url("/views/wb-name/view-name") == "wb-name/sheets/view-name" + assert GetUrl.get_view_url("views/wb-name/view-name", None) == "wb-name/sheets/view-name" + + def test_view_name_with_url_params(self): + assert GetUrl.get_view_url("views/wb-name/view-name?:refresh=y", None) == "wb-name/sheets/view-name" """ GetUrl.get_view_without_extension(view_name) @@ -93,22 +105,14 @@ def test_parse_export_url_to_workbook_and_view_bad_url(self): assert view is None assert wb is None - def test_extract_query_params(self): - url = "wb-name/view-name?param1=value1" - options = TSC.PDFRequestOptions() - assert options.view_filters is not None - assert len(options.view_filters) is 0 - ExportCommand.extract_filter_values_from_url_params(options, url) - assert len(options.view_filters) == 1 - assert options.view_filters[0] == ("param1", "value1") - @mock.patch("tableauserverclient.Server") def test_download_csv(self, mock_server): mock_server.views = mock.MagicMock() mock_server.views.csv = mock.MagicMock() mock_view = tableauserverclient.ViewItem() url = "wb-name/view-name?param1=value1" - ExportCommand.download_csv(mock_server, mock_view, url, mock_logger) + mock_args.url = url + ExportCommand.download_csv(mock_server, mock_view, mock_args, mock_logger) @mock.patch("tableauserverclient.Server") def test_download_image(self, mock_server): @@ -116,7 +120,8 @@ def test_download_image(self, mock_server): mock_server.views.png = mock.MagicMock() mock_view = tableauserverclient.ViewItem() url = "wb-name/view-name?param1=value1" - ExportCommand.download_png(mock_server, mock_view, url, mock_logger) + mock_args.url = url + ExportCommand.download_png(mock_server, mock_view, mock_args, mock_logger) @mock.patch("tableauserverclient.Server") def test_download_view_pdf(self, mock_server): @@ -124,7 +129,8 @@ def test_download_view_pdf(self, mock_server): mock_server.views.pdf = mock.MagicMock() mock_view = tableauserverclient.ViewItem() url = "wb-name/view-name?param1=value1" - ExportCommand.download_view_pdf(mock_server, mock_view, url, mock_logger) + mock_args.url = url + ExportCommand.download_view_pdf(mock_server, mock_view, mock_args, mock_logger) @mock.patch("tableauserverclient.Server") def test_download_wb_pdf(self, mock_server): @@ -132,7 +138,50 @@ def test_download_wb_pdf(self, mock_server): mock_server.workbooks.pdf = mock.MagicMock() mock_view = tableauserverclient.ViewItem() url = "wb-name/view-name?param1=value1" - ExportCommand.download_wb_pdf(mock_server, mock_view, url, mock_logger) + mock_args.url = url + ExportCommand.download_wb_pdf(mock_server, mock_view, mock_args, mock_logger) + + +@mock.patch("tableauserverclient.ViewItem", fake_item) +class DS_WB_Tests(unittest.TestCase): + def test_apply_filter(self): + url = "wb-name/view-name?param1=value1" + options = TSC.PDFRequestOptions() + assert options.view_filters is not None + assert len(options.view_filters) is 0 + ExportCommand.apply_filter_value(options, "param1=value1", mock_logger) + assert len(options.view_filters) == 1 + assert options.view_filters[0] == ("param1", "value1") + + def test_extract_query_params(self): + url = "wb-name/view-name?param1=value1" + options = TSC.PDFRequestOptions() + assert options.view_filters is not None + assert len(options.view_filters) is 0 + ExportCommand.apply_values_from_url_params(options, url, mock_logger) + assert len(options.view_filters) == 1 + assert options.view_filters[0] == ("param1", "value1") + + def test_refresh_true(self): + url = "wb-name/view-name?:refresh=TRUE" + options = TSC.PDFRequestOptions() + assert options.max_age == -1 + ExportCommand.apply_values_from_url_params(options, url, mock_logger) + assert options.max_age == 0 + + def test_refresh_yes(self): + url = "wb-name/view-name?:refresh=yes" + options = TSC.PDFRequestOptions() + assert options.max_age == -1 + ExportCommand.apply_values_from_url_params(options, url, mock_logger) + assert options.max_age == 0 + + def test_refresh_y(self): + url = "wb-name/view-name?:refresh=y" + options = TSC.PDFRequestOptions() + assert options.max_age == -1 + ExportCommand.apply_values_from_url_params(options, url, mock_logger) + assert options.max_age == 0 def test_save_to_binary_file(self): mock_content = bytes() diff --git a/tests/e2e/online_tests.py b/tests/e2e/online_tests.py index bce557ef..f7da3677 100644 --- a/tests/e2e/online_tests.py +++ b/tests/e2e/online_tests.py @@ -51,7 +51,7 @@ def setup_class(cls): def _create_project(self, project_name, parent_path=None): command = "createproject" arguments = [command, "--name", project_name] - if parent_path or parent_location: + if parent_path: arguments.append("--parent-project-path") arguments.append(parent_path) _test_command(arguments) @@ -59,9 +59,9 @@ def _create_project(self, project_name, parent_path=None): def _delete_project(self, project_name, parent_path=None): command = "deleteproject" arguments = [command, project_name] - if parent_path or parent_location: + if parent_path: arguments.append("--parent-project-path") - arguments.append(parent_path or parent_location) + arguments.append(parent_path) _test_command(arguments) def _publish_samples(self, project_name): From a92d7f48e3fee5d7f782afe596020f83a1c3f38d Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Mon, 12 Sep 2022 22:09:42 -0700 Subject: [PATCH 22/37] remove setup.py from build steps --- .github/workflows/check-coverage.yml | 2 +- .github/workflows/generate-metadata.yml | 2 +- .github/workflows/package.yml | 2 +- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/run-e2-tests.yml | 2 +- .github/workflows/run-tests.yml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/check-coverage.yml b/.github/workflows/check-coverage.yml index cb83fa36..7de40783 100644 --- a/.github/workflows/check-coverage.yml +++ b/.github/workflows/check-coverage.yml @@ -31,7 +31,7 @@ jobs: pip install -e .[build] pip install -e .[test] doit version - python setup.py build + python -m build # https://github.com/marketplace/actions/pytest-coverage-comment - name: Generate coverage report diff --git a/.github/workflows/generate-metadata.yml b/.github/workflows/generate-metadata.yml index 213b564a..a1d6e88b 100644 --- a/.github/workflows/generate-metadata.yml +++ b/.github/workflows/generate-metadata.yml @@ -23,7 +23,7 @@ jobs: python -m pip install --upgrade pip python -m pip install -e .[build] doit version - python setup.py build + python -m build - name: Generate dependencies list run: python bin/license-checker.py diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index cf6ba4fc..476e06d5 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -57,7 +57,7 @@ jobs: pip install -e .[build] pip install -e .[package] doit version - python setup.py build + python -m build - name: Package with pyinstaller for ${{matrix.TARGET}} run: ${{matrix.CMD_BUILD}} diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index f0a36b53..b9ea0521 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -25,7 +25,7 @@ jobs: run: | pip install -e .[build] doit version - python setup.py sdist --formats=gztar + python -m build - name: Publish distribution 📦 to Test PyPI if: ${{ inputs.is_draft }} uses: pypa/gh-action-pypi-publish@release/v1 # license BSD-2 diff --git a/.github/workflows/run-e2-tests.yml b/.github/workflows/run-e2-tests.yml index 26f2222c..0bf4c3ea 100644 --- a/.github/workflows/run-e2-tests.yml +++ b/.github/workflows/run-e2-tests.yml @@ -37,7 +37,7 @@ jobs: pip install -e .[build] pip install -e .[test] doit version - python setup.py build + python -m build - name: Run e2e tests run: | diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 37266837..9c981c75 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -34,7 +34,7 @@ jobs: pip install -e .[build] pip install -e .[test] doit version - python setup.py build + python -m build - name: Test with pytest run: pytest From 4eeca1dab9ec3c567396086dff856c5c074db5dc Mon Sep 17 00:00:00 2001 From: Jac Date: Thu, 29 Sep 2022 12:46:17 -0700 Subject: [PATCH 23/37] Jac/update build scripts (#182) * fully port to pyproject.toml * update packaging script * add explicit install of build module * make e2e tests mypy-ok * remove makespec step for now --- .github/workflows/check-coverage.yml | 5 +- .github/workflows/generate-metadata.yml | 18 ++++--- .github/workflows/package.yml | 14 ++--- .github/workflows/publish-pypi.yml | 6 ++- .github/workflows/run-e2-tests.yml | 5 +- .github/workflows/run-tests.yml | 5 +- pyproject.toml | 68 ++++++++++++++++++++++++- setup.cfg | 10 ---- setup.py | 65 ++--------------------- tabcmd-linux.spec | 2 +- tabcmd-mac.spec | 2 +- tabcmd-windows.spec | 2 +- tabcmd.spec | 2 +- tabcmd/execution/localize.py | 46 ++++++++++------- tabcmd/execution/logger_config.py | 2 +- tests/e2e/setup_e2e.py | 3 +- tests/e2e/tests_integration.py | 2 +- 17 files changed, 139 insertions(+), 118 deletions(-) delete mode 100644 setup.cfg diff --git a/.github/workflows/check-coverage.yml b/.github/workflows/check-coverage.yml index 7de40783..0bfaa03a 100644 --- a/.github/workflows/check-coverage.yml +++ b/.github/workflows/check-coverage.yml @@ -28,8 +28,9 @@ jobs: run: | python --version python -m pip install --upgrade pip - pip install -e .[build] - pip install -e .[test] + python -m pip install --upgrade build + pip install . + pip install .[test] doit version python -m build diff --git a/.github/workflows/generate-metadata.yml b/.github/workflows/generate-metadata.yml index a1d6e88b..7f4abb2c 100644 --- a/.github/workflows/generate-metadata.yml +++ b/.github/workflows/generate-metadata.yml @@ -20,12 +20,15 @@ jobs: - name: Install App and Extras run: | + python --version python -m pip install --upgrade pip - python -m pip install -e .[build] + python -m pip install --upgrade build + pip install . + pip install .[test] doit version python -m build - - name: Generate dependencies list + - name: Run license check run: python bin/license-checker.py - name: Type-check @@ -35,7 +38,10 @@ jobs: run: black . --check - name: Generate pyinstaller specs - run: echo Using locally generated specs - # pyi-makespec tabcmd\tabcmd.py -n tabcmd-windows -i res\tabcmd.ico -F -c --version-file versionfile.txt --onefile --collect-data tabcmd.locales && - # pyi-makespec tabcmd\tabcmd.py -n tabcmd-mac -i res\tabcmd.icns -F -c --version-file versionfile.txt --onefile --collect-data tabcmd.locales --osx-bundle-identifier com.tableau.tabcmd - # 3. Unix + run: | + echo "Using manually generated specs" + + # todo: add hidden dependencies to these command lines + # pyi-makespec tabcmd\tabcmd.py -n tabcmd-windows -i res\tabcmd.ico -F -c --version-file versionfile.txt --onefile --collect-data tabcmd.locales + # pyi-makespec tabcmd\tabcmd.py -n tabcmd-mac -i res\tabcmd.icns -F -c --version-file versionfile.txt --onefile --collect-data tabcmd.locales --osx-bundle-identifier com.tableau.tabcmd + # 3. Unix diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 476e06d5..2b46a3e6 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -26,13 +26,12 @@ jobs: TARGET: windows CMD_BUILD: > pyinstaller tabcmd-windows.spec --clean --noconfirm --distpath ./dist/windows - OUT_FILE_NAME: tabcmd.exe + OUT_FILE_NAME: tabcmd-windows.exe ASSET_MIME: application/vnd.microsoft.portable-executable - os: macos-latest TARGET: macos CMD_BUILD: > - pyinstaller tabcmd-mac.spec --clean --noconfirm --distpath ./dist/macos && - cwd && ls && ls dist + 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 ASSET_MIME: application/zip @@ -54,8 +53,11 @@ jobs: - name: Install dependencies and build run: | - pip install -e .[build] - pip install -e .[package] + python --version + python -m pip install --upgrade pip + python -m pip install --upgrade build + pip install . + pip install .[test] doit version python -m build @@ -66,7 +68,7 @@ jobs: uses: WebFreak001/upload-asset@v1.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # automatically provided by github actions - OS: windows # just a variable we use in the name pattern + OS: windows # a variable we use in the name pattern? with: file: ./dist/${{ matrix.TARGET }}/tabcmd/${{ matrix.OUT_FILE_NAME}} mime: ${{ matrix.ASSET_MIME}} # required by GitHub API diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index b9ea0521..42c234e3 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -23,7 +23,11 @@ jobs: python-version: 3.8 - name: Build dist files run: | - pip install -e .[build] + python --version + python -m pip install --upgrade pip + python -m pip install --upgrade build + pip install . + pip install .[package] doit version python -m build - name: Publish distribution 📦 to Test PyPI diff --git a/.github/workflows/run-e2-tests.yml b/.github/workflows/run-e2-tests.yml index 0bf4c3ea..ec1262af 100644 --- a/.github/workflows/run-e2-tests.yml +++ b/.github/workflows/run-e2-tests.yml @@ -34,8 +34,9 @@ jobs: run: | python --version python -m pip install --upgrade pip - pip install -e .[build] - pip install -e .[test] + python -m pip install --upgrade build + pip install . + pip install .[test] doit version python -m build diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 9c981c75..bad6a73b 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -31,8 +31,9 @@ jobs: run: | python --version python -m pip install --upgrade pip - pip install -e .[build] - pip install -e .[test] + python -m pip install --upgrade build + pip install . + pip install .[test] doit version python -m build diff --git a/pyproject.toml b/pyproject.toml index 485decd9..77c00308 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,73 @@ [build-system] requires = ["build", "setuptools>=62", "wheel", "setuptools_scm>=6.2"] +build-backend = "setuptools.build_meta" [tool.setuptools_scm] -"local_scheme"= "no-local-version" # require pypi supported versions always +local_scheme = "no-local-version" # require pypi supported versions always +[tool.setuptools] +packages = ["tabcmd"] +[tool.setuptools.package-data] +tabcmd = ["tabcmd.locales/**/*.mo"] [tool.black] line-length = 120 +target-version = ['py37', 'py38', 'py39', 'py310'] extend-exclude = '^/bin/*' +[tool.mypy] +disable_error_code = [ + 'misc', + 'import' +] +files = ["tabcmd", "tests"] +show_error_codes = true +ignore_missing_imports = true +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = "--junitxml=./test.junit.xml" + +[project] +name="tabcmd" +dynamic = ["version"] +description="A command line client for working with Tableau Server." +authors = [{name="Tableau", email="github@tableau.com"}] +license = {file = "LICENSE"} +readme = "README.md" +requires-python = ">=3.7" +classifiers = [ + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10" +] +dependencies = [ + 'argparse', + "appdirs", + "doit", + "ftfy", + "polling2", + "pyinstaller_versionfile", + "requests>=2.11,<3.0", + "setuptools_scm", + "types-appdirs", + "types-mock", + "types-requests", + "types-setuptools", + "tableauserverclient>=0.19", + "urllib3>=1.24.3,<2.0", +] +[project.optional-dependencies] +test = [ + "black", + "mock", + "mypy", + "pytest>=7.0", + "pytest-cov", + "pytest-order", + "pytest-runner", + "requests-mock>=1.0,<2.0"] +localize = ["doit", "ftfy"] +package = ["pyinstaller>=5.1"] +[project.urls] +repository = "https://github.com/tableau/tabcmd" +[project.scripts] +tabcmd = "tabcmd.tabcmd:main" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index d4230e64..00000000 --- a/setup.cfg +++ /dev/null @@ -1,10 +0,0 @@ -[pycodestyle] -max_line_length = 120 -[mypy] -exclude = tests/e2e -ignore_missing_imports = True -[tool:pytest] -# useful settings for debugging -# log_cli = True -# log_cli_level = DEBUG - diff --git a/setup.py b/setup.py index 8d834b82..914e6b5d 100644 --- a/setup.py +++ b/setup.py @@ -1,63 +1,4 @@ -from setuptools import setup, find_packages +# legacy stub setup.py, do not add configuration here +from setuptools import setup -setup( - name="tabcmd", - author="Tableau", - author_email="github@tableau.com", - description="A command line client for working with Tableau Server.", - long_description="A command line client for working with Tableau Server.", - license="MIT", - url="https://github.com/tableau/tabcmd", - python_requires=">=3.7", - packages=find_packages(), - package_data={"tabcmd": ["tabcmd.locales/**/*.mo"]}, - include_package_data=True, - entry_points={"console_scripts": ["tabcmd = tabcmd.tabcmd:main"]}, - setup_requires=[ - # copy of pyproject.toml for back compat - "build", - "setuptools>=62", - "setuptools_scm>=6.2", - "wheel", - ], - install_requires=[ - "polling2", - "requests>=2.11,<3.0", - "tableauserverclient>=0.19", - "urllib3>=1.24.3,<2.0", - ], - extras_require={ - "localize": [ - "doit", - "ftfy", - ], - "build": [ - "appdirs", - "black", - "doit", - "ftfy", - "mypy", - "pyinstaller_versionfile", - "setuptools>=62", - "setuptools_scm", - "types-appdirs", - "types-mock", - "types-requests", - "types-setuptools", - ], - "package": [ - "pyinstaller>=5.1", - "pyinstaller-versionfile", - ], - "test": [ - "mock", - "pytest", - "pytest-cov", - "pytest-order", - "pytest-runner", - "requests-mock>=1.0,<2.0", - ], - }, - test_suite="tests", - zip_safe=False, -) +setup() diff --git a/tabcmd-linux.spec b/tabcmd-linux.spec index d71fd83e..c5e367fc 100644 --- a/tabcmd-linux.spec +++ b/tabcmd-linux.spec @@ -12,7 +12,7 @@ a = Analysis( pathex=[], binaries=[], datas=datas, - hiddenimports=[], + hiddenimports=['tableauserverclient', 'requests.packages.urllib3', 'pkg_resources'], hookspath=[], hooksconfig={}, runtime_hooks=[], diff --git a/tabcmd-mac.spec b/tabcmd-mac.spec index fcce4bfe..fee41fa8 100644 --- a/tabcmd-mac.spec +++ b/tabcmd-mac.spec @@ -13,7 +13,7 @@ a = Analysis( pathex=[], binaries=[], datas=datas, - hiddenimports=[], + hiddenimports=['tableauserverclient', 'requests.packages.urllib3', 'pkg_resources'], hookspath=[], hooksconfig={}, runtime_hooks=[], diff --git a/tabcmd-windows.spec b/tabcmd-windows.spec index d71fd83e..03f1dfd1 100644 --- a/tabcmd-windows.spec +++ b/tabcmd-windows.spec @@ -12,7 +12,7 @@ a = Analysis( pathex=[], binaries=[], datas=datas, - hiddenimports=[], + hiddenimports=['tableauserverclient', 'requests', 'pkg_resources'], hookspath=[], hooksconfig={}, runtime_hooks=[], diff --git a/tabcmd.spec b/tabcmd.spec index 650b541a..a1e0421c 100644 --- a/tabcmd.spec +++ b/tabcmd.spec @@ -9,7 +9,7 @@ a = Analysis( pathex=[], binaries=[], datas=[], - hiddenimports=[], + hiddenimports=['tableauserverclient', 'requests.packages.urllib3', 'pkg_resources'], hookspath=[], hooksconfig={}, runtime_hooks=[], diff --git a/tabcmd/execution/localize.py b/tabcmd/execution/localize.py index a53e43f1..ec70ee30 100644 --- a/tabcmd/execution/localize.py +++ b/tabcmd/execution/localize.py @@ -3,6 +3,7 @@ import logging import os import sys +from os import listdir from typing import Any from typing import Callable @@ -20,7 +21,7 @@ def _(string_key: str) -> str: def _identity_func(x: Any) -> Any: - return x + return "++" + x + "++" # The client should present text in the OS language, or english if not present. @@ -41,7 +42,7 @@ def set_client_locale(lang: str = None, logger=None) -> Callable: for lang in locale_options: try: if lang: - translate = _load_language(lang, domain) + translate = _load_language(lang, domain, logger) break except Exception as e: print("Failed to load language '", lang, "':", e) @@ -49,23 +50,32 @@ def set_client_locale(lang: str = None, logger=None) -> Callable: return translate or _identity_func -# Handling file locations in unbundled (e.g dev) layout and when bundled by pyinstaller +"""Get absolute path to resource, works for unbundled (e.g dev) and when bundled by PyInstaller""" # https://stackoverflow.com/questions/7674790/bundling-data-files-with-pyinstaller-onefile/13790741#13790741 -def resource_path(relative_path): - """Get absolute path to resource, works for dev and for PyInstaller""" - base_path = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__))) - return os.path.join(base_path, relative_path) - - -def _load_language(current_locale, domain): - locale_path = os.path.join("..", "locales") - - # fallback=True means if loading the translated files fails, strings will be returned - # we use the identity function above instead - locale_dir = resource_path(locale_path) - language: gettext.NullTranslations = gettext.translation( - domain, locale_dir, languages=[current_locale], fallback=False - ) +def define_locale_dir(logger): + try: + base_path = getattr(sys, "_MEIPASS") + except AttributeError: # sys._MEIPASS will only exist in bundled pyinstaller exe, + # in unbundled src code we take the location of the current file + # and go 2 dirs up so that the relative path /tabcmd/locales is still correct + base_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..") + relative_path = os.path.join(".", "tabcmd", "locales") + locale_dir = os.path.join(base_path, relative_path) + logger.debug("Checking for language resources at " + locale_dir) + """ to debug pyinstaller file bundling, try something like this example debug line + try: + logger.debug(listdir(sys._MEIPASS)) + except AttributeError as e: + logger.debug(e) + """ + print(locale_dir) + print(listdir(locale_dir)) + return locale_dir + + +def _load_language(current_locale, domain, logger): + locale_dir = define_locale_dir(logger) + language: gettext.NullTranslations = gettext.translation(domain, locale_dir, languages=[current_locale]) language.install() # I believe this is the expensive call _ = language.gettext return _ diff --git a/tabcmd/execution/logger_config.py b/tabcmd/execution/logger_config.py index 96f7f067..8a42e2cb 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: "%(message)-30s", + logging.INFO: "%(filename)-10s: %(message)-30s", logging.DEBUG: "%(asctime)s %(levelname)-5s: (%(name)-10s %(filename)-10s: %(lineno)d): %(message)-30s", } diff --git a/tests/e2e/setup_e2e.py b/tests/e2e/setup_e2e.py index ddbe1573..4edecf7c 100644 --- a/tests/e2e/setup_e2e.py +++ b/tests/e2e/setup_e2e.py @@ -1,9 +1,8 @@ import subprocess import os -import pytest try: - from tests.e2e import credentials + from tests.e2e import credentials # type: ignore except ImportError: credentials = None # type: ignore diff --git a/tests/e2e/tests_integration.py b/tests/e2e/tests_integration.py index d41070a1..e18c38b6 100644 --- a/tests/e2e/tests_integration.py +++ b/tests/e2e/tests_integration.py @@ -8,7 +8,7 @@ try: - from tests.e2e import credentials + from tests.e2e import credentials # type: ignore except ImportError: credentials = None # type: ignore From a8d0e663754ae8230495cd2f1fe5e9b64ff8bd82 Mon Sep 17 00:00:00 2001 From: Jac Date: Tue, 11 Oct 2022 10:59:10 -0700 Subject: [PATCH 24/37] Jac/image export (#186) * Fix image export (https://github.com/tableau/tabcmd/issues/184) * implement pdf layout/page type --- tabcmd/commands/auth/session.py | 2 +- .../datasources_and_workbooks_command.py | 11 ++++- .../export_command.py | 43 +++++++++++++------ 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/tabcmd/commands/auth/session.py b/tabcmd/commands/auth/session.py index a380ab73..188c0581 100644 --- a/tabcmd/commands/auth/session.py +++ b/tabcmd/commands/auth/session.py @@ -44,8 +44,8 @@ def __init__(self): self.timeout = None self.logging_level = "info" - self._read_from_json() self.logger = log(__class__.__name__, self.logging_level) # instantiate here mostly for tests + self._read_from_json() self.tableau_server = None # this one is an object that doesn't get persisted in the file # called before we connect to the server diff --git a/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py b/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py index 86536550..1eadb3a5 100644 --- a/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py +++ b/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py @@ -99,8 +99,15 @@ def apply_png_options(request_options: TSC.ImageRequestOptions, args, logger): if args.height or args.width: # only applicable for png logger.warn("Height/width arguments not yet implemented in export") - if args.image_resolution: - request_options.image_resolution = args.image_resolution + # Always request high-res images + request_options.image_resolution = "high" + + @staticmethod + def apply_pdf_options(request_options: TSC.PDFRequestOptions, args, logger): + request_options.page_type = args.pagesize + if args.pagelayout: + logger.debug("Setting page layout to: {}".format(args.pagelayout)) + request_options.orientation = args.pagelayout @staticmethod def save_to_data_file(logger, output, filename): diff --git a/tabcmd/commands/datasources_and_workbooks/export_command.py b/tabcmd/commands/datasources_and_workbooks/export_command.py index 2e714543..79ddedb6 100644 --- a/tabcmd/commands/datasources_and_workbooks/export_command.py +++ b/tabcmd/commands/datasources_and_workbooks/export_command.py @@ -6,6 +6,8 @@ from tabcmd.execution.logger_config import log from .datasources_and_workbooks_command import DatasourcesAndWorkbooks +pagesize = TSC.PDFRequestOptions.PageType # type alias for brevity + class ExportCommand(DatasourcesAndWorkbooks): @@ -24,12 +26,29 @@ def define_args(export_parser): export_parser.add_argument( "--pagelayout", choices=["landscape", "portrait"], - default="landscape", + default=None, help="page orientation (landscape or portrait) of the exported PDF", ) - export_parser.add_argument("--pagesize", default="letter", help="Set the page size of the exported PDF") export_parser.add_argument( - "--image-resolution", default="High", help="Set the image resolution of the exported image" + "--pagesize", + choices=[ + pagesize.A3, + pagesize.A4, + pagesize.A5, + pagesize.B4, + pagesize.B5, + pagesize.Executive, + pagesize.Folio, + pagesize.Ledger, + pagesize.Legal, + pagesize.Letter, + pagesize.Note, + pagesize.Quarto, + pagesize.Tabloid, + pagesize.Unspecified, + ], + default="letter", + help="Set the page size of the exported PDF", ) export_parser.add_argument( @@ -111,44 +130,44 @@ def apply_values_from_args(request_options: TSC.PDFRequestOptions, args, logger= @staticmethod def download_wb_pdf(server, workbook_item, args, logger): - logger.trace(args.url) + logger.debug(args.url) pdf_options = TSC.PDFRequestOptions(maxage=1) ExportCommand.apply_values_from_url_params(pdf_options, args.url, logger) ExportCommand.apply_values_from_args(pdf_options, args, logger) - logger.trace(pdf_options) + logger.debug(pdf_options.get_query_params()) server.workbooks.populate_pdf(workbook_item, pdf_options) return workbook_item.pdf @staticmethod def download_view_pdf(server, view_item, args, logger): - logger.trace(args.url) + logger.debug(args.url) pdf_options = TSC.PDFRequestOptions(maxage=1) ExportCommand.apply_values_from_url_params(pdf_options, args.url, logger) ExportCommand.apply_values_from_args(pdf_options, args, logger) - logger.trace(pdf_options) + logger.debug(pdf_options.get_query_params()) server.views.populate_pdf(view_item, pdf_options) return view_item.pdf @staticmethod def download_csv(server, view_item, args, logger): - logger.trace(args.url) + logger.debug(args.url) csv_options = TSC.CSVRequestOptions(maxage=1) ExportCommand.apply_values_from_url_params(csv_options, args.url, logger) ExportCommand.apply_values_from_args(csv_options, args, logger) - logger.trace(csv_options) + logger.debug(csv_options.get_query_params()) server.views.populate_csv(view_item, csv_options) return view_item.csv @staticmethod def download_png(server, view_item, args, logger): - logger.trace(args.url) + logger.debug(args.url) image_options = TSC.ImageRequestOptions(maxage=1) ExportCommand.apply_values_from_url_params(image_options, args.url, logger) ExportCommand.apply_values_from_args(image_options, args, logger) DatasourcesAndWorkbooks.apply_png_options(image_options, args, logger) - logger.trace(image_options) + logger.debug(image_options.get_query_params()) server.views.populate_image(view_item, image_options) - return view_item.png + return view_item.image @staticmethod def parse_export_url_to_workbook_and_view(logger, url): From cf75a1fb82b80e7b4a5b6289bdefff3ae6ad57af Mon Sep 17 00:00:00 2001 From: Jac Date: Tue, 11 Oct 2022 11:04:35 -0700 Subject: [PATCH 25/37] tfs 1427025: Confusing error message related to sign-in (#187) Make error message generic when signin args not present --- tabcmd/commands/auth/session.py | 4 ++-- tabcmd/execution/localize.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tabcmd/commands/auth/session.py b/tabcmd/commands/auth/session.py index 188c0581..139e3e69 100644 --- a/tabcmd/commands/auth/session.py +++ b/tabcmd/commands/auth/session.py @@ -245,8 +245,8 @@ def create_session(self, args): signed_in_object = self._sign_in(credentials) if not signed_in_object: - missing_var = _("editdomain.errors.requires_nickname_name").format("username", "token") - Errors.exit_with_error(self.logger, _("session.errors.missing_arguments").format(missing_var)) + message = "Run 'tabcmd login -h' for details on required arguments" + Errors.exit_with_error(self.logger, _("session.errors.missing_arguments").format(message)) if args.no_cookie: self._remove_json() else: diff --git a/tabcmd/execution/localize.py b/tabcmd/execution/localize.py index ec70ee30..d6eed6dd 100644 --- a/tabcmd/execution/localize.py +++ b/tabcmd/execution/localize.py @@ -68,8 +68,8 @@ def define_locale_dir(logger): except AttributeError as e: logger.debug(e) """ - print(locale_dir) - print(listdir(locale_dir)) + logger.debug(locale_dir) + logger.debug(listdir(locale_dir)) return locale_dir From 859275332cbc107c54f16e0338a62a2e8dd20bbc Mon Sep 17 00:00:00 2001 From: Jac Date: Tue, 11 Oct 2022 11:06:01 -0700 Subject: [PATCH 26/37] add arguments to publish command (#181) * add arguments to publish command to publish data connection with Tableau Bridge --- .../commands/datasources_and_workbooks/publish_command.py | 1 + tabcmd/execution/global_options.py | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tabcmd/commands/datasources_and_workbooks/publish_command.py b/tabcmd/commands/datasources_and_workbooks/publish_command.py index ec607755..59432ee1 100644 --- a/tabcmd/commands/datasources_and_workbooks/publish_command.py +++ b/tabcmd/commands/datasources_and_workbooks/publish_command.py @@ -65,6 +65,7 @@ def run_command(args): elif source in ["tds", "tdsx", "hyper"]: new_datasource = TSC.DatasourceItem(project_id, name=args.name) + new_datasource.use_remote_query_agent = args.use_tableau_bridge try: new_datasource = server.datasources.publish(new_datasource, args.filename, publish_mode) except IOError as ioe: diff --git a/tabcmd/execution/global_options.py b/tabcmd/execution/global_options.py index 34672806..30211a13 100644 --- a/tabcmd/execution/global_options.py +++ b/tabcmd/execution/global_options.py @@ -316,8 +316,12 @@ def set_publish_args(parser): ) parser.add_argument("--oauth-username", help="The email address of a preconfigured OAuth connection") parser.add_argument("--save-oauth", action="store_true", help="Save embedded OAuth credentials in the datasource") - parser.add_argument("--thumbnail-username", help="Not yet implemented") - parser.add_argument("--thumbnail-group", help="Not yet implemented") # not implemented in the REST API + + thumbnails = parser.add_mutually_exclusive_group() + thumbnails.add_argument("--thumbnail-username", help="Not yet implemented") + thumbnails.add_argument("--thumbnail-group", help="Not yet implemented") # not implemented in the REST API + + parser.add_argument("--use-tableau-bridge", help="Refresh datasource through Tableau Bridge") def set_overwrite_option(parser): From b96cc080d36c326ad9a00d63b45915ca731f62f3 Mon Sep 17 00:00:00 2001 From: Jac Date: Tue, 18 Oct 2022 01:58:32 -0700 Subject: [PATCH 27/37] Improve confusing help message for signing in (#190) * Make error message generic when signin args not present * stop duplicate logging in session --- tabcmd/commands/auth/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tabcmd/commands/auth/session.py b/tabcmd/commands/auth/session.py index 139e3e69..ca9efbc9 100644 --- a/tabcmd/commands/auth/session.py +++ b/tabcmd/commands/auth/session.py @@ -214,7 +214,7 @@ def create_session(self, args): self._read_existing_state() self._update_session_data(args) self.logging_level = args.logging_level or self.logging_level - self.logger = log(__class__.__name__, self.logging_level) + self.logger = self.logger or log(__class__.__name__, self.logging_level) credentials = None if args.password: From 6be0c25c1534d26cc2f8c8b36156301db515fd3a Mon Sep 17 00:00:00 2001 From: Jac Date: Mon, 31 Oct 2022 19:33:55 -0700 Subject: [PATCH 28/37] extract encryption defaults to false (#193) also renamed some tests for easier reading --- tabcmd/execution/global_options.py | 2 +- tests/commands/test_execution.py | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/tabcmd/execution/global_options.py b/tabcmd/execution/global_options.py index 30211a13..a1f76c02 100644 --- a/tabcmd/execution/global_options.py +++ b/tabcmd/execution/global_options.py @@ -134,7 +134,7 @@ def set_encryption_option(parser): parser.add_argument( "--encrypt", dest="encrypt", - action="store_false", + action="store_true", # set to true IF user passes in option --encrypt help="Encrypt the newly created extract.", ) return parser diff --git a/tests/commands/test_execution.py b/tests/commands/test_execution.py index f737829c..364ce456 100644 --- a/tests/commands/test_execution.py +++ b/tests/commands/test_execution.py @@ -11,26 +11,25 @@ class ExecutionTests(unittest.TestCase): def test_launch(self): parser = TabcmdController.initialize() TabcmdController.run(parser, ["help"]) - # check exit code = 0? def test_initialize(self): TabcmdController.initialize() @mock.patch("sys.argv", "") - def test_inputs(self): + def test_no_inputs_exits(self): fake_parser = mock.MagicMock(argparse.ArgumentParser) with self.assertRaises(SystemExit): TabcmdController.run(fake_parser, None) - def test_invalid_command(self): + def test_invalid_command_exits(self): parser = TabcmdController.initialize() - # crashes out during parse_args + # alerts and exits during parse_args with self.assertRaises(SystemExit): TabcmdController.run(parser, ["boo", "--language", "fr"]) - def test_launch_languages(self): + def test_launch_languages_succeeds(self): parser = TabcmdController.initialize() - # crashes out from list_sites.cmd + # fails to connect to server... with self.assertRaises(SystemExit): TabcmdController.run(parser, ["listsites", "--language", "fr"]) From e88f3ab0c3a0688144a311474dc06606fb3fd01b Mon Sep 17 00:00:00 2001 From: Jac Date: Wed, 9 Nov 2022 14:55:48 -0800 Subject: [PATCH 29/37] call sites endpoint correctly (#194) --- tabcmd/commands/server.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tabcmd/commands/server.py b/tabcmd/commands/server.py index cf3700cf..fbec528e 100644 --- a/tabcmd/commands/server.py +++ b/tabcmd/commands/server.py @@ -93,7 +93,8 @@ def get_site_for_command_or_throw(logger, server, site_name): @staticmethod def get_site_by_name(logger, server, site_name): try: - site_item = Server.get_items_by_name(logger, server.sites, site_name)[0] + # sites don't use the normal filter + site_item = server.sites.get_by_name(site_name) except Exception as e: Errors.exit_with_error(logger, exception=e) return site_item From 35ec9afcc0538e97c3f80150f34b96ac9a5c9734 Mon Sep 17 00:00:00 2001 From: Jac Date: Wed, 9 Nov 2022 14:56:19 -0800 Subject: [PATCH 30/37] publish data source to Tableau Cloud for published live connection with Tableau Bridge (#195) * add argument to publish command From 37919b41661a9731030d45f023eabbf66b9b1846 Mon Sep 17 00:00:00 2001 From: Jac Date: Mon, 21 Nov 2022 15:54:46 -0800 Subject: [PATCH 31/37] open csv files allowing for BOM (#200) * open csv files prepared for BOM * fix error for new stricter mypy --- .gitignore | 1 + tabcmd/commands/datasources_and_workbooks/export_command.py | 1 - .../commands/datasources_and_workbooks/get_url_command.py | 1 - tabcmd/commands/server.py | 2 +- tabcmd/execution/global_options.py | 4 ++-- tabcmd/execution/localize.py | 6 +++--- tests/commands/test_geturl_utils.py | 2 -- tests/parsers/common_setup.py | 3 +++ tests/parsers/test_parser_add_user.py | 2 +- tests/parsers/test_parser_create_site_users.py | 2 +- tests/parsers/test_parser_create_user.py | 2 +- tests/parsers/test_parser_delete_site_user.py | 2 +- tests/parsers/test_parser_remove_user.py | 2 +- 13 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index d586d56e..4f38905a 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,4 @@ site-packages # local tabcmd-dev workon +test.junit.xml diff --git a/tabcmd/commands/datasources_and_workbooks/export_command.py b/tabcmd/commands/datasources_and_workbooks/export_command.py index 428c530f..27d5d619 100644 --- a/tabcmd/commands/datasources_and_workbooks/export_command.py +++ b/tabcmd/commands/datasources_and_workbooks/export_command.py @@ -113,7 +113,6 @@ def run_command(args): else: ExportCommand.save_to_file(logger, output, save_name) - except Exception as e: Errors.exit_with_error(logger, "Error saving to file", e) diff --git a/tabcmd/commands/datasources_and_workbooks/get_url_command.py b/tabcmd/commands/datasources_and_workbooks/get_url_command.py index be0d96a0..688426d9 100644 --- a/tabcmd/commands/datasources_and_workbooks/get_url_command.py +++ b/tabcmd/commands/datasources_and_workbooks/get_url_command.py @@ -79,7 +79,6 @@ def explain_expected_url(logger): ) Errors.exit_with_error(logger, message) - @staticmethod def get_file_type_from_filename(logger, file_name, url): file_name = file_name or url diff --git a/tabcmd/commands/server.py b/tabcmd/commands/server.py index fbec528e..6777b881 100644 --- a/tabcmd/commands/server.py +++ b/tabcmd/commands/server.py @@ -43,7 +43,7 @@ def find_user(logger, server, username): return Server.get_items_by_name(logger, server.users, username)[0] @staticmethod - def get_items_by_name(logger, item_endpoint, item_name: str, container: TSC.ProjectItem = None) -> List: + def get_items_by_name(logger, item_endpoint, item_name: str, container: Optional[TSC.ProjectItem] = None) -> List: # TODO: typing should reflect that this returns TSC.TableauItem and item_endpoint is of type TSC.QuerysetEndpoint[same] item_type = type(item_endpoint).__name__ item_log_name: str = "[" + item_type + "] " + item_name diff --git a/tabcmd/execution/global_options.py b/tabcmd/execution/global_options.py index a1f76c02..61467455 100644 --- a/tabcmd/execution/global_options.py +++ b/tabcmd/execution/global_options.py @@ -40,7 +40,7 @@ def set_users_file_arg(parser): parser.add_argument( "--users", required=True, - type=argparse.FileType("r", encoding="UTF-8"), + type=argparse.FileType("r", encoding="utf-8-sig"), help="CSV file containing a list of users.", ) return parser @@ -50,7 +50,7 @@ def set_users_file_positional(parser): parser.add_argument( "filename", metavar="filename.csv", - type=argparse.FileType("r", encoding="UTF-8"), + type=argparse.FileType("r", encoding="utf-8-sig"), help="CSV file containing a list of users.", ) return parser diff --git a/tabcmd/execution/localize.py b/tabcmd/execution/localize.py index d6eed6dd..00c770f0 100644 --- a/tabcmd/execution/localize.py +++ b/tabcmd/execution/localize.py @@ -4,7 +4,7 @@ import os import sys from os import listdir -from typing import Any +from typing import Any, Optional from typing import Callable translate = None @@ -14,7 +14,7 @@ def _(string_key: str) -> str: global translate if not translate: - translate = set_client_locale(None, None) + translate = set_client_locale() if not translate: translate = _identity_func return translate(string_key) @@ -25,7 +25,7 @@ def _identity_func(x: Any) -> Any: # The client should present text in the OS language, or english if not present. -def set_client_locale(lang: str = None, logger=None) -> Callable: +def set_client_locale(lang: str = "", logger=None) -> Optional[Callable]: if not logger: logger = logging.getLogger() diff --git a/tests/commands/test_geturl_utils.py b/tests/commands/test_geturl_utils.py index fb354141..77d21309 100644 --- a/tests/commands/test_geturl_utils.py +++ b/tests/commands/test_geturl_utils.py @@ -114,7 +114,6 @@ def test_download_csv(self, mock_server): mock_args.url = url ExportCommand.download_csv(mock_server, mock_view, mock_args, mock_logger) - @mock.patch("tableauserverclient.Server") def test_download_image(self, mock_server): mock_server.views = mock.MagicMock() @@ -184,7 +183,6 @@ def test_refresh_y(self): ExportCommand.apply_values_from_url_params(options, url, mock_logger) assert options.max_age == 0 - def test_save_to_binary_file(self): mock_content = bytes() filename = "test_out.pdf" diff --git a/tests/parsers/common_setup.py b/tests/parsers/common_setup.py index 6530998b..619d4805 100644 --- a/tests/parsers/common_setup.py +++ b/tests/parsers/common_setup.py @@ -2,6 +2,9 @@ from collections import namedtuple +encoding = "utf-8-sig" + + def mock_command_action(): print("a mockery!") diff --git a/tests/parsers/test_parser_add_user.py b/tests/parsers/test_parser_add_user.py index fe229bce..fe57f669 100644 --- a/tests/parsers/test_parser_add_user.py +++ b/tests/parsers/test_parser_add_user.py @@ -27,7 +27,7 @@ def test_add_users_parser_users_file(self): mock_args = [commandname, "group-name", "--users", "users.csv"] args = self.parser_under_test.parse_args(mock_args) self.assertEqual(args.name, "group-name"), args - open_file.assert_called_with("users.csv", "r", -1, "UTF-8", None), args + open_file.assert_called_with("users.csv", "r", -1, encoding, None), args @mock.patch("builtins.open") def test_add_user_parser_missing_group_name(self, filereader): diff --git a/tests/parsers/test_parser_create_site_users.py b/tests/parsers/test_parser_create_site_users.py index ea157a98..5b46bd00 100644 --- a/tests/parsers/test_parser_create_site_users.py +++ b/tests/parsers/test_parser_create_site_users.py @@ -16,7 +16,7 @@ def test_create_site_users_parser_users_file(self): with mock.patch("builtins.open", mock.mock_open(read_data="test")) as open_file: mock_args = [commandname, "users.csv"] args = self.parser_under_test.parse_args(mock_args) - open_file.assert_called_with("users.csv", "r", -1, "UTF-8", None) + open_file.assert_called_with("users.csv", "r", -1, encoding, None) def test_create_site_user_parser_missing_arguments(self): mock_args = [commandname] diff --git a/tests/parsers/test_parser_create_user.py b/tests/parsers/test_parser_create_user.py index f93ea58f..440d430d 100644 --- a/tests/parsers/test_parser_create_user.py +++ b/tests/parsers/test_parser_create_user.py @@ -16,7 +16,7 @@ def test_create_users_parser_users_file(self): with mock.patch("builtins.open", mock.mock_open(read_data="test")) as open_file: mock_args = [commandname, "users.csv"] args = self.parser_under_test.parse_args(mock_args) - open_file.assert_called_with("users.csv", "r", -1, "UTF-8", None) + open_file.assert_called_with("users.csv", "r", -1, encoding, None) def test_create_user_parser_missing_arguments(self): mock_args = [commandname] diff --git a/tests/parsers/test_parser_delete_site_user.py b/tests/parsers/test_parser_delete_site_user.py index df66af66..c0d81b7e 100644 --- a/tests/parsers/test_parser_delete_site_user.py +++ b/tests/parsers/test_parser_delete_site_user.py @@ -18,7 +18,7 @@ def test_delete_site_user_parser(self): with mock.patch("builtins.open", mock.mock_open(read_data="test")) as open_file: mock_args = [commandname, "users.csv"] args = self.parser_under_test.parse_args(mock_args) - open_file.assert_called_with("users.csv", "r", -1, "UTF-8", None) + open_file.assert_called_with("users.csv", "r", -1, encoding, None) def test_delete_site_user_parser_missing_arguments(self): mock_args = [commandname] diff --git a/tests/parsers/test_parser_remove_user.py b/tests/parsers/test_parser_remove_user.py index 83562c97..6441354a 100644 --- a/tests/parsers/test_parser_remove_user.py +++ b/tests/parsers/test_parser_remove_user.py @@ -23,7 +23,7 @@ def test_remove_users_parser_users_file(self): mock_args = [commandname, "group-name", "--users", "users.csv"] args = self.parser_under_test.parse_args(mock_args) self.assertEqual(args.name, "group-name") - open_file.assert_called_with("users.csv", "r", -1, "UTF-8", None) + open_file.assert_called_with("users.csv", "r", -1, encoding, None) def test_remove_users_parser_missing_group_name(self): with mock.patch("builtins.open", mock.mock_open(read_data="test")): From a4f6f39cb6158900be3fbdbf109e85414955a7d8 Mon Sep 17 00:00:00 2001 From: Jac Date: Mon, 28 Nov 2022 14:10:20 -0800 Subject: [PATCH 32/37] Jac/packaging (#198) * update package action, add readme * Update tabcmd.spec with locale files and libraries * change the spec to --onefile --- .github/workflows/package.yml | 2 +- .github/workflows/publish-pypi.yml | 2 -- README.md | 2 ++ pyproject.toml | 6 +++--- tabcmd.spec | 32 ++++++++++++++++++------------ 5 files changed, 25 insertions(+), 19 deletions(-) diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 2b46a3e6..39324cbf 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -57,7 +57,7 @@ jobs: python -m pip install --upgrade pip python -m pip install --upgrade build pip install . - pip install .[test] + pip install .[package] doit version python -m build diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 42c234e3..fa2c59ac 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -27,8 +27,6 @@ jobs: python -m pip install --upgrade pip python -m pip install --upgrade build pip install . - pip install .[package] - doit version python -m build - name: Publish distribution 📦 to Test PyPI if: ${{ inputs.is_draft }} diff --git a/README.md b/README.md index c22552aa..e1703650 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ To run tabcmd from your local copy, from a console window in the same directory > mypy tabcmd tests - packaging is done with pyinstaller. You can only build an executable for the platform you build on. +- To package a release, we first bump the version with `doit version` and build as 2.x.0 before packaging > pyinstaller tabcmd\tabcmd.py --clean --noconfirm produces dist/tabcmd.exe @@ -113,3 +114,4 @@ Copies of tabcmd that shipped with Tableau Server are referred to by the version At some point in the future, tabcmd will no longer be included with Tableau Server. *We have no intention of breaking Server install flows.* If you have specific suggestions or concerns on what that will look like, feel free to open an issue here or a thread on the Community Forums. + diff --git a/pyproject.toml b/pyproject.toml index 77c00308..80abf009 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,11 +2,11 @@ requires = ["build", "setuptools>=62", "wheel", "setuptools_scm>=6.2"] build-backend = "setuptools.build_meta" [tool.setuptools_scm] -local_scheme = "no-local-version" # require pypi supported versions always +"local_scheme" = "no-local-version" # require pypi supported versions always [tool.setuptools] packages = ["tabcmd"] [tool.setuptools.package-data] -tabcmd = ["tabcmd.locales/**/*.mo"] +"tabcmd" = ["tabcmd.locales/**/*.mo"] [tool.black] line-length = 120 target-version = ['py37', 'py38', 'py39', 'py310'] @@ -66,7 +66,7 @@ test = [ "pytest-runner", "requests-mock>=1.0,<2.0"] localize = ["doit", "ftfy"] -package = ["pyinstaller>=5.1"] +package = ["pyinstaller>=5.1", "doit"] [project.urls] repository = "https://github.com/tableau/tabcmd" [project.scripts] diff --git a/tabcmd.spec b/tabcmd.spec index a1e0421c..e28d4ca3 100644 --- a/tabcmd.spec +++ b/tabcmd.spec @@ -1,14 +1,26 @@ # -*- mode: python ; coding: utf-8 -*- - block_cipher = None +localized_strings = [ + ('tabcmd/locales/en/LC_MESSAGES/tabcmd.mo', 'tabcmd/locales/en/LC_MESSAGES'), + ('tabcmd/locales/de/LC_MESSAGES/tabcmd.mo', 'tabcmd/locales/de/LC_MESSAGES'), + ('tabcmd/locales/es/LC_MESSAGES/tabcmd.mo', 'tabcmd/locales/es/LC_MESSAGES'), + ('tabcmd/locales/fr/LC_MESSAGES/tabcmd.mo', 'tabcmd/locales/fr/LC_MESSAGES'), + ('tabcmd/locales/ga/LC_MESSAGES/tabcmd.mo', 'tabcmd/locales/ga/LC_MESSAGES'), + ('tabcmd/locales/it/LC_MESSAGES/tabcmd.mo', 'tabcmd/locales/it/LC_MESSAGES'), + ('tabcmd/locales/ja/LC_MESSAGES/tabcmd.mo', 'tabcmd/locales/ja/LC_MESSAGES'), + ('tabcmd/locales/ko/LC_MESSAGES/tabcmd.mo', 'tabcmd/locales/ko/LC_MESSAGES'), + ('tabcmd/locales/pt/LC_MESSAGES/tabcmd.mo', 'tabcmd/locales/pt/LC_MESSAGES'), + ('tabcmd/locales/sv/LC_MESSAGES/tabcmd.mo', 'tabcmd/locales/sv/LC_MESSAGES'), + ('tabcmd/locales/zh/LC_MESSAGES/tabcmd.mo', 'tabcmd/locales/zh/LC_MESSAGES'), + ] a = Analysis( ['tabcmd\\tabcmd.py'], pathex=[], binaries=[], - datas=[], + datas=localized_strings, hiddenimports=['tableauserverclient', 'requests.packages.urllib3', 'pkg_resources'], hookspath=[], hooksconfig={}, @@ -24,13 +36,17 @@ pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) exe = EXE( pyz, a.scripts, + a.binaries, + a.zipfiles, + a.datas, [], - exclude_binaries=True, name='tabcmd', debug=False, bootloader_ignore_signals=False, strip=False, upx=True, + upx_exclude=[], + runtime_tmpdir=None, console=True, disable_windowed_traceback=False, argv_emulation=False, @@ -38,13 +54,3 @@ exe = EXE( codesign_identity=None, entitlements_file=None, ) -coll = COLLECT( - exe, - a.binaries, - a.zipfiles, - a.datas, - strip=False, - upx=True, - upx_exclude=[], - name='tabcmd', -) From 51d91b0d3d671095477bab68259e5d2336b744de Mon Sep 17 00:00:00 2001 From: Jac Date: Mon, 28 Nov 2022 17:12:39 -0800 Subject: [PATCH 33/37] Jac/delete site (#202) * forget site name on new login * catch non-server errors * delete site by id, not name * changed default behavior on object creation to match old tabcmd --- tabcmd/commands/auth/session.py | 9 ++++++-- tabcmd/commands/server.py | 2 +- tabcmd/commands/site/delete_site_command.py | 23 ++++++++++++++++----- tabcmd/execution/parent_parser.py | 2 +- tests/commands/test_session.py | 2 +- 5 files changed, 28 insertions(+), 10 deletions(-) diff --git a/tabcmd/commands/auth/session.py b/tabcmd/commands/auth/session.py index 805ad387..cefc8558 100644 --- a/tabcmd/commands/auth/session.py +++ b/tabcmd/commands/auth/session.py @@ -54,11 +54,16 @@ def _update_session_data(self, args): # user id and site id are never passed in as args # last_login_using and tableau_server are internal data # self.command = args.??? - self.username = args.username or self.username + self.username = args.username or self.username or "" + self.username = self.username.lower() + self.server_url = args.server or self.server_url or "http://localhost" + self.server_url = self.server_url.lower() + if args.server is not None: + self.site_name = None self.site_name = args.site_name or self.site_name or "" + self.site_name = self.site_name.lower() if self.site_name == "default": self.site_name = "" - self.server_url = args.server or self.server_url or "http://localhost" self.logging_level = args.logging_level or self.logging_level self.password_file = args.password_file self.token_name = args.token_name or self.token_name diff --git a/tabcmd/commands/server.py b/tabcmd/commands/server.py index 6777b881..bde8c624 100644 --- a/tabcmd/commands/server.py +++ b/tabcmd/commands/server.py @@ -91,7 +91,7 @@ def get_site_for_command_or_throw(logger, server, site_name): return site_item @staticmethod - def get_site_by_name(logger, server, site_name): + def get_site_by_name(logger, server, site_name) -> TSC.SiteItem: try: # sites don't use the normal filter site_item = server.sites.get_by_name(site_name) diff --git a/tabcmd/commands/site/delete_site_command.py b/tabcmd/commands/site/delete_site_command.py index 2d210c86..babc5f41 100644 --- a/tabcmd/commands/site/delete_site_command.py +++ b/tabcmd/commands/site/delete_site_command.py @@ -17,7 +17,7 @@ class DeleteSiteCommand(Server): @staticmethod def define_args(delete_site_parser): - delete_site_parser.add_argument("site_name_to_delete", metavar="site-name", help="name of site to delete") + delete_site_parser.add_argument("site_name_to_delete", metavar="site-name", help=strings[2]) @staticmethod def run_command(args): @@ -25,9 +25,22 @@ def run_command(args): logger.debug(_("tabcmd.launching")) session = Session() server = session.create_session(args) - site_url = Server.get_site_by_name(logger, server, args.site_name_to_delete).content_url + target_site: TSC.SiteItem = Server.get_site_by_name(logger, server, args.site_name_to_delete) + target_site_id = target_site.id + logger.debug(strings[3].format(target_site_id, server.site_id)) try: - server.sites.delete(site_url) - logger.info("Successfully deleted the site") + server.sites.delete(target_site_id) except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, "Error deleting site", e) + Errors.exit_with_error(logger, strings[1], e) + except BaseException as e: + Errors.exit_with_error(logger, strings[4], e) + logger.info(strings[0].format(args.site_name_to_delete)) + + +strings = [ + "Successfully deleted site {}", + "Server responded with an error while deleting site", + "name of site to delete", + "Deleting site {0}, logged in to site {1}", + "Error while deleting site", +] diff --git a/tabcmd/execution/parent_parser.py b/tabcmd/execution/parent_parser.py index cb1a68d0..57b6d6b2 100644 --- a/tabcmd/execution/parent_parser.py +++ b/tabcmd/execution/parent_parser.py @@ -56,7 +56,7 @@ def parent_parser_with_global_options(self): parser.add_argument( "--continue-if-exists", - action="store_false", + action="store_true", help="Treat resource conflicts as item creation success e.g project already exists", ) diff --git a/tests/commands/test_session.py b/tests/commands/test_session.py index ebcf770b..8a909608 100644 --- a/tests/commands/test_session.py +++ b/tests/commands/test_session.py @@ -45,7 +45,7 @@ no_prompt=False, ) -fakeserver = "http://SRVR" +fakeserver = "http://SRVR".lower() def _set_mocks_for_json_file_saved_username(mock_json_load, auth_token, username): From 4565b7e265107b5e77e51b848fa9e076ba511f0e Mon Sep 17 00:00:00 2001 From: andyoneal Date: Tue, 6 Dec 2022 15:39:38 -0600 Subject: [PATCH 34/37] Add Datasources to the Get Command (#162) * add datasources to the get command Co-authored-by: Jac Fitzgerald --- .../datasources_and_workbooks_command.py | 13 ++ .../get_url_command.py | 118 ++++++++++++------ .../project/delete_project_command.py | 2 +- .../locales/en/tabcmd_messages_en.properties | 3 + tests/commands/test_geturl_utils.py | 2 +- tests/e2e/language_tests.py | 44 ++++++- tests/e2e/online_tests.py | 63 +++++++--- 7 files changed, 182 insertions(+), 63 deletions(-) diff --git a/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py b/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py index 1eadb3a5..2c2d421d 100644 --- a/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py +++ b/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py @@ -122,3 +122,16 @@ def save_to_file(logger, output, filename): with open(filename, "wb") as f: f.write(output) logger.info(_("export.success").format("", filename)) + + @staticmethod + def get_ds_by_content_url(logger, server, datasource_content_url) -> TSC.DatasourceItem: + logger.debug(_("export.status").format(datasource_content_url)) + try: + req_option = TSC.RequestOptions() + req_option.filter.add(TSC.Filter("contentUrl", TSC.RequestOptions.Operator.Equals, datasource_content_url)) + matching_datasources, paging = server.datasources.get(req_option) + except TSC.ServerResponseError as e: + Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response").format("")) + if len(matching_datasources) < 1: + Errors.exit_with_error(logger, message=_("dataalerts.failure.error.datasourceNotFound")) + return matching_datasources[0] diff --git a/tabcmd/commands/datasources_and_workbooks/get_url_command.py b/tabcmd/commands/datasources_and_workbooks/get_url_command.py index 688426d9..302283c1 100644 --- a/tabcmd/commands/datasources_and_workbooks/get_url_command.py +++ b/tabcmd/commands/datasources_and_workbooks/get_url_command.py @@ -1,4 +1,5 @@ import tableauserverclient as TSC +from tableauserverclient import ServerResponseError from tabcmd.commands.auth.session import Session from tabcmd.commands.constants import Errors @@ -16,6 +17,8 @@ class GetUrl(DatasourcesAndWorkbooks): name: str = "get" description: str = _("get.short_description") + valid_file_types = {"workbook": ["twbx", "twb"], "datasource": ["tdsx", "tds"], "view": ["pdf", "png", "csv"]} + valid_content_types = ["workbook", "view", "datasource"] @staticmethod def define_args(get_url_parser): @@ -30,6 +33,8 @@ def run_command(args): # A view can be returned in PDF, PNG, or CSV (summary data only) format. # A Tableau workbook is returned as a TWB if it connects to a datasource/live connection, # or a TWBX if it uses an extract. + # A Tableau datasource is returned as a TDS if it connects to a live connection, + # or a TDSX if it uses an extract. logger = log(__class__.__name__, args.logging_level) logger.debug(_("tabcmd.launching")) session = Session() @@ -38,44 +43,45 @@ def run_command(args): Errors.exit_with_error(logger, _("export.errors.white_space_workbook_view")) url = args.url.lstrip("/") # strip opening / if present - file_type = GetUrl.get_file_type_from_filename(logger, args.filename, url) content_type = GetUrl.evaluate_content_type(logger, url) + if file_type not in GetUrl.valid_file_types[content_type]: + Errors.exit_with_error( + logger, + message=_("get.errors.invalid_file_type").format( + _( + "content_type." + content_type + ), # --> _("content_type.workbook") -> "workbook", "Arbeitsmappe", etc + GetUrl.valid_file_types[content_type], + ), + ) + try: + GetUrl.get_content_as_file(file_type, content_type, logger, args, server, url) + except ServerResponseError as e: + Errors.exit_with_error(logger, e) + except BaseException as be: + Errors.exit_with_error(logger, be, "Error during file download") - if content_type == "workbook": - if file_type in ["twbx", "twb"]: - GetUrl.generate_twb(logger, server, args, file_type, url) - else: - Errors.exit_with_error( - logger, message=_("publish.errors.mutually_exclusive_option").format("twb", "twbx") - ) - else: # content type = view - view_url = GetUrl.get_view_url(url, logger) - if file_type == "pdf": - GetUrl.generate_pdf(logger, server, args, view_url) - elif file_type == "png": - GetUrl.generate_png(logger, server, args, view_url) - elif file_type == "csv": - GetUrl.generate_csv(logger, server, args, view_url) - else: - Errors.exit_with_error(logger, message=_("tabcmd.get.extension.not_found")) + ## this first set of methods is all parsing the url and file input from the user @staticmethod def evaluate_content_type(logger, url): - # specify a view to get using "views//." - # specify a workbook to get using "workbooks/.". - if url.startswith("views/"): - return "view" - elif url.startswith("workbooks/"): - return "workbook" - else: - GetUrl.explain_expected_url(logger) + # specify a view to get using "/views//." + # specify a workbook to get using "/workbooks/.". + # specify a datasource to get using "/datasources/." + content_type = "" + for content_type in GetUrl.valid_content_types: + if url.find(content_type) == 0: + return content_type + Errors.exit_with_error(logger, message=_("get.errors.invalid_content_type").format(url)) @staticmethod - def explain_expected_url(logger): - view_example = "/views//." - message = "{} [{}]".format( - _("export.errors.requires_workbook_view_param").format(__class__.__name__), view_example + def explain_expected_url(logger, url): + view_example = "/views//[.ext]" + wb_example = "/workbooks/[.ext]" + ds_example = "/datasources/ "wb-name" + def get_resource_name(url, logger): # workbooks/wb-name" -> "wb-name", datasource/ds-name -> ds-name name_parts = url.split("/") if len(name_parts) != 2: - GetUrl.explain_expected_url(logger) - workbook_name = name_parts[::-1][0] # last part - workbook_name = GetUrl.strip_query_params(workbook_name) - workbook_name = GetUrl.get_name_without_possible_extension(workbook_name) - return workbook_name + GetUrl.explain_expected_url(logger, url) + resource_name_with_params = name_parts[::-1][0] # last part + resource_name_with_ext = GetUrl.strip_query_params(resource_name_with_params) + resource_name = GetUrl.get_name_without_possible_extension(resource_name_with_ext) + return resource_name @staticmethod def get_view_url(url, logger): # "views/wb-name/view-name" -> wb-name/sheets/view-name name_parts = url.split("/") # ['views', 'wb-name', 'view-name'] if len(name_parts) != 3: - GetUrl.explain_expected_url(logger) + GetUrl.explain_expected_url(logger, url) workbook_name = name_parts[1] view_name = name_parts[::-1][0] view_name = GetUrl.strip_query_params(view_name) @@ -142,6 +148,26 @@ def filename_from_args(file_argument, item_name, filetype): file_argument = "{}.{}".format(item_name, filetype) return file_argument + ## methods below here have done all the parsing and just have to do the download and saving + ## these should be able to be shared with export + + @staticmethod + def get_content_as_file(file_type, content_type, logger, args, server, url): + if content_type == "workbook": + return GetUrl.generate_twb(logger, server, args, file_type, url) + elif content_type == "datasource": + return GetUrl.generate_tds(logger, server, args, file_type) + elif content_type == "view": + view_url = GetUrl.get_view_url(url, logger) + if file_type == "pdf": + return GetUrl.generate_pdf(logger, server, args, view_url) + elif file_type == "png": + return GetUrl.generate_png(logger, server, args, view_url) + elif file_type == "csv": + return GetUrl.generate_csv(logger, server, args, view_url) + # all the known options above will return early. If we get here we are confused. + Errors.exit_with_error(logger, message=_("tabcmd.get.extension.not_found")) + @staticmethod def generate_pdf(logger, server, args, view_url): try: @@ -185,8 +211,7 @@ def generate_csv(logger, server, args, view_url): @staticmethod def generate_twb(logger, server, args, file_extension, url): - workbook_name = GetUrl.get_workbook_name(url, logger) - + workbook_name = GetUrl.get_resource_name(url, logger) try: target_workbook = GetUrl.get_wb_by_content_url(logger, server, workbook_name) logger.debug(_("content_type.workbook") + ": {}".format(workbook_name)) @@ -196,3 +221,16 @@ def generate_twb(logger, server, args, file_extension, url): logger.info(_("export.success").format(target_workbook.name, file_name_with_path)) except TSC.ServerResponseError as e: Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) + + @staticmethod + def generate_tds(logger, server, args, file_extension): + datasource_name = GetUrl.get_resource_name(logger, args.url) + try: + target_datasource = GetUrl.get_ds_by_content_url(logger, server, datasource_name) + logger.debug(_("content_type.datasource") + ": {}".format(datasource_name)) + file_name_with_path = GetUrl.filename_from_args(args.filename, datasource_name, file_extension) + logger.debug("Saving as {}".format(file_name_with_path)) + server.datasources.download(target_datasource.id, filepath=None, no_extract=False) + logger.info(_("export.success").format(target_datasource.name, file_name_with_path)) + except TSC.ServerResponseError as e: + Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) diff --git a/tabcmd/commands/project/delete_project_command.py b/tabcmd/commands/project/delete_project_command.py index 9ea30d8d..16a8855a 100644 --- a/tabcmd/commands/project/delete_project_command.py +++ b/tabcmd/commands/project/delete_project_command.py @@ -31,7 +31,7 @@ def run_command(args): logger.debug("parent path: {}".format(args.parent_project_path)) try: - logger.debug(_("deleteproject.status").format(args.parent_project_path, args.project_name)) + logger.debug(_("deleteproject.status").format(args.parent_project_path + "/" + args.project_name)) project = Server.get_project_by_name_and_parent_path( logger, server, args.project_name, args.parent_project_path ) diff --git a/tabcmd/locales/en/tabcmd_messages_en.properties b/tabcmd/locales/en/tabcmd_messages_en.properties index 6992a7f2..e0dc83e0 100644 --- a/tabcmd/locales/en/tabcmd_messages_en.properties +++ b/tabcmd/locales/en/tabcmd_messages_en.properties @@ -165,6 +165,7 @@ export.errors.need_country_and_languge=The options --country and --language must export.errors.white_space_workbook_view=The name of the workbook or view to export cannot include spaces. Use the normalized name of the workbook or view as it appears in the URL. export.errors.requires_workbook_view_name=The ''{0}'' command requires a / name export.errors.requires_workbook_view_param=The ''{0}'' command requires a / parameter, and there must be at least one slash (/) in this parameter +export.errors.requires_resource_param=The ''{0}'' command requires a resource path in a specific format. Given: {0}. Accepted values: {1}, {2}, {3} export.options.country=If not using user''s default locale, the country abbreviation for locale (find in IANA Language Subtag Registry). Must use with --language export.options.csv=Export data in CSV format (default) export.options.fullpdf=Export visual views in PDF format (if workbook was published with tabs) @@ -185,6 +186,8 @@ fileuploader.progress=Upload progress: {0}% get.description=Get a file from the server. The file will be named after the last component of the path if no filename argument is specified get.errors.requires_url=The ''{0}'' command requires an URL +get.errors.invalid_content_type=Could not find one of the expected types to export in '{0}' +get.errors.invalid_file_type=To export a {0}, the file type must be one of {1} get.options.file=Name to save the file as get.short_description=Get a file from the server diff --git a/tests/commands/test_geturl_utils.py b/tests/commands/test_geturl_utils.py index 77d21309..117a9181 100644 --- a/tests/commands/test_geturl_utils.py +++ b/tests/commands/test_geturl_utils.py @@ -60,7 +60,7 @@ def test_get_view_without_extension_that_doesnt_have_one(self): assert GetUrl.get_name_without_possible_extension(filename) == filename def test_get_workbook_name(self): - assert GetUrl.get_workbook_name("workbooks/wbname", mock_logger) == "wbname" + assert GetUrl.get_resource_name("workbooks/wbname", mock_logger) == "wbname" def test_view_name(self): assert GetUrl.get_view_url("views/wb-name/view-name", None) == "wb-name/sheets/view-name" diff --git a/tests/e2e/language_tests.py b/tests/e2e/language_tests.py index 5e80e7a8..6fced7a3 100644 --- a/tests/e2e/language_tests.py +++ b/tests/e2e/language_tests.py @@ -74,17 +74,33 @@ def _publish_wb(self, file, name): arguments = [command, file, "--name", name, "--overwrite"] return _test_command(arguments) + def _publish_ds(self, file, name): + command = "publish" + arguments = [command, file, "--name", name, "--overwrite"] + return _test_command(arguments) + def _delete_wb(self, file): command = "delete" arguments = [command, "-w", file] _test_command(arguments) + def _delete_ds(self, file): + command = "delete" + arguments = [command, file, "--datasource"] + _test_command(arguments) + def _get_view(self, wb_name_on_server, sheet_name): server_file = "/views/" + wb_name_on_server + "/" + sheet_name command = "get" arguments = [command, server_file] _test_command(arguments) + def _get_datasource(self, server_file): + command = "get" + server_file = "/datasources/" + server_file + arguments = [command, server_file] + _test_command(arguments) + def _get_custom_view(self): command = "get" @@ -113,6 +129,8 @@ def _delete_extract(self, wb_name): TWBX_WITH_EXTRACT_SHEET = "sheet1" TWBX_FILE_WITHOUT_EXTRACT = "simple-data.twbx" TWBX_WITHOUT_EXTRACT_NAME = "WorkbookWithoutExtract" + TDSX_WITH_EXTRACT_NAME = "WorldIndicators" + TDSX_FILE_WITH_EXTRACT = "World Indicators.tdsx" @pytest.mark.order(2) def test_create_site_users(self): @@ -200,7 +218,23 @@ def test__delete_wb(self): name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME self._delete_wb(name_on_server) - @pytest.mark.order(12) + @pytest.mark.order(13) + def test_publish_ds(self): + name_on_server = OnlineCommandTest.TDSX_WITH_EXTRACT_NAME + file = os.path.join("tests", "assets", OnlineCommandTest.TDSX_FILE_WITH_EXTRACT) + self._publish_ds(file, name_on_server) + + @pytest.mark.order(14) + def test__get_ds(self): + ds_name_on_server = OnlineCommandTest.TDSX_WITH_EXTRACT_NAME + self._get_datasource(ds_name_on_server + ".tdsx") + + @pytest.mark.order(15) + def test__delete_ds(self): + name_on_server = OnlineCommandTest.TDSX_WITH_EXTRACT_NAME + self._delete_ds(name_on_server) + + @pytest.mark.order(16) def test_create_extract(self): # This workbook doesn't work for creating an extract name_on_server = OnlineCommandTest.TWBX_WITHOUT_EXTRACT_NAME @@ -208,12 +242,12 @@ def test_create_extract(self): self._publish_wb(file, name_on_server) # which damn workbook will work here self._create_extract(name_on_server) - @pytest.mark.order(13) + @pytest.mark.order(17) def test_refresh_extract(self): name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME self._refresh_extract(name_on_server) - @pytest.mark.order(14) + @pytest.mark.order(18) def test_delete_extract(self): name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME file = os.path.join("tests", "assets", OnlineCommandTest.TWBX_FILE_WITH_EXTRACT) @@ -231,7 +265,7 @@ def test_help(self): # def test_logout(self): # _test_command(["logout"]) - @pytest.mark.order(15) + @pytest.mark.order(19) def test_export(self): name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME file = os.path.join("tests", "assets", OnlineCommandTest.TWBX_FILE_WITH_EXTRACT) @@ -241,7 +275,7 @@ def test_export(self): arguments = [command, friendly_name, "--fullpdf", "-f", "exported_file.pdf"] _test_command(arguments) - @pytest.mark.order(16) + @pytest.mark.order(20) def test_delete_site_users(self): command = "deletesiteusers" users = os.path.join("tests", "assets", "usernames.csv") diff --git a/tests/e2e/online_tests.py b/tests/e2e/online_tests.py index f7da3677..236820f5 100644 --- a/tests/e2e/online_tests.py +++ b/tests/e2e/online_tests.py @@ -54,6 +54,7 @@ def _create_project(self, project_name, parent_path=None): if parent_path: arguments.append("--parent-project-path") arguments.append(parent_path) + arguments.append("--continue-if-exists") _test_command(arguments) def _delete_project(self, project_name, parent_path=None): @@ -74,11 +75,21 @@ def _publish_wb(self, file, name): arguments = [command, file, "--name", name, "--overwrite"] return _test_command(arguments) + def _publish_ds(self, file, name): + command = "publish" + arguments = [command, file, "--name", name, "--overwrite"] + return _test_command(arguments) + def _delete_wb(self, file): command = "delete" arguments = [command, file] _test_command(arguments) + def _delete_ds(self, file): + command = "delete" + arguments = [command, file, "--datasource"] + _test_command(arguments) + def _get_view(self, wb_name_on_server, sheet_name): server_file = "/views/" + wb_name_on_server + "/" + sheet_name command = "get" @@ -97,6 +108,12 @@ def _get_workbook(self, server_file): arguments = [command, server_file] _test_command(arguments) + def _get_datasource(self, server_file): + command = "get" + server_file = "/datasources/" + server_file + arguments = [command, server_file] + _test_command(arguments) + def _create_extract(self, wb_name): command = "createextracts" arguments = [command, "-w", wb_name, "--encrypt"] @@ -125,6 +142,8 @@ def _list(self, item_type: str): TWBX_FILE_WITHOUT_EXTRACT = "simple-data.twbx" TWBX_WITHOUT_EXTRACT_NAME = "WorkbookWithoutExtract" TWBX_WITHOUT_EXTRACT_SHEET = "Testsheet1" + TDSX_WITH_EXTRACT_NAME = "WorldIndicators" + TDSX_FILE_WITH_EXTRACT = "World Indicators.tdsx" @pytest.mark.order(1) def test_login(self): @@ -161,6 +180,7 @@ def test_group_creategroup(self): groupname = group_name command = "creategroup" arguments = [command, groupname] + arguments.append("--continue-if-exists") _test_command(arguments) @pytest.mark.order(4) @@ -172,6 +192,7 @@ def test_users_add_to_group(self): command = "addusers" filename = os.path.join("tests", "assets", "usernames.csv") arguments = [command, groupname, "--users", filename] + arguments.append("--continue-if-exists") _test_command(arguments) @pytest.mark.order(5) @@ -218,12 +239,6 @@ def test_create_projects(self): def test_list_projects(self): self._list("projects") - """ - @pytest.mark.order(9) - def test_publish_samples(self): - self._publish_samples(project_name) - """ - @pytest.mark.order(10) def test_delete_projects(self): if not project_admin: @@ -265,21 +280,37 @@ def test_wb_delete(self): self._delete_wb(name_on_server) @pytest.mark.order(12) - def test_extract_delete(self): + def test_publish_ds(self): + name_on_server = OnlineCommandTest.TDSX_WITH_EXTRACT_NAME + file = os.path.join("tests", "assets", OnlineCommandTest.TDSX_FILE_WITH_EXTRACT) + self._publish_ds(file, name_on_server) + + @pytest.mark.order(13) + def test__get_ds(self): + ds_name_on_server = OnlineCommandTest.TDSX_WITH_EXTRACT_NAME + self._get_datasource(ds_name_on_server + ".tdsx") + + @pytest.mark.order(14) + def test__delete_ds(self): + name_on_server = OnlineCommandTest.TDSX_WITH_EXTRACT_NAME + self._delete_ds(name_on_server) + + @pytest.mark.order(15) + def test_delete_extract(self): # fails because the extract has a bad data connection :/ name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME file = os.path.join("tests", "assets", OnlineCommandTest.TWBX_FILE_WITH_EXTRACT) self._publish_wb(file, name_on_server) self._delete_extract(name_on_server) - @pytest.mark.order(13) - def test_extract_create(self): + @pytest.mark.order(16) + def test_create_extract(self): # Fails because it 'already has an extract' :/ name_on_server = OnlineCommandTest.TWBX_WITHOUT_EXTRACT_NAME self._create_extract(name_on_server) - @pytest.mark.order(14) - def test_extract_refresh(self): + @pytest.mark.order(17) + def test_refresh_extract(self): # must be a datasource owned by the test user name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME file = os.path.join("tests", "assets", OnlineCommandTest.TWBX_FILE_WITH_EXTRACT) @@ -289,7 +320,7 @@ def test_extract_refresh(self): self._refresh_extract(name_on_server) self._delete_wb(name_on_server) - @pytest.mark.order(15) + @pytest.mark.order(19) def test_export_wb_pdf(self): name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME file = os.path.join("tests", "assets", OnlineCommandTest.TWBX_FILE_WITH_EXTRACT) @@ -299,7 +330,7 @@ def test_export_wb_pdf(self): arguments = [command, friendly_name, "--fullpdf", "-f", "exported_wb.pdf"] _test_command(arguments) - @pytest.mark.order(15) + @pytest.mark.order(19) def test_export_view_pdf(self): name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME file = os.path.join("tests", "assets", OnlineCommandTest.TWBX_FILE_WITH_EXTRACT) @@ -309,8 +340,8 @@ def test_export_view_pdf(self): arguments = [command, friendly_name, "--pdf", "-f", "exported_view.pdf"] _test_command(arguments) - @pytest.mark.order(16) - def test_users_delete_site_users(self): + @pytest.mark.order(20) + def test_delete_site_users(self): if not server_admin and not site_admin: pytest.skip("Must be server or site administrator to delete site users") @@ -318,7 +349,7 @@ def test_users_delete_site_users(self): users = os.path.join("tests", "assets", "usernames.csv") _test_command([command, users]) - @pytest.mark.order(20) + @pytest.mark.order(21) def test_list_sites(self): if not server_admin: pytest.skip("Must be server administrator to list sites") From eafe30e32d1767ac4cc45bc6fb612fb796f809d8 Mon Sep 17 00:00:00 2001 From: Jac Date: Wed, 7 Dec 2022 11:58:51 -0800 Subject: [PATCH 35/37] format command-specific help (#203) * different output based on switch used * hide token-value like we do password * refactoring: help command and parent_parser --- tabcmd/__main__.py | 3 +- .../delete_command.py | 9 +- .../export_command.py | 21 +- .../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 +- tabcmd/commands/help/__init__.py | 0 tabcmd/commands/help/help_command.py | 74 ----- .../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 | 12 +- 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 | 40 ++- tabcmd/execution/logger_config.py | 5 +- tabcmd/execution/map_of_commands.py | 2 - tabcmd/execution/parent_parser.py | 314 +++++++++++------- tabcmd/execution/tabcmd_controller.py | 10 +- tests/commands/test_run_commands.py | 8 - tests/parsers/test_parser_create_user.py | 2 +- 36 files changed, 326 insertions(+), 304 deletions(-) delete mode 100644 tabcmd/commands/help/__init__.py delete mode 100644 tabcmd/commands/help/help_command.py 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) 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..6775181a 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,15 @@ def define_args(export_parser): pagesize.Tabloid, pagesize.Unspecified, ], + type=str.lower, default="letter", help="Set the page size of the exported PDF", ) - export_parser.add_argument( - "--width", default=800, help="Set the width of the image in pixels. Default is 800 px" - ) - export_parser.add_argument("--filename", "-f", help="filename to store the exported data") - export_parser.add_argument("--height", default=600, help=_("export.options.height")) - export_parser.add_argument( + group.add_argument("--width", default=800, help="Set the width of the image in pixels. Default is 800 px") + group.add_argument("--filename", "-f", help="filename to store the exported data") + group.add_argument("--height", default=600, help=_("export.options.height")) + group.add_argument( "--filter", metavar="COLUMN:VALUE", help="View filter to apply to the view", diff --git a/tabcmd/commands/datasources_and_workbooks/get_url_command.py b/tabcmd/commands/datasources_and_workbooks/get_url_command.py index 302283c1..1fb3139d 100644 --- a/tabcmd/commands/datasources_and_workbooks/get_url_command.py +++ b/tabcmd/commands/datasources_and_workbooks/get_url_command.py @@ -22,8 +22,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/help/__init__.py b/tabcmd/commands/help/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tabcmd/commands/help/help_command.py b/tabcmd/commands/help/help_command.py deleted file mode 100644 index 6c5409d8..00000000 --- a/tabcmd/commands/help/help_command.py +++ /dev/null @@ -1,74 +0,0 @@ -import argparse -from typing import Any, List - -from tabcmd.execution.localize import _ -from tabcmd.execution.logger_config import log - - -class HelpCommand: - """ - Command to show user help options - """ - - name: str = "help" - description: str = "Show Help and exit" - - @staticmethod - def define_args(parser): - parser.add_argument("help_option", nargs="?") - - @staticmethod - def run_command(args: argparse.Namespace): - - # whaddya mean, '__class__' is not defined ??!?!!? - logger = log(__class__.__name__, args.logging_level) # type: ignore[name-defined] - logger.debug(_("tabcmd.launching")) - - # delayed import, TODO fix cyclic imports - from tabcmd.execution.map_of_commands import CommandsMap - - all_commands: List[Any] = CommandsMap.commands_hash_map - - description: str = ( - "tabcmd - Tableau Server Command Line Utility 2.0 \n \n" - "tabcmd help -- List all available commands and global options \n" - "tabcmd help -- Show Help for a specific command\n\n" - ) - - if args.help_option: - - if args.help_option in map(lambda command: command.name, all_commands): - command_objects = filter(lambda command: command.name == args.help_option, all_commands) - cli_cmd = list(command_objects)[0] - logger.info(cli_cmd.name.ljust(25) + cli_cmd.description + "\n") - command_parser = argparse.ArgumentParser(parents=[]) - cli_cmd.define_args(command_parser) - - positionals = [] - optionals = [] - for option in command_parser._actions: - if option.option_strings: - optionals.append(option) - else: - positionals.append(option) - - if positionals: - logger.info("Required arguments") - for option in positionals: - logger.info("{0} {1}{2}{3}".format(option.dest.ljust(25), "{", option.help, "}")) - if optionals: - logger.info("\nOptional arguments") - for option in optionals: - logger.info("{0} {1} ".format(option.option_strings, option.help)) - - logger.info("\nUsage") - usage = cli_cmd.name + " " - for opt in positionals: - usage = usage + opt.dest - if len(positionals) < len(command_parser._actions): - usage = usage + " [--optional arguments]" - logger.info(usage) - - else: - for cmd in all_commands: - logger.info(cmd.name + ": " + cmd.description) diff --git a/tabcmd/commands/project/create_project_command.py b/tabcmd/commands/project/create_project_command.py index b1838b48..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 16a8855a..f1397f99 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..2d7599da 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=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..e47f246f 100644 --- a/tabcmd/commands/site/edit_site_command.py +++ b/tabcmd/commands/site/edit_site_command.py @@ -19,13 +19,11 @@ class EditSiteCommand(Server): @staticmethod def define_args(edit_site_parser): - edit_site_parser.add_argument("site_name", metavar="site-name", help="editsite.options.site-name") - edit_site_parser.add_argument( - "--site-name", default=None, dest="new_site_name", help=_("editsite.options.site-name") - ) - - set_common_site_args(edit_site_parser) - set_site_status_arg(edit_site_parser) + args_group = edit_site_parser.add_argument_group(title=EditSiteCommand.name) + args_group.add_argument("site_name", metavar="site-name", help="editsite.options.site-name") + args_group.add_argument("--site-name", default=None, dest="new_site_name", help=_("editsite.options.site-name")) + set_common_site_args(args_group) + set_site_status_arg(args_group) @staticmethod def run_command(args): 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..0e66cd3c 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 -# TODO make this lower case? +site_roles = [ + "ServerAdministrator", + "SiteAdministratorCreator", + "SiteAdministratorExplorer", + "SiteAdministrator", + "Creator", + "ExplorerCanPublish", + "Publisher", + "Explorer", + "Interactor", + "Viewer", + "Unlicensed", +] + + def set_role_arg(parser): parser.add_argument( "-r", "--role", - choices=[ - "ServerAdministrator", - "SiteAdministratorCreator", - "SiteAdministratorExplorer", - "SiteAdministrator", - "Creator", - "ExplorerCanPublish", - "Publisher", - "Explorer", - "Interactor", - "Viewer", - "Unlicensed", - ], - help="Specifies a site role for all users in the .csv file.", + choices=list(map(lambda x: x.lower(), site_roles)), + type=str.lower, + help="Specifies a site role for all users in the .csv file. Possible roles: " + ", ".join(site_roles), + metavar="SITE_ROLE", ) return parser @@ -202,6 +206,7 @@ def set_site_status_arg(parser): parser.add_argument( "--status", choices=["ACTIVE", "SUSPENDED"], + type=str.upper, help="Set to ACTIVE to activate a site, or to SUSPENDED to suspend a site.", ) return parser @@ -244,8 +249,9 @@ def set_common_site_args(parser): parser.add_argument( "--run-now-enabled", - help="Allow or deny users from running extract refreshes, flows, or schedules manually. \ - true to allow users to run tasks manually or false to prevent users from running tasks manually.", + choices=["true", "false"], + type=str.lower, + help="Allow or deny users from running extract refreshes, flows, or schedules manually.", ) return parser diff --git a/tabcmd/execution/logger_config.py b/tabcmd/execution/logger_config.py index 8a42e2cb..17fe6907 100644 --- a/tabcmd/execution/logger_config.py +++ b/tabcmd/execution/logger_config.py @@ -6,7 +6,7 @@ FORMATS = { logging.ERROR: "%(asctime)s %(levelname)-5s:(%(name)-10s %(filename)-10s: %(lineno)d): %(message)-30s", logging.WARN: "%(asctime)s %(levelname)-5s: (%(name)-10s %(filename)-10s: %(lineno)d): %(message)-30s", - logging.INFO: "%(filename)-10s: %(message)-30s", + logging.INFO: "%(message)-30s", logging.DEBUG: "%(asctime)s %(levelname)-5s: (%(name)-10s %(filename)-10s: %(lineno)d): %(message)-30s", } @@ -45,6 +45,9 @@ def configure_log(name: str, logging_level_input: str): """function for logging statements to console and logfile""" logging_level = getattr(logging, logging_level_input.upper()) log_format = FORMATS[logging_level] + if logging_level is not logging.INFO: + FORMATS[logging.INFO] = "%(filename)-10s: %(message)-30s" + logging.basicConfig( level=logging_level, format=log_format, filename="tabcmd.log", filemode="a", datefmt="%Y-%m" "-%d " "%H:%M:%S" ) diff --git a/tabcmd/execution/map_of_commands.py b/tabcmd/execution/map_of_commands.py index c6f7a679..4b60e854 100644 --- a/tabcmd/execution/map_of_commands.py +++ b/tabcmd/execution/map_of_commands.py @@ -12,7 +12,6 @@ from tabcmd.commands.extracts.refresh_extracts_command import * from tabcmd.commands.group.create_group_command import * from tabcmd.commands.group.delete_group_command import * -from tabcmd.commands.help.help_command import * from tabcmd.commands.project.create_project_command import * from tabcmd.commands.project.delete_project_command import * from tabcmd.commands.project.publish_samples_command import * @@ -50,7 +49,6 @@ class CommandsMap: EncryptExtracts, ExportCommand, GetUrl, - HelpCommand, ListSiteCommand, ListCommand, LoginCommand, diff --git a/tabcmd/execution/parent_parser.py b/tabcmd/execution/parent_parser.py index 57b6d6b2..a2495792 100644 --- a/tabcmd/execution/parent_parser.py +++ b/tabcmd/execution/parent_parser.py @@ -1,5 +1,10 @@ import argparse +import logging + from .localize import _ +from .logger_config import log +from .map_of_commands import CommandsMap + # when we drop python 3.8, this could be replaced with this lighter weight option # from importlib.metadata import version, PackageNotFoundError @@ -12,143 +17,212 @@ pass +""" +Note: output order is influenced first by grouping, then by order they are added in here +Most of this function is about making the help output look nice. +Argparse uses argument groups to separate arguments in the help output - but that doesn't +work quite as documented when in nested parsers, like we have. +Everything that is just added directly to the parser will be in the default set of +'optional arguments' directly on the newly created parser, which is renamed 'behavior arguments' to +differentiate from the signin/connection options. +Everything we add to a mutually-exclusive-group in here will be in the default set of +'optional arguments' on the *parent* parser, which is displayed all together. To make this a nice set, +many arguments are added to a mutually-exclusive-group of one argument. They are named things like +'formatting_group1' to make it clear this is a formatting choice, not functional. +The arguments for each command must be added to a group for that command. +""" + + +def parent_parser_with_global_options(): + parser = argparse.ArgumentParser(usage=argparse.SUPPRESS, add_help=False) + parser._optionals.title = strings[0] + + formatting_group1 = parser.add_mutually_exclusive_group() + formatting_group1.add_argument( + "-s", + "--server", + default=None, # default is handled in Session class + metavar="", + help=_("session.options.server"), + ) + + formatting_group2 = parser.add_mutually_exclusive_group() + formatting_group2.add_argument( + "-t", "--site", default="", dest="site_name", metavar="SITEID", help=_("session.options.site") + ) + + auth_options = parser.add_mutually_exclusive_group() + auth_options.add_argument("--token-name", default=None, metavar="", help=strings[13]) + auth_options.add_argument("-u", "--username", default=None, metavar="", help=_("session.options.username")) + + secret_values = parser.add_mutually_exclusive_group() + secret_values.add_argument( + "--token-value", + default=None, + metavar="", + help=strings[12], + ) + secret_values.add_argument( + "-p", "--password", default=None, metavar="", help=_("session.options.password") + ) + secret_values.add_argument( + "--password-file", default=None, metavar="", help=_("session.options.password-file") + ) + secret_values.add_argument("--token-file", default=None, metavar="", help=strings[11]) + + formatting_group3 = parser.add_mutually_exclusive_group() + formatting_group3.add_argument("--no-prompt", action="store_true", help=_("session.options.no-prompt")) + + certificates = parser.add_mutually_exclusive_group() + certificates.add_argument( + "-c", + "--use-certificate", + dest="certificate", + default=None, + metavar="", + help=_("session.options.use-certificate"), + ) + certificates.add_argument( + "--no-certcheck", + action="store_true", + help=_("session.options.no-certcheck"), + ) + + formatting_group4 = parser.add_mutually_exclusive_group() + formatting_group4.add_argument("--no-cookie", action="store_true", help=_("session.options.no-cookie")) + + proxy_group = parser.add_mutually_exclusive_group() + proxy_group.add_argument( + "-x", "--proxy", dest="proxy", default=None, metavar="", help=_("session.options.proxy") + ) + proxy_group.add_argument( + "--no-proxy", + action="store_false", + help=_("session.options.no-proxy"), + ) + + formatting_group5 = parser.add_mutually_exclusive_group() + formatting_group5.add_argument( + "--timeout", + default=None, # default is handled in Session class + metavar="", # can't use -t, it's already used for --site + help=_("session.options.timeout"), + ) + + # general behavioral options + parser.add_argument( + "--continue-if-exists", + action="store_false", + help=strings[9], + ) + + parser.add_argument( + "--country", + choices=["de", "en", "es", "fr", "it", "ja", "ko", "pt", "sv", "zh"], + type=str.lower, # coerce input to lowercase to act case insensitive + help=_("export.options.country"), + ) + + parser.add_argument( + "--language", + choices=["de", "en", "es", "fr", "it", "ja", "ko", "pt", "sv", "zh"], + type=str.lower, # coerce input to lowercase to act case insensitive + help=strings[10], + ) + + parser.add_argument( + "-l", + "--logging-level", + choices=["TRACE", "DEBUG", "INFO", "ERROR"], + type=str.upper, # coerce input to uppercase to act case insensitive + default="info", + help=strings[8], + ) + + parser.add_argument( + "-v", + "--version", + action="version", + version=strings[6] + "v" + version + "\n \n", + help=strings[7], + ) + return parser + + class ParentParser: # Ref https://docs.python.org/3/library/argparse.html """Parser that will be inherited by all commands. Contains authentication and logging level setting""" def __init__(self): - self.global_options = self.parent_parser_with_global_options() - self.root = argparse.ArgumentParser(parents=[self.global_options]) + self.global_options = parent_parser_with_global_options() + self.root = argparse.ArgumentParser( + prog="tabcmd", description=strings[15], parents=[self.global_options], epilog=strings[2] + ) + self.root._optionals.title = strings[1] # https://stackoverflow.com/questions/7498595/python-argparse-add-argument-to-multiple-subparsers - self.subparsers = self.root.add_subparsers() + self.subparsers = self.root.add_subparsers( + title=strings[3], + description=strings[4], + metavar=strings[5], # instead of printing the list of choices + ) def get_root_parser(self): return self.root + def connect_commands(self): + commands = CommandsMap.commands_hash_map + for command in commands: + self.include(command) + return self.root + def include(self, command): additional_parser = self.subparsers.add_parser( command.name, help=command.description, parents=[self.global_options] ) + additional_parser._optionals.title = strings[1] # This line is where we actually set each parser to call the correct command additional_parser.set_defaults(func=command) command.define_args(additional_parser) return additional_parser - # ordered alphabetically by short option - this is reflected directly in help output - def parent_parser_with_global_options(self): - parser = argparse.ArgumentParser(usage=argparse.SUPPRESS, add_help=False) - - certificates = parser.add_mutually_exclusive_group() - certificates.add_argument( - "-c", - "--use-certificate", - dest="certificate", - default=None, - metavar="", - help=_("session.options.use-certificate"), - ) - certificates.add_argument( - "--no-certcheck", - action="store_true", - help=_("session.options.no-certcheck"), - ) - - parser.add_argument( - "--continue-if-exists", - action="store_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") - ) - - secret_values = parser.add_mutually_exclusive_group() - secret_values.add_argument( - "--token-value", - default=None, - metavar="", - help="Use the specified Tableau Server Personal Access Token. Requires --token-name to be set.", - ) - secret_values.add_argument( - "-p", "--password", default=None, metavar="", help=_("session.options.password") - ) - secret_values.add_argument( - "--password-file", default=None, metavar="", help=_("session.options.password-file") - ) - - proxy_group = parser.add_mutually_exclusive_group() - proxy_group.add_argument( - "-x", "--proxy", dest="proxy", default=None, metavar="", help=_("session.options.proxy") - ) - proxy_group.add_argument( - "--no-proxy", - action="store_false", - help=_("session.options.no-proxy"), - ) - - parser.add_argument( - "-s", - "--server", - default=None, # default is handled in Session class - metavar="", - help=_("session.options.server"), - ) - parser.add_argument( - "-t", "--site", default="", dest="site_name", metavar="SITEID", help=_("session.options.site") - ) - - parser.add_argument( - "--timeout", - default=None, # default is handled in Session class - metavar="", # can't use -t, it's already used for --site - help=_("session.options.timeout"), - ) - - # TODO get the list of choices dynamically? - parser.add_argument( - "--language", - choices=["de", "en", "es", "fr", "it", "ja", "ko", "pt", "sv", "zh"], - help="Set the language to use. Exported data will be returned in this lang/locale." - "If not set, the client will use your computer locale, and the server will use your user account locale", - ) - - parser.add_argument( - "--country", - choices=["de", "en", "es", "fr", "it", "ja", "ko", "pt", "sv", "zh"], - help=_("export.options.country"), - ) - - # -h goes to argparse default help - - parser.add_argument( - "-v", - "--version", - action="version", - version="Tableau Server Command Line Utility v" + version + "\n \n", - help="Show version information and exit.", - ) - - return parser + def include_help(self): + additional_parser = self.subparsers.add_parser("help", help=strings[14], parents=[self.global_options]) + additional_parser._optionals.title = strings[1] + additional_parser.set_defaults(func=Help(self)) + + +class Help: + + parser = None + # This needs to have access to the parser when it gets called + def __init__(self, _parser: ParentParser): + self.parser = _parser + + def run_command(self, args): + logger = log(__name__, "info") + logger.info(strings[6] + " " + version + "\n") + logger.info(self.parser.root.format_help()) + + +strings = [ + "global behavioral arguments", # 0 - global_behavior_args + "global connection arguments", # 1 - global_conn_args + "For more help see https://tableau.github.io/tabcmd/", # 2 - for_more_help + "list of tabcmd commands", # 3 + "For help on a specific command use 'tabcmd -h'.", # 4 + "{ [command args]}", # 5 + "Tableau Server Command Line Utility", # 6 + "Show version information and exit.", # 7 + "Use the specified logging level. The default level is INFO.", # 8 + "Treat resource conflicts as item creation success e.g project already exists", # 9 + "Set the language to use. Exported data will be returned in this lang/locale.\n \ + If not set, the client will use your computer locale, and the server will use \ + your user account locale", # 10 + "Read the Personal Access Token from a file.", # 11 + "Use the specified Tableau Server Personal Access Token. Requires --token-name to be set.", # 12 + "The name of the Tableau Server Personal Access Token. If using a token to sign in,\ + this is required at least once to begin session.", # 13 + "Show message listing commands and global options, then exit", # 14 + "tabcmd -- Run a specific command", # 15 +] diff --git a/tabcmd/execution/tabcmd_controller.py b/tabcmd/execution/tabcmd_controller.py index 0dee0ad0..001b2b5b 100644 --- a/tabcmd/execution/tabcmd_controller.py +++ b/tabcmd/execution/tabcmd_controller.py @@ -2,7 +2,7 @@ import sys from .localize import set_client_locale -from .map_of_commands import * +from .logger_config import log from .parent_parser import ParentParser @@ -10,10 +10,8 @@ class TabcmdController: @staticmethod def initialize(): manager = ParentParser() - parent = manager.get_root_parser() - commands = CommandsMap.commands_hash_map - for command in commands: - manager.include(command) + parent = manager.connect_commands() + manager.include_help() return parent # during normal execution, leaving input as none will default to sys.argv @@ -29,7 +27,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) 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) 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 35ff22ad5d2000277606e0881239ce76817f75cb Mon Sep 17 00:00:00 2001 From: Jac Date: Wed, 7 Dec 2022 15:35:45 -0800 Subject: [PATCH 36/37] Tfs 1482014 publishing (#205) * make db-auth/oauth mutually exclusive * Create a db credentials item for publishing with --db-username, etc arguments --- tabcmd/commands/constants.py | 8 ++- .../datasources_and_workbooks_command.py | 33 ++++++----- .../get_url_command.py | 34 ++++++++---- .../publish_command.py | 24 +++++++- tabcmd/commands/server.py | 6 +- tabcmd/commands/site/delete_site_command.py | 2 +- tabcmd/execution/global_options.py | 13 +++-- tests/commands/test_run_commands.py | 2 + tests/e2e/online_tests.py | 55 ++++++++++++++----- 9 files changed, 122 insertions(+), 55 deletions(-) diff --git a/tabcmd/commands/constants.py b/tabcmd/commands/constants.py index 0eaa9b3c..ba4e6288 100644 --- a/tabcmd/commands/constants.py +++ b/tabcmd/commands/constants.py @@ -44,10 +44,11 @@ def log_stack(logger): start = 0 n_lines = 5 logger.trace(HEADER_FMT % (file, func)) - for frame in stack[start + 1 : n_lines]: + + for frame in stack[start + 2 : n_lines]: file, line, func = frame[1:4] logger.trace(STACK_FMT % (file, line, func)) - except BaseException as e: + except Exception as e: logger.info("Error printing stack trace:", e) @staticmethod @@ -64,10 +65,11 @@ def exit_with_error(logger, message=None, exception=None): # "session.session_expired_login")) # session.renew_session() if message: - logger.debug(message) + logger.debug("Error message: " + message) Errors.check_common_error_codes_and_explain(logger, exception) except Exception as exc: print("Error during log call from exception - {} {}".format(exc.__class__, message)) + print("Exiting...") sys.exit(1) @staticmethod diff --git a/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py b/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py index 2c2d421d..e59851ab 100644 --- a/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py +++ b/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py @@ -23,6 +23,7 @@ def get_view_by_content_url(logger, server, view_content_url) -> TSC.ViewItem: try: req_option = TSC.RequestOptions() req_option.filter.add(TSC.Filter("contentUrl", TSC.RequestOptions.Operator.Equals, view_content_url)) + logger.trace(req_option.get_query_params()) matching_views, paging = server.views.get(req_option) except TSC.ServerResponseError as e: Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response").format(e)) @@ -36,6 +37,7 @@ def get_wb_by_content_url(logger, server, workbook_content_url) -> TSC.WorkbookI try: req_option = TSC.RequestOptions() req_option.filter.add(TSC.Filter("contentUrl", TSC.RequestOptions.Operator.Equals, workbook_content_url)) + logger.trace(req_option.get_query_params()) matching_workbooks, paging = server.workbooks.get(req_option) except TSC.ServerResponseError as e: Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response").format("")) @@ -43,6 +45,20 @@ def get_wb_by_content_url(logger, server, workbook_content_url) -> TSC.WorkbookI Errors.exit_with_error(logger, message=_("dataalerts.failure.error.workbookNotFound")) return matching_workbooks[0] + @staticmethod + def get_ds_by_content_url(logger, server, datasource_content_url) -> TSC.DatasourceItem: + logger.debug(_("export.status").format(datasource_content_url)) + try: + req_option = TSC.RequestOptions() + req_option.filter.add(TSC.Filter("contentUrl", TSC.RequestOptions.Operator.Equals, datasource_content_url)) + logger.trace(req_option.get_query_params()) + matching_datasources, paging = server.datasources.get(req_option) + except TSC.ServerResponseError as e: + Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response").format("")) + if len(matching_datasources) < 1: + Errors.exit_with_error(logger, message=_("dataalerts.failure.error.datasourceNotFound")) + return matching_datasources[0] + @staticmethod def apply_values_from_url_params(request_options: TSC.PDFRequestOptions, url, logger) -> None: # should be able to replace this with request_options._append_view_filters(params) @@ -63,7 +79,7 @@ def apply_values_from_url_params(request_options: TSC.PDFRequestOptions, url, lo else: # it must be a filter DatasourcesAndWorkbooks.apply_filter_value(request_options, value, logger) - except BaseException as e: + except Exception as e: logger.warn("Error building filter params", e) # ExportCommand.log_stack(logger) # type: ignore @@ -86,7 +102,7 @@ def apply_option_value(request_options: TSC.PDFRequestOptions, value: str, logge logger.debug("Set max age to {} from {}".format(request_options.max_age, value)) elif ":size" == setting[0]: height, width = setting[1].split(",") - logger.warn("Height/weight parameters not yet implemented ({})".format(value)) + logger.warn("Height/width parameters not yet implemented ({})".format(value)) else: logger.debug("Parameter[s] not recognized: {}".format(value)) @@ -122,16 +138,3 @@ def save_to_file(logger, output, filename): with open(filename, "wb") as f: f.write(output) logger.info(_("export.success").format("", filename)) - - @staticmethod - def get_ds_by_content_url(logger, server, datasource_content_url) -> TSC.DatasourceItem: - logger.debug(_("export.status").format(datasource_content_url)) - try: - req_option = TSC.RequestOptions() - req_option.filter.add(TSC.Filter("contentUrl", TSC.RequestOptions.Operator.Equals, datasource_content_url)) - matching_datasources, paging = server.datasources.get(req_option) - except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response").format("")) - if len(matching_datasources) < 1: - Errors.exit_with_error(logger, message=_("dataalerts.failure.error.datasourceNotFound")) - return matching_datasources[0] diff --git a/tabcmd/commands/datasources_and_workbooks/get_url_command.py b/tabcmd/commands/datasources_and_workbooks/get_url_command.py index 1fb3139d..e4ec7faf 100644 --- a/tabcmd/commands/datasources_and_workbooks/get_url_command.py +++ b/tabcmd/commands/datasources_and_workbooks/get_url_command.py @@ -1,3 +1,5 @@ +import inspect + import tableauserverclient as TSC from tableauserverclient import ServerResponseError @@ -60,7 +62,7 @@ def run_command(args): GetUrl.get_content_as_file(file_type, content_type, logger, args, server, url) except ServerResponseError as e: Errors.exit_with_error(logger, e) - except BaseException as be: + except Exception as be: Errors.exit_with_error(logger, be, "Error during file download") ## this first set of methods is all parsing the url and file input from the user @@ -77,12 +79,14 @@ def evaluate_content_type(logger, url): Errors.exit_with_error(logger, message=_("get.errors.invalid_content_type").format(url)) @staticmethod - def explain_expected_url(logger, url): + def explain_expected_url(logger, url: str, command: str): view_example = "/views//[.ext]" wb_example = "/workbooks/[.ext]" ds_example = "/datasources/ "wb-name", datasource/ds-name -> ds-name + def get_resource_name(url: str, logger): # workbooks/wb-name" -> "wb-name", datasource/ds-name -> ds-name + url = url.lstrip("/") # strip opening / if present name_parts = url.split("/") if len(name_parts) != 2: - GetUrl.explain_expected_url(logger, url) + GetUrl.explain_expected_url(logger, url, "GetURl") resource_name_with_params = name_parts[::-1][0] # last part resource_name_with_ext = GetUrl.strip_query_params(resource_name_with_params) resource_name = GetUrl.get_name_without_possible_extension(resource_name_with_ext) @@ -136,7 +141,7 @@ def get_resource_name(url, logger): # workbooks/wb-name" -> "wb-name", datasour def get_view_url(url, logger): # "views/wb-name/view-name" -> wb-name/sheets/view-name name_parts = url.split("/") # ['views', 'wb-name', 'view-name'] if len(name_parts) != 3: - GetUrl.explain_expected_url(logger, url) + GetUrl.explain_expected_url(logger, url, "GetURl") workbook_name = name_parts[1] view_name = name_parts[::-1][0] view_name = GetUrl.strip_query_params(view_name) @@ -171,6 +176,7 @@ def get_content_as_file(file_type, content_type, logger, args, server, url): @staticmethod def generate_pdf(logger, server, args, view_url): + logger.trace("Entered method " + inspect.stack()[0].function) try: view_item: TSC.ViewItem = GetUrl.get_view_by_content_url(logger, server, view_url) logger.debug(_("content_type.view") + ": {}".format(view_item.name)) @@ -184,6 +190,7 @@ def generate_pdf(logger, server, args, view_url): @staticmethod def generate_png(logger, server, args, view_url): + logger.trace("Entered method " + inspect.stack()[0].function) try: view_item: TSC.ViewItem = GetUrl.get_view_by_content_url(logger, server, view_url) logger.debug(_("content_type.view") + ": {}".format(view_item.name)) @@ -197,6 +204,7 @@ def generate_png(logger, server, args, view_url): @staticmethod def generate_csv(logger, server, args, view_url): + logger.trace("Entered method " + inspect.stack()[0].function) try: view_item: TSC.ViewItem = GetUrl.get_view_by_content_url(logger, server, view_url) logger.debug(_("content_type.view") + ": {}".format(view_item.name)) @@ -212,6 +220,7 @@ def generate_csv(logger, server, args, view_url): @staticmethod def generate_twb(logger, server, args, file_extension, url): + logger.trace("Entered method " + inspect.stack()[0].function) workbook_name = GetUrl.get_resource_name(url, logger) try: target_workbook = GetUrl.get_wb_by_content_url(logger, server, workbook_name) @@ -225,7 +234,8 @@ def generate_twb(logger, server, args, file_extension, url): @staticmethod def generate_tds(logger, server, args, file_extension): - datasource_name = GetUrl.get_resource_name(logger, args.url) + logger.trace("Entered method " + inspect.stack()[0].function) + datasource_name = GetUrl.get_resource_name(args.url, logger) try: target_datasource = GetUrl.get_ds_by_content_url(logger, server, datasource_name) logger.debug(_("content_type.datasource") + ": {}".format(datasource_name)) diff --git a/tabcmd/commands/datasources_and_workbooks/publish_command.py b/tabcmd/commands/datasources_and_workbooks/publish_command.py index c529604b..93a6fe0b 100644 --- a/tabcmd/commands/datasources_and_workbooks/publish_command.py +++ b/tabcmd/commands/datasources_and_workbooks/publish_command.py @@ -51,15 +51,31 @@ def run_command(args): args.project_name = "default" args.parent_project_path = "" - publish_mode = PublishCommand.get_publish_mode(args) + publish_mode = PublishCommand.get_publish_mode(args) # --overwrite, --replace logger.info("Publishing as " + publish_mode) + if args.db_username: + creds = TSC.models.ConnectionCredentials(args.db_username, args.db_password, embed=args.save_db_password) + elif args.oauth_username: + creds = TSC.models.ConnectionCredentials(args.oauth_username, None, embed=False, oauth=args.save_oauth) + else: + logger.debug("No db-username or oauth-username found in command") + creds = None + source = PublishCommand.get_filename_extension_if_tableau_type(logger, args.filename) logger.info(_("publish.status").format(args.filename)) if source in ["twbx", "twb"]: new_workbook = TSC.WorkbookItem(project_id, name=args.name, show_tabs=args.tabbed) try: - new_workbook = server.workbooks.publish(new_workbook, args.filename, publish_mode) + new_workbook = server.workbooks.publish( + new_workbook, + args.filename, + publish_mode, + connection_credentials=creds, + as_job=False, + skip_connection_check=False, + ) + except IOError as ioe: Errors.exit_with_error(logger, ioe) logger.info(_("publish.success") + "\n{}".format(new_workbook.webpage_url)) @@ -68,7 +84,9 @@ def run_command(args): new_datasource = TSC.DatasourceItem(project_id, name=args.name) new_datasource.use_remote_query_agent = args.use_tableau_bridge try: - new_datasource = server.datasources.publish(new_datasource, args.filename, publish_mode) + new_datasource = server.datasources.publish( + new_datasource, args.filename, publish_mode, connection_credentials=creds + ) except IOError as ioe: Errors.exit_with_error(logger, exc) logger.info(_("publish.success") + "\n{}".format(new_datasource.webpage_url)) diff --git a/tabcmd/commands/server.py b/tabcmd/commands/server.py index bde8c624..f215e3a8 100644 --- a/tabcmd/commands/server.py +++ b/tabcmd/commands/server.py @@ -45,10 +45,10 @@ def find_user(logger, server, username): @staticmethod def get_items_by_name(logger, item_endpoint, item_name: str, container: Optional[TSC.ProjectItem] = None) -> List: # TODO: typing should reflect that this returns TSC.TableauItem and item_endpoint is of type TSC.QuerysetEndpoint[same] - item_type = type(item_endpoint).__name__ - item_log_name: str = "[" + item_type + "] " + item_name + item_log_name: str = "[{0}] {1}".format(type(item_endpoint).__name__, item_name) if container: - item_log_name = str(container) + "/" + item_log_name + container_name: str = "({0}) {1}".format(container.__class__, container.name) + item_log_name = "{0}/{1}".format(container_name, item_log_name) logger.debug(_("export.status").format(item_log_name)) req_option = TSC.RequestOptions() req_option.filter.add(TSC.Filter(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Operator.Equals, item_name)) diff --git a/tabcmd/commands/site/delete_site_command.py b/tabcmd/commands/site/delete_site_command.py index 2d7599da..854c4dd9 100644 --- a/tabcmd/commands/site/delete_site_command.py +++ b/tabcmd/commands/site/delete_site_command.py @@ -33,7 +33,7 @@ def run_command(args): server.sites.delete(target_site_id) except TSC.ServerResponseError as e: Errors.exit_with_error(logger, strings[1], e) - except BaseException as e: + except Exception as e: Errors.exit_with_error(logger, strings[4], e) logger.info(strings[0].format(args.site_name_to_delete)) diff --git a/tabcmd/execution/global_options.py b/tabcmd/execution/global_options.py index 0e66cd3c..61de456b 100644 --- a/tabcmd/execution/global_options.py +++ b/tabcmd/execution/global_options.py @@ -291,10 +291,15 @@ def set_publish_args(parser): parser.add_argument("-n", "--name", help="Name to publish the new datasource or workbook by.") set_overwrite_option(parser) - parser.add_argument( + + creds = parser.add_mutually_exclusive_group() + creds.add_argument("--oauth-username", help="The email address of a preconfigured OAuth connection") + creds.add_argument( "--db-username", help="Use this option to publish a database user name with the workbook, data source, or data extract.", ) + parser.add_argument("--save-oauth", action="store_true", help="Save embedded OAuth credentials in the datasource") + parser.add_argument( "--db-password", help="publish a database password with the workbook, data source, or extract", @@ -304,6 +309,7 @@ def set_publish_args(parser): action="store_true", help="Stores the provided database password on the server.", ) + parser.add_argument( "--tabbed", action="store_true", @@ -320,14 +326,11 @@ def set_publish_args(parser): action="store_true", help="Encrypt extracts in the workbook, datasource, or extract being published to the server", ) - parser.add_argument("--oauth-username", help="The email address of a preconfigured OAuth connection") - parser.add_argument("--save-oauth", action="store_true", help="Save embedded OAuth credentials in the datasource") - thumbnails = parser.add_mutually_exclusive_group() thumbnails.add_argument("--thumbnail-username", help="Not yet implemented") thumbnails.add_argument("--thumbnail-group", help="Not yet implemented") # not implemented in the REST API - parser.add_argument("--use-tableau-bridge", help="Refresh datasource through Tableau Bridge") + parser.add_argument("--use-tableau-bridge", action="store_true", help="Refresh datasource through Tableau Bridge") def set_overwrite_option(parser): diff --git a/tests/commands/test_run_commands.py b/tests/commands/test_run_commands.py index a7e03597..7a1afbe2 100644 --- a/tests/commands/test_run_commands.py +++ b/tests/commands/test_run_commands.py @@ -138,6 +138,8 @@ def test_publish(self, mock_session, mock_server): mock_args.parent_project_path = "projects" mock_args.name = "" mock_args.tabbed = True + mock_args.db_username = None + mock_args.oauth_username = None mock_server.projects = getter publish_command.PublishCommand.run_command(mock_args) mock_session.assert_called() diff --git a/tests/e2e/online_tests.py b/tests/e2e/online_tests.py index 236820f5..abd10b71 100644 --- a/tests/e2e/online_tests.py +++ b/tests/e2e/online_tests.py @@ -4,6 +4,7 @@ import time import unittest +from credentials import waremart_password, waremart_user from tests.e2e import setup_e2e debug_log = "--logging-level=DEBUG" @@ -70,15 +71,28 @@ def _publish_samples(self, project_name): arguments = [command, "--name", project_name] _test_command(arguments) - def _publish_wb(self, file, name): + def _publish_args(self, file, name, tabbed=None): command = "publish" arguments = [command, file, "--name", name, "--overwrite"] - return _test_command(arguments) - - def _publish_ds(self, file, name): - command = "publish" - arguments = [command, file, "--name", name, "--overwrite"] - return _test_command(arguments) + return arguments + + def _publish_creds_args( + self, arguments, db_user=None, db_pass=None, db_save=None, oauth_user=None, oauth_save=None + ): + if db_user: + arguments.append("--db-username") + arguments.append(db_user) + if db_pass: + arguments.append("--db-password") + arguments.append(db_pass) + if db_save: + arguments.append("--save-db-password") + if oauth_user: + arguments.append("--oauth-username") + arguments.append(oauth_user) + if oauth_save: + arguments.append("--save-oauth") + return arguments def _delete_wb(self, file): command = "delete" @@ -144,6 +158,7 @@ def _list(self, item_type: str): TWBX_WITHOUT_EXTRACT_SHEET = "Testsheet1" TDSX_WITH_EXTRACT_NAME = "WorldIndicators" TDSX_FILE_WITH_EXTRACT = "World Indicators.tdsx" + TWB_WITH_EMBEDDED_CONNECTION = "embedded_connection_waremart.twb" @pytest.mark.order(1) def test_login(self): @@ -250,7 +265,8 @@ def test_delete_projects(self): def test_wb_publish(self): name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME file = os.path.join("tests", "assets", OnlineCommandTest.TWBX_FILE_WITH_EXTRACT) - self._publish_wb(file, name_on_server) + arguments = self._publish_args(file, name_on_server) + _test_command(arguments) @pytest.mark.order(10) def test_wb_get(self): @@ -279,11 +295,20 @@ def test_wb_delete(self): name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME self._delete_wb(name_on_server) + @pytest.mark.order(11) + def test_wb_publish_embedded(self): + name_on_server = OnlineCommandTest.TWB_WITH_EMBEDDED_CONNECTION + file = os.path.join("tests", "assets", OnlineCommandTest.TWB_WITH_EMBEDDED_CONNECTION) + arguments = self._publish_args(file, name_on_server) + arguments = self._publish_creds_args(arguments, waremart_user, waremart_password, True) + _test_command(arguments) + @pytest.mark.order(12) def test_publish_ds(self): name_on_server = OnlineCommandTest.TDSX_WITH_EXTRACT_NAME file = os.path.join("tests", "assets", OnlineCommandTest.TDSX_FILE_WITH_EXTRACT) - self._publish_ds(file, name_on_server) + arguments = self._publish_args(file, name_on_server) + _test_command(arguments) @pytest.mark.order(13) def test__get_ds(self): @@ -300,7 +325,7 @@ def test_delete_extract(self): # fails because the extract has a bad data connection :/ name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME file = os.path.join("tests", "assets", OnlineCommandTest.TWBX_FILE_WITH_EXTRACT) - self._publish_wb(file, name_on_server) + self._publish_args(file, name_on_server) self._delete_extract(name_on_server) @pytest.mark.order(16) @@ -314,7 +339,8 @@ def test_refresh_extract(self): # must be a datasource owned by the test user name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME file = os.path.join("tests", "assets", OnlineCommandTest.TWBX_FILE_WITH_EXTRACT) - self._publish_wb(file, name_on_server) + arguments = self._publish_args(file, name_on_server) + _test_command(arguments) name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME self._refresh_extract(name_on_server) @@ -324,7 +350,9 @@ def test_refresh_extract(self): def test_export_wb_pdf(self): name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME file = os.path.join("tests", "assets", OnlineCommandTest.TWBX_FILE_WITH_EXTRACT) - self._publish_wb(file, name_on_server) + arguments = self._publish_args(file, name_on_server) + _test_command(arguments) + command = "export" friendly_name = name_on_server + "/" + OnlineCommandTest.TWBX_WITH_EXTRACT_SHEET arguments = [command, friendly_name, "--fullpdf", "-f", "exported_wb.pdf"] @@ -334,7 +362,8 @@ def test_export_wb_pdf(self): def test_export_view_pdf(self): name_on_server = OnlineCommandTest.TWBX_WITH_EXTRACT_NAME file = os.path.join("tests", "assets", OnlineCommandTest.TWBX_FILE_WITH_EXTRACT) - self._publish_wb(file, name_on_server) + arguments = self._publish_args(file, name_on_server) + _test_command(arguments) command = "export" friendly_name = name_on_server + "/" + OnlineCommandTest.TWBX_WITH_EXTRACT_SHEET + "?param1=3" arguments = [command, friendly_name, "--pdf", "-f", "exported_view.pdf"] From c587f0b638b04dde454b2bd0c6294e39f868ebd9 Mon Sep 17 00:00:00 2001 From: Jac Date: Thu, 8 Dec 2022 12:59:00 -0800 Subject: [PATCH 37/37] Tfs 1482014 publish - error handling (#206) * catch non-server exceptions --- World Indicators.tdsx | Bin 0 -> 4041 bytes tabcmd/__main__.py | 5 +-- tabcmd/commands/auth/session.py | 5 +-- tabcmd/commands/constants.py | 35 +++++++++++++----- .../datasources_and_workbooks_command.py | 12 +++--- .../delete_command.py | 4 +- .../get_url_command.py | 26 ++++++------- .../publish_command.py | 8 ++-- .../extracts/create_extracts_command.py | 8 +++- .../extracts/decrypt_extracts_command.py | 4 +- .../extracts/delete_extracts_command.py | 4 +- .../extracts/encrypt_extracts_command.py | 2 +- .../extracts/reencrypt_extracts_command.py | 2 +- .../extracts/refresh_extracts_command.py | 2 +- tabcmd/commands/group/create_group_command.py | 5 +-- tabcmd/commands/group/delete_group_command.py | 3 +- .../project/create_project_command.py | 17 ++++----- .../project/delete_project_command.py | 8 ++-- tabcmd/commands/site/create_site_command.py | 16 +++----- tabcmd/commands/site/delete_site_command.py | 2 - tabcmd/commands/site/edit_site_command.py | 4 +- tabcmd/commands/site/list_command.py | 6 +-- tabcmd/commands/site/list_sites_command.py | 8 ++-- tabcmd/commands/user/user_data.py | 6 +-- tabcmd/execution/localize.py | 4 +- tabcmd/execution/tabcmd_controller.py | 2 +- tabcmd/locales/en/LC_MESSAGES/tabcmd.po | 16 +++++++- tabcmd/tabcmd.py | 3 +- 28 files changed, 114 insertions(+), 103 deletions(-) create mode 100644 World Indicators.tdsx diff --git a/World Indicators.tdsx b/World Indicators.tdsx new file mode 100644 index 0000000000000000000000000000000000000000..7c8e53db0597c5cabf5c1c3a8219b1b2fcc3c17c GIT binary patch literal 4041 zcmZ{nRag{&wuMPWx7XJ`RuNMYy!ks3-`hZ-8hp>w#N zr+e%T<*FIxlz`?|7f`MY|#IE4lThp_s) zy%q{}4LP#)3t5q(#2`yBkCCWkgf6ePHscE67QhE~G{NP?9UeJ55+Nt?WW@IhbTR}u zURcGA$LhB*YRV6Z+DldE?jLeSA38%}@#pZOz6F2H(g-7&IgP~6jqj(@ZG}q>&IQVz z-gaKV*DqIN9?H(i;8TraldoHI7SWD!%$eWeMTzTyGBbsD9`vkE9pTy^^aQ-$9CT&%IvND;KF$A zE@p7zN~1_hRuI4A0lp%;zTlFEGsIpF;+VD~0T69*j~VaaPPhkZ_-obw<<@NJ5~?Xx z3ySog%w`)Gu|Z7=HfQVB7Yl#};drZ0W0OiA?Mh5eRQDFg*x4-ZotsLVP{CzLf1yi? zD4Y8XKt7@!XzHoA{M)u(4Md1iUSd(~mgmFmd9HPnX*$m131ih%;%Z8}&(y{^>E6Jf zlbC3x&xwBVIkHTFxPzqYo|{h>J<_$u)uq196uMs_0F&obD}akfMu-1uMqHzH>EPeK z4-R*3+i!`K?wcxdma2H{x*)QyDZc6H{#g4iHxdLaZZ7^7|0VskVJsZ|ig0I$sgp>r zj-8uRZDz2Zf15Ro>WF8OC;iP^9J8S zFKW77>BmGPZn(1ic;+Oz^$1ww0!4Fr|B_<84XQj5;+#rt{pB6whrr2QrruB=-+>e- zj52&?i@wo`q|0N~eqs|wnhh4v-ISfcmrQ!Es z(krfxvKOMe)p|0GlR+Gs?YwKK4{)o?E|`DuyzO{N>lW+EXG>fWnJC0WDrj-5RDc8iD(j9-$Kn2Si7G8bCkc65$MGYAof0)Yk6)yF~#(Q+_320F{?Sr`tI zqb--AJnCw%LE7VantB#%RojEH1387QjXQqN<&b0qJ0+Od0`#1?J!y*$)?^pPPXmbs z;k75MQi2-qay@1 z19>ItgqIO{9+W|LKMqsJ8vWZ678)@qh;@)$k1)W+`8Gx*x)A@KwJT%m6HC(GyH%&K zhe86(HLAO9(>1966;j00Y}96;c6lh+vnL{Oijkx3FG=Yrdu3-8L?tv9dJl#f3AeVV z&8FzL5iF%33&EFLi7LmQaRJ8+kt@@;DXcSei1s7KmhfgSy!MNBMtESvpLWreZy}J! zaa+KUoK!4SPx_07UfmXlwT#MzXV*>f209IBZo4G)o0UFa7mcz0oBjTTusf1^F>gcP zlLJRJN2>VWT_!d-#&K9UPtL#i8r7$MvGuPD!ZV%3H!bsAz7@C+k9r)%rlSY5o~P`A1KG z4nKwYvuj{p1eoGWfat=!@BD50g}gP&%BUVK(cGlvvZNWuVCj44`GQ&F65;0;O(e<8 za@JNdMrjk*OMXC`+gOT{QnYevo8zCUH%zp;bMUZ?iYgv+Ov}G{h>1NHfGHP7&En(c z?n|vbPrrnK19g~L-)aDt*B`RTZND(CovXF-b7hn~&;8VrM|=Be4N%~K9#GB)O|$cn zSOo)Vv30}jH`xezxr60Jx`33=w zWVX+33AGRiRTHM*~(;jFyk50?reCTg_3*WFB_Kv1?e*DT)Cj-p5 zuj$YL?;B~=XEc=}CZ^Y0_+V|No#_CD<7jV#Tzff{k=5(3I~oDug>8c<9{m>9lp@t% zXr+6RhH+{}w&hWJ5nhjHwED_KC}Mvt1=V)mkOKT)1ZJb1WCy2YCG{wHHySyH-W>`&6{95g(-X3n6ITV!gT6?*nfBFhQ2ng4Vd#2`H%#nl z1?#`ZH=@WmFMCNzPQ5^}E|u0X`kTVbUElIIeb#-UJ6=5$B%f}(tgqd>U5a9EFjRLM z&!6e7jSUso2ZA} zNO0oJ7L(rj%=qwjEO>Nq1`(3wh^uDbSD-RvcvLv*%uAD(%}{Vjx6~NYH~^S)TAw|l zG(P7-9`I{i3mP?YK4w8U++DoSrlmJ!4iX%1_znVM2rQhPVk&l7tH~o zlyT8WKS&%(c?NQ7n`$C@ZSu+>=?AIzNMS5$??ufM{UkLRNhQHKQ?h0(4W}d?Yx(QJ z?9RCump6;GD{C}B-1piw8R8aD_v-rW_jSAGY|wY@?^DC@N0v8`8_F#+dD9|%SLV3` zfVvg-Hb>}Vxnguuxo$s_J6p?hV5F~U9$Hq1e>UmmyUj>JA4J`}|D=GltY)^~-D=?A zMC{}F1_6^oXz*S~QBh#8c4!4br+MCq>Qk946}Vu9jKe~`c&Kn_-bZg&gc+b1U`Ygs zjg&TeIY+@w$?WXK@h!jL(SqsLCX=jXiMX$81acB4$piSPpKm8y6{ZLMM?ah zbW;>+y(>H~fgPSWVfS>#)e$mRvsHLQrrT8$FasRIj3QPm_;{Bz%J2n=N0%1)8Y{*v zrqo$17_-VL2bOuWxSyaD23*=XS4~R@~;gjW&^ZcxxRIVt2 zfI^41Xs^WzkV90%_KGI#@j(jKCF$e7PtbnbYMV^*rNe}rPC_gpHt$2 zD!5qg8KCNY6Zu%i@r0c1aGyUmFYMchB-z0#rw%oufV@nc>Xln(V}8YR2bm>VIfx8E5B5HD04kl zuzXgPbryc?R`v_5+~i45-epc znArLZXzp_U0niD!J^ld(eT-@iXUnDuH^dKX{jq%CK|sE55P{mM=BT|*-F zd61|QsrR7l(kiIx=cogn>}ngWq`0MjT3Rhic&9re$Ajb=(YD3@8GEdJ-9GOrs2j2t zoqqeI0Qd9ebPL-gEebZRnmM&GL1KGKmF8wNVm8gmZomUc;ID$+!m1b-tV5rjD0c7l zS$XS8)r&QDuC@~rvEOGBR7ZkB)gE=eZ6NuT1{A+ZNXGRii9kCZ@h|eL2^RGE!s#7p zo++BM8C_xY?OCp_lJvf(Xi0ORe zep_5t>2M(>H^z4^`D!A6UR1sz|7VNq>0Q&JS7K+VKRv@-WkEWP9{Dco3(WrvTRIc$s^Z!&nCZ9kCId%7)?00; z%{JikkklYf;vpFaBDyr@^hvjGF7ox$F*}hYzCU|E1K)EuLR8yW2g2gVANs=!JDMxW zn~ydxUM=(WI!@BBHySir5n;5itaUYT8L9QawV2&GMq0Ib$gr@~tg)~Rb+B=$vHlmg g|NHhI-2T7%|KQtD2mjH3n%MtF{h#~&`;LY6Kl^{v761SM literal 0 HcmV?d00001 diff --git a/tabcmd/__main__.py b/tabcmd/__main__.py index 0c800dea..97a969ee 100644 --- a/tabcmd/__main__.py +++ b/tabcmd/__main__.py @@ -3,9 +3,8 @@ try: from tabcmd.tabcmd import main except ImportError: - 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") + print(sys.stderr, "Tabcmd needs to be run as a module, it cannot be run as a script") + print(sys.stderr, "Try running python -m tabcmd") sys.exit(1) if __name__ == "__main__": diff --git a/tabcmd/commands/auth/session.py b/tabcmd/commands/auth/session.py index cefc8558..d0f970a4 100644 --- a/tabcmd/commands/auth/session.py +++ b/tabcmd/commands/auth/session.py @@ -195,10 +195,9 @@ def _sign_in(self, tableau_auth): if not self.username: self.username = self.tableau_server.users.get_by_id(self.user_id).name self.logger.info(_("common.output.succeeded")) - except TSC.ServerResponseError as e: - Errors.exit_with_error(self.logger, _("publish.errors.unexpected_server_response"), e) + except Exception as e: + Errors.exit_with_error(self.logger, e) self.logger.debug("Signed into {0}{1} as {2}".format(self.server_url, self.site_name, self.username)) - return self.tableau_server def _get_saved_credentials(self): diff --git a/tabcmd/commands/constants.py b/tabcmd/commands/constants.py index ba4e6288..e78daf36 100644 --- a/tabcmd/commands/constants.py +++ b/tabcmd/commands/constants.py @@ -1,6 +1,8 @@ import inspect import sys +from tableauserverclient import ServerResponseError + from tabcmd.execution.localize import _ @@ -30,6 +32,10 @@ def is_login_error(error): if hasattr(error, "code"): return error.code == Constants.login_error + @staticmethod + def is_server_response_error(error): + return isinstance(error, ServerResponseError) + # https://gist.github.com/FredLoney/5454553 @staticmethod def log_stack(logger): @@ -58,20 +64,29 @@ def exit_with_error(logger, message=None, exception=None): if message and not exception: logger.error(message) if exception: - if Errors.is_expired_session(exception): - logger.error(_("session.errors.session_expired")) - # TODO: add session as an argument to this method - # and add the full command line as a field in Session? - # "session.session_expired_login")) - # session.renew_session() if message: logger.debug("Error message: " + message) Errors.check_common_error_codes_and_explain(logger, exception) except Exception as exc: - print("Error during log call from exception - {} {}".format(exc.__class__, message)) - print("Exiting...") + print(sys.stderr, "Error during log call from exception - {}".format(exc)) + try: + logger.info("Exiting...") + except Exception: + print(sys.stderr, "Exiting...") sys.exit(1) @staticmethod - def check_common_error_codes_and_explain(logger, error): - logger.error(error) + def check_common_error_codes_and_explain(logger, exception): + if Errors.is_server_response_error(exception): + logger.error(_("publish.errors.unexpected_server_response").format(exception)) + if Errors.is_expired_session(exception): + logger.error(_("session.errors.session_expired")) + # TODO: add session as an argument to this method + # and add the full command line as a field in Session? + # "session.session_expired_login")) + # session.renew_session + return + if exception.code.startswith(Constants.source_not_found): + logger.error(_("publish.errors.server_resource_not_found"), exception) + else: + logger.error(exception) diff --git a/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py b/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py index e59851ab..1d345a12 100644 --- a/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py +++ b/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py @@ -25,8 +25,8 @@ def get_view_by_content_url(logger, server, view_content_url) -> TSC.ViewItem: req_option.filter.add(TSC.Filter("contentUrl", TSC.RequestOptions.Operator.Equals, view_content_url)) logger.trace(req_option.get_query_params()) matching_views, paging = server.views.get(req_option) - except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response").format(e)) + except Exception as e: + Errors.exit_with_error(logger, e) if len(matching_views) < 1: Errors.exit_with_error(logger, message=_("errors.xmlapi.not_found")) return matching_views[0] @@ -39,8 +39,8 @@ def get_wb_by_content_url(logger, server, workbook_content_url) -> TSC.WorkbookI req_option.filter.add(TSC.Filter("contentUrl", TSC.RequestOptions.Operator.Equals, workbook_content_url)) logger.trace(req_option.get_query_params()) matching_workbooks, paging = server.workbooks.get(req_option) - except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response").format("")) + except Exception as e: + Errors.exit_with_error(logger, e) if len(matching_workbooks) < 1: Errors.exit_with_error(logger, message=_("dataalerts.failure.error.workbookNotFound")) return matching_workbooks[0] @@ -53,8 +53,8 @@ def get_ds_by_content_url(logger, server, datasource_content_url) -> TSC.Datasou req_option.filter.add(TSC.Filter("contentUrl", TSC.RequestOptions.Operator.Equals, datasource_content_url)) logger.trace(req_option.get_query_params()) matching_datasources, paging = server.datasources.get(req_option) - except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response").format("")) + except Exception as e: + Errors.exit_with_error(logger, e) if len(matching_datasources) < 1: Errors.exit_with_error(logger, message=_("dataalerts.failure.error.datasourceNotFound")) return matching_datasources[0] diff --git a/tabcmd/commands/datasources_and_workbooks/delete_command.py b/tabcmd/commands/datasources_and_workbooks/delete_command.py index 3815b86b..e28f5f45 100644 --- a/tabcmd/commands/datasources_and_workbooks/delete_command.py +++ b/tabcmd/commands/datasources_and_workbooks/delete_command.py @@ -73,5 +73,5 @@ def run_command(args): else: server.datasources.delete(item_to_delete.id) logger.info(_("common.output.succeeded")) - except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, "Error deleting from server", e) + except Exception as e: + Errors.exit_with_error(logger, e) diff --git a/tabcmd/commands/datasources_and_workbooks/get_url_command.py b/tabcmd/commands/datasources_and_workbooks/get_url_command.py index e4ec7faf..26458746 100644 --- a/tabcmd/commands/datasources_and_workbooks/get_url_command.py +++ b/tabcmd/commands/datasources_and_workbooks/get_url_command.py @@ -60,10 +60,8 @@ def run_command(args): ) try: GetUrl.get_content_as_file(file_type, content_type, logger, args, server, url) - except ServerResponseError as e: + except Exception as e: Errors.exit_with_error(logger, e) - except Exception as be: - Errors.exit_with_error(logger, be, "Error during file download") ## this first set of methods is all parsing the url and file input from the user @@ -131,7 +129,7 @@ def get_resource_name(url: str, logger): # workbooks/wb-name" -> "wb-name", dat url = url.lstrip("/") # strip opening / if present name_parts = url.split("/") if len(name_parts) != 2: - GetUrl.explain_expected_url(logger, url, "GetURl") + GetUrl.explain_expected_url(logger, url, "GetUrl") resource_name_with_params = name_parts[::-1][0] # last part resource_name_with_ext = GetUrl.strip_query_params(resource_name_with_params) resource_name = GetUrl.get_name_without_possible_extension(resource_name_with_ext) @@ -141,7 +139,7 @@ def get_resource_name(url: str, logger): # workbooks/wb-name" -> "wb-name", dat def get_view_url(url, logger): # "views/wb-name/view-name" -> wb-name/sheets/view-name name_parts = url.split("/") # ['views', 'wb-name', 'view-name'] if len(name_parts) != 3: - GetUrl.explain_expected_url(logger, url, "GetURl") + GetUrl.explain_expected_url(logger, url, "GetUrl") workbook_name = name_parts[1] view_name = name_parts[::-1][0] view_name = GetUrl.strip_query_params(view_name) @@ -185,8 +183,8 @@ def generate_pdf(logger, server, args, view_url): server.views.populate_pdf(view_item, req_option_pdf) filename = GetUrl.filename_from_args(args.filename, view_item.name, "pdf") DatasourcesAndWorkbooks.save_to_file(logger, view_item.pdf, filename) - except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) + except Exception as e: + Errors.exit_with_error(logger, e) @staticmethod def generate_png(logger, server, args, view_url): @@ -199,8 +197,8 @@ def generate_png(logger, server, args, view_url): server.views.populate_image(view_item, req_option_csv) filename = GetUrl.filename_from_args(args.filename, view_item.name, "png") DatasourcesAndWorkbooks.save_to_file(logger, view_item.image, filename) - except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) + except Exception as e: + Errors.exit_with_error(logger, e) @staticmethod def generate_csv(logger, server, args, view_url): @@ -213,8 +211,6 @@ def generate_csv(logger, server, args, view_url): server.views.populate_csv(view_item, req_option_csv) file_name_with_path = GetUrl.filename_from_args(args.filename, view_item.name, "csv") DatasourcesAndWorkbooks.save_to_data_file(logger, view_item.csv, file_name_with_path) - except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) except Exception as e: Errors.exit_with_error(logger, exception=e) @@ -229,8 +225,8 @@ def generate_twb(logger, server, args, file_extension, url): logger.debug("Saving as {}".format(file_name_with_path)) server.workbooks.download(target_workbook.id, filepath=None, no_extract=False) logger.info(_("export.success").format(target_workbook.name, file_name_with_path)) - except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) + except Exception as e: + Errors.exit_with_error(logger, e) @staticmethod def generate_tds(logger, server, args, file_extension): @@ -243,5 +239,5 @@ def generate_tds(logger, server, args, file_extension): logger.debug("Saving as {}".format(file_name_with_path)) server.datasources.download(target_datasource.id, filepath=None, no_extract=False) logger.info(_("export.success").format(target_datasource.name, file_name_with_path)) - except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) + except Exception as e: + Errors.exit_with_error(logger, e) diff --git a/tabcmd/commands/datasources_and_workbooks/publish_command.py b/tabcmd/commands/datasources_and_workbooks/publish_command.py index 93a6fe0b..c4866bbd 100644 --- a/tabcmd/commands/datasources_and_workbooks/publish_command.py +++ b/tabcmd/commands/datasources_and_workbooks/publish_command.py @@ -1,4 +1,5 @@ import tableauserverclient as TSC +from tableauserverclient import ServerResponseError from tabcmd.commands.auth.session import Session from tabcmd.commands.constants import Errors @@ -75,9 +76,8 @@ def run_command(args): as_job=False, skip_connection_check=False, ) - - except IOError as ioe: - Errors.exit_with_error(logger, ioe) + except Exception as e: + Errors.exit_with_error(logger, e) logger.info(_("publish.success") + "\n{}".format(new_workbook.webpage_url)) elif source in ["tds", "tdsx", "hyper"]: @@ -87,7 +87,7 @@ def run_command(args): new_datasource = server.datasources.publish( new_datasource, args.filename, publish_mode, connection_credentials=creds ) - except IOError as ioe: + except Exception as exc: Errors.exit_with_error(logger, exc) logger.info(_("publish.success") + "\n{}".format(new_datasource.webpage_url)) diff --git a/tabcmd/commands/extracts/create_extracts_command.py b/tabcmd/commands/extracts/create_extracts_command.py index 8f09ba90..f23be293 100644 --- a/tabcmd/commands/extracts/create_extracts_command.py +++ b/tabcmd/commands/extracts/create_extracts_command.py @@ -54,8 +54,12 @@ def run_command(args): includeAll=args.include_all, datasources=args.embedded_datasources, ) - except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, exception=e) + except Exception as e: + + if args.continue_if_exists and Errors.is_resource_conflict(e): + logger.info(_("tabcmd.result.already_exists").format(_("content_type.extract"), args.name)) + return + Errors.exit_with_error(logger, e) logger.info(_("common.output.job_queued_success")) logger.debug("Extract creation queued with JobID: {}".format(job.id)) diff --git a/tabcmd/commands/extracts/decrypt_extracts_command.py b/tabcmd/commands/extracts/decrypt_extracts_command.py index 0fd0b1e3..a43d8fa6 100644 --- a/tabcmd/commands/extracts/decrypt_extracts_command.py +++ b/tabcmd/commands/extracts/decrypt_extracts_command.py @@ -29,8 +29,8 @@ def run_command(args): try: logger.info(_("decryptextracts.status").format(args.site_name)) job = server.sites.decrypt_extracts(site_item.id) - except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, "Error decrypting extracts", e) + except Exception as e: + Errors.exit_with_error(logger, e) logger.info(_("common.output.job_queued_success")) logger.debug("Extract decryption queued with JobID: {}".format(job.id)) diff --git a/tabcmd/commands/extracts/delete_extracts_command.py b/tabcmd/commands/extracts/delete_extracts_command.py index e4e3dcc6..62ba3bb0 100644 --- a/tabcmd/commands/extracts/delete_extracts_command.py +++ b/tabcmd/commands/extracts/delete_extracts_command.py @@ -41,8 +41,8 @@ def run_command(args): logger.info(_("deleteextracts.for.workbook_name").format(args.workbook)) workbook_item = Server.get_workbook_item(logger, server, args.workbook) job = server.workbooks.delete_extract(workbook_item) - except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, _("deleteextracts.errors.error"), e) + except Exception as e: + Errors.exit_with_error(logger, e) logger.info(_("common.output.job_queued_success")) logger.debug("Extract deletion queued with JobID: {}".format(job.id)) diff --git a/tabcmd/commands/extracts/encrypt_extracts_command.py b/tabcmd/commands/extracts/encrypt_extracts_command.py index a3507624..1b697fd7 100644 --- a/tabcmd/commands/extracts/encrypt_extracts_command.py +++ b/tabcmd/commands/extracts/encrypt_extracts_command.py @@ -31,7 +31,7 @@ def run_command(args): try: logger.info(_("encryptextracts.status").format(site_item.name)) job = server.sites.encrypt_extracts(site_item.id) - except TSC.ServerResponseError as e: + except Exception as e: Errors.exit_with_error(logger, e) logger.info(_("common.output.job_queued_success")) diff --git a/tabcmd/commands/extracts/reencrypt_extracts_command.py b/tabcmd/commands/extracts/reencrypt_extracts_command.py index 37c1c121..4f7b4ef5 100644 --- a/tabcmd/commands/extracts/reencrypt_extracts_command.py +++ b/tabcmd/commands/extracts/reencrypt_extracts_command.py @@ -31,7 +31,7 @@ def run_command(args): try: logger.info(_("reencryptextracts.status").format(site_item.name)) job = server.sites.encrypt_extracts(site_item.id) - except TSC.ServerResponseError as e: + except Exception as e: Errors.exit_with_error(logger, e) logger.info(_("common.output.job_queued_success")) diff --git a/tabcmd/commands/extracts/refresh_extracts_command.py b/tabcmd/commands/extracts/refresh_extracts_command.py index 290e6cf3..e18020a3 100644 --- a/tabcmd/commands/extracts/refresh_extracts_command.py +++ b/tabcmd/commands/extracts/refresh_extracts_command.py @@ -79,7 +79,7 @@ def run_command(args): elif args.url: logger.error("URL not yet implemented") - except TSC.ServerResponseError as e: + except Exception as e: Errors.exit_with_error(logger, _("refreshextracts.errors.error"), e) logger.info(_("common.output.job_queued_success")) diff --git a/tabcmd/commands/group/create_group_command.py b/tabcmd/commands/group/create_group_command.py index 8eea4198..8aeddb84 100644 --- a/tabcmd/commands/group/create_group_command.py +++ b/tabcmd/commands/group/create_group_command.py @@ -31,9 +31,8 @@ def run_command(args): new_group = TSC.GroupItem(args.name) server.groups.create(new_group) logger.info(_("common.output.succeeded")) - except TSC.ServerResponseError as e: - # quite likely a 403 because you must be server/site admin to call this + except Exception as e: if args.continue_if_exists and Errors.is_resource_conflict(e): - logger.info(_("tabcmd.result.already_exists.group").format(args.name)) + logger.info(_("tabcmd.result.already_exists").format(_("content_type.group"), args.name)) return Errors.exit_with_error(logger, _("tabcmd.result.failed.create_group")) diff --git a/tabcmd/commands/group/delete_group_command.py b/tabcmd/commands/group/delete_group_command.py index d2c8ccf0..accf62e4 100644 --- a/tabcmd/commands/group/delete_group_command.py +++ b/tabcmd/commands/group/delete_group_command.py @@ -32,6 +32,5 @@ def run_command(args): logger.info(_("deletegroup.status").format(group_id)) server.groups.delete(group_id) logger.info(_("common.output.succeeded")) - except TSC.ServerResponseError as e: - # quite likely a 403 because you must be server/site admin to call this + except Exception as e: Errors.exit_with_error(logger, _("tabcmd.result.failed.delete.group"), e) diff --git a/tabcmd/commands/project/create_project_command.py b/tabcmd/commands/project/create_project_command.py index be5d2507..f1bec5b0 100644 --- a/tabcmd/commands/project/create_project_command.py +++ b/tabcmd/commands/project/create_project_command.py @@ -37,8 +37,8 @@ def run_command(args): try: logger.info(_("tabcmd.find.parent_project").format(args.parent_project_path)) parent = Server.get_project_by_name_and_parent_path(logger, server, None, args.parent_project_path) - except TSC.ServerResponseError as exc: - Errors.exit_with_error(logger, _("publish.errors.server_resource_not_found"), exc) + except Exception as exc: + Errors.exit_with_error(logger, exc) readable_name = "{0}/{1}".format(args.parent_project_path, args.project_name) parent_id = parent.id logger.debug("parent project = `{0}`, id = {1}".format(args.parent_project_path, parent_id)) @@ -48,11 +48,8 @@ def run_command(args): project_item = server.projects.create(new_project) logger.info(_("common.output.succeeded")) return project_item - except TSC.ServerResponseError as e: - if Errors.is_resource_conflict(e): - if args.continue_if_exists: - logger.info(_("tabcmd.result.already_exists").format(args.project_name)) - return - else: - Errors.exit_with_error(logger, _("tabcmd.result.already_exists").format(args.project_name)) - Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) + except Exception as e: + if Errors.is_resource_conflict(e) and args.continue_if_exists: + logger.info(_("tabcmd.result.already_exists").format(_("content_type.project"), args.project_name)) + return + Errors.exit_with_error(logger, e) diff --git a/tabcmd/commands/project/delete_project_command.py b/tabcmd/commands/project/delete_project_command.py index f1397f99..74b72395 100644 --- a/tabcmd/commands/project/delete_project_command.py +++ b/tabcmd/commands/project/delete_project_command.py @@ -32,17 +32,17 @@ def run_command(args): logger.debug("parent path: {}".format(args.parent_project_path)) try: - logger.debug(_("deleteproject.status").format(args.parent_project_path + "/" + args.project_name)) + logger.debug(_("deleteproject.status").format((args.parent_project_path or "") + "/" + args.project_name)) project = Server.get_project_by_name_and_parent_path( logger, server, args.project_name, args.parent_project_path ) - except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) + except Exception as e: + Errors.exit_with_error(logger, e) project_id = project.id try: logger.info(_("deleteproject.status").format(args.project_name)) server.projects.delete(project_id) logger.info(_("common.output.succeeded")) - except TSC.ServerResponseError as e: + except Exception as e: Errors.exit_with_error(logger, "tabcmd.result.failure.delete.project", e) diff --git a/tabcmd/commands/site/create_site_command.py b/tabcmd/commands/site/create_site_command.py index b54d57d0..84208188 100644 --- a/tabcmd/commands/site/create_site_command.py +++ b/tabcmd/commands/site/create_site_command.py @@ -42,14 +42,8 @@ def run_command(args): logger.info(_("createsite.status").format(args.new_site_name)) server.sites.create(new_site) logger.info(_("common.output.succeeded")) - except TSC.ServerResponseError as e: - if Errors.is_resource_conflict(e): - if args.continue_if_exists: - logger.info(_("createsite.errors.site_name_already_exists").format(args.new_site_name)) - return - else: - Errors.exit_with_error( - logger, _("createsite.errors.site_name_already_exists").format(args.site_name) - ) - - Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) + except Exception as e: + if Errors.is_resource_conflict(e) and args.continue_if_exists: + logger.info(_("createsite.errors.site_name_already_exists").format(args.new_site_name)) + return + Errors.exit_with_error(logger, e) diff --git a/tabcmd/commands/site/delete_site_command.py b/tabcmd/commands/site/delete_site_command.py index 854c4dd9..56256c8e 100644 --- a/tabcmd/commands/site/delete_site_command.py +++ b/tabcmd/commands/site/delete_site_command.py @@ -31,8 +31,6 @@ def run_command(args): logger.debug(strings[3].format(target_site_id, server.site_id)) try: server.sites.delete(target_site_id) - except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, strings[1], e) except Exception as e: Errors.exit_with_error(logger, strings[4], e) logger.info(strings[0].format(args.site_name_to_delete)) diff --git a/tabcmd/commands/site/edit_site_command.py b/tabcmd/commands/site/edit_site_command.py index e47f246f..2d7e3db4 100644 --- a/tabcmd/commands/site/edit_site_command.py +++ b/tabcmd/commands/site/edit_site_command.py @@ -46,5 +46,5 @@ def run_command(args): server.sites.update(site_item) logger.info(_("common.output.succeeded")) - except TSC.ServerResponseError as e: - Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response"), e) + except Exception as e: + Errors.exit_with_error(logger, e) diff --git a/tabcmd/commands/site/list_command.py b/tabcmd/commands/site/list_command.py index f2af2a72..a77eab75 100644 --- a/tabcmd/commands/site/list_command.py +++ b/tabcmd/commands/site/list_command.py @@ -39,8 +39,8 @@ def run_command(args): logger.info("===== Listing {0} content for user {1}...".format(content_type, session.username)) for item in items: - print("NAME:".rjust(10), item.name) - print("ID:".rjust(10), item.id) + logger.info("NAME:".rjust(10), item.name) + logger.info("ID:".rjust(10), item.id) - except TSC.ServerResponseError as e: + except Exception as e: Errors.exit_with_error(logger, e) diff --git a/tabcmd/commands/site/list_sites_command.py b/tabcmd/commands/site/list_sites_command.py index be532584..66ad24c7 100644 --- a/tabcmd/commands/site/list_sites_command.py +++ b/tabcmd/commands/site/list_sites_command.py @@ -31,9 +31,9 @@ def run_command(args): sites, pagination = server.sites.get() logger.info(_("listsites.status").format(session.username)) for site in sites: - print("NAME:".rjust(10), site.name) - print("SITEID:".rjust(10), site.content_url) + logger.info("NAME:".rjust(10), site.name) + logger.info("SITEID:".rjust(10), site.content_url) if args.get_extract_encryption_mode: - print("EXTRACTENCRYPTION:", site.extract_encryption_mode) - except TSC.ServerResponseError as e: + logger.info("EXTRACTENCRYPTION:", site.extract_encryption_mode) + except Exception as e: Errors.exit_with_error(logger, e) diff --git a/tabcmd/commands/user/user_data.py b/tabcmd/commands/user/user_data.py index f8c088f7..a95448d7 100644 --- a/tabcmd/commands/user/user_data.py +++ b/tabcmd/commands/user/user_data.py @@ -218,10 +218,8 @@ def act_on_users( group = None try: group = UserCommand.find_group(logger, server, args.name) - except TSC.ServerResponseError as e: - Errors.exit_with_error( - logger, _("errors.reportable.impersonation.group_not_found").format(args.name), exception=e - ) + except Exception as e: + Errors.exit_with_error(logger, e) n_users_handled: int = 0 number_of_errors: int = 0 diff --git a/tabcmd/execution/localize.py b/tabcmd/execution/localize.py index 00c770f0..f38995cc 100644 --- a/tabcmd/execution/localize.py +++ b/tabcmd/execution/localize.py @@ -33,7 +33,7 @@ def set_client_locale(lang: str = "", logger=None) -> Optional[Callable]: try: locale_options = [_validate_lang(lang), _get_default_locale(), "en"] except Exception as e: - print(e) + print(sys.stderr, e) locale_options = ["en"] logger.debug("Language options: {}".format(locale_options)) @@ -45,7 +45,7 @@ def set_client_locale(lang: str = "", logger=None) -> Optional[Callable]: translate = _load_language(lang, domain, logger) break except Exception as e: - print("Failed to load language '", lang, "':", e) + print(sys.stderr, "Failed to load language '", lang, "':", e) return translate or _identity_func diff --git a/tabcmd/execution/tabcmd_controller.py b/tabcmd/execution/tabcmd_controller.py index 001b2b5b..1f7f0fdc 100644 --- a/tabcmd/execution/tabcmd_controller.py +++ b/tabcmd/execution/tabcmd_controller.py @@ -23,7 +23,7 @@ def run(parser, user_input=None): sys.exit(0) user_input = user_input or sys.argv[1:] namespace = parser.parse_args(user_input) - if namespace.logging_level: + if namespace.logging_level and namespace.logging_level != logging.INFO: print("logging:", namespace.logging_level) logger = log(__name__, namespace.logging_level or logging.INFO) diff --git a/tabcmd/locales/en/LC_MESSAGES/tabcmd.po b/tabcmd/locales/en/LC_MESSAGES/tabcmd.po index 613e6904..1fae5004 100644 --- a/tabcmd/locales/en/LC_MESSAGES/tabcmd.po +++ b/tabcmd/locales/en/LC_MESSAGES/tabcmd.po @@ -13,8 +13,8 @@ msgstr "" # https://sourcegraph.prod.tableautools.com/teams/near/-/blob/workgroup/src/shared/libraries/tab-strings/res/localization/shared_wg_en.properties # how much of this can be replaced by server responses? -msgid "tabcmd.result.already_exists.group" -msgstr "Group called ''{}'' already exists" +msgid "tabcmd.result.already_exists" +msgstr "{} called ''{}'' already exists" msgid "tabcmd.result.failed.delete.group" msgstr "Error deleting group from server" @@ -1702,6 +1702,14 @@ msgstr "Shared version" msgid "flows.drafts.default_name" msgstr "New Flow" +#: +msgid "content_type.group" +msgstr "Group" + +#: +msgid "content_type.user" +msgstr "User" + #: msgid "content_type.project" msgstr "Project" @@ -1734,6 +1742,10 @@ msgstr "Collection" msgid "content_type.visualization" msgstr "Visualization" +#: +msgid "content_type.extract" +msgstr "Extract" + #: msgid "content_type.askdata_visualization" msgstr "Ask Data Visualization" diff --git a/tabcmd/tabcmd.py b/tabcmd/tabcmd.py index 75951897..eeaec6ed 100644 --- a/tabcmd/tabcmd.py +++ b/tabcmd/tabcmd.py @@ -14,8 +14,9 @@ def main(): parser = TabcmdController.initialize() TabcmdController.run(parser) except Exception as e: - print("Unhandled exception: {}".format(type(e).__name__)) + print(sys.stderr, "Unhandled exception: {}".format(type(e).__name__)) print( + sys.stderr, f"{type(e).__name__} at line {e.__traceback__.tb_lineno} of {__file__}: {e}", ) sys.exit(1)