diff --git a/.github/workflows/generate-metadata.yml b/.github/workflows/generate-metadata.yml index b46f4793..2e46c5b5 100644 --- a/.github/workflows/generate-metadata.yml +++ b/.github/workflows/generate-metadata.yml @@ -16,7 +16,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.7' + python-version: '3.9' - name: Install App and Extras run: | diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 5e1ffdc0..d9be64c3 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -9,8 +9,9 @@ name: Release-Executable # https://anshumanfauzdar.medium.com/using-github-actions-to-bundle-python-application-into-a-single-package-and-automatic-release-834bd42e0670 on: - release: - types: [published] + push: + tags: + - '*' workflow_dispatch: jobs: @@ -58,7 +59,7 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: 3.9 - name: Install dependencies and build run: | @@ -94,10 +95,20 @@ jobs: with: name: ${{ matrix.OUT_FILE_NAME }} path: ./dist/${{ matrix.TARGET }}/${{ matrix.OUT_FILE_NAME }} - + - name: Upload artifact for Mac if: matrix.TARGET == 'macos' uses: actions/upload-artifact@v4 with: name: ${{ matrix.BUNDLE_NAME }} path: ./dist/${{ matrix.TARGET }}/${{ matrix.BUNDLE_NAME }}.tar + + - name: Upload binaries to release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: ./dist/${{ matrix.TARGET }}/${{ matrix.OUT_FILE_NAME }}/ + tag: ${{ github.ref_name }} + overwrite: true + promote: true + diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index bbf766af..d08307ff 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -20,7 +20,7 @@ jobs: fetch-depth: 0 - uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: 3.9 - name: Build dist files run: | python --version diff --git a/.github/workflows/run-e2-tests.yml b/.github/workflows/run-e2-tests.yml index 0d7d7083..68a8014b 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.9', '3.10', '3'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3'] runs-on: ${{ matrix.os }} diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 06e4d87f..fd9a4804 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -15,7 +15,7 @@ jobs: fail-fast: true matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ['3.8', '3.9', '3.10', '3'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3'] runs-on: ${{ matrix.os }} diff --git a/pyproject.toml b/pyproject.toml index 00a6ed92..a8839e58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ dependencies = [ "types-mock", "types-requests", "types-setuptools", - "tableauserverclient==0.31", + "tableauserverclient==0.34", "urllib3", ] [project.optional-dependencies] 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 a10e4029..5a9cfe17 100644 --- a/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py +++ b/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py @@ -152,3 +152,14 @@ 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_custom_view_by_id(logger, server, custom_view_id) -> TSC.CustomViewItem: + logger.debug(_("export.status").format(custom_view_id)) + try: + matching_custom_view = server.custom_views.get_by_id(custom_view_id) + except Exception as e: + Errors.exit_with_error(logger, exception=e) + if matching_custom_view is None: + Errors.exit_with_error(logger, message=_("errors.xmlapi.not_found")) + return matching_custom_view diff --git a/tabcmd/commands/datasources_and_workbooks/datasources_workbooks_views_url_parser.py b/tabcmd/commands/datasources_and_workbooks/datasources_workbooks_views_url_parser.py new file mode 100644 index 00000000..e5d3e4db --- /dev/null +++ b/tabcmd/commands/datasources_and_workbooks/datasources_workbooks_views_url_parser.py @@ -0,0 +1,198 @@ +import os + +from uuid import UUID + +from tabcmd.commands.constants import Errors +from tabcmd.commands.datasources_and_workbooks.datasources_and_workbooks_command import DatasourcesAndWorkbooks +from tabcmd.commands.server import Server +from tabcmd.execution.localize import _ + + +class DatasourcesWorkbooksAndViewsUrlParser(Server): + """ + Base Class for parsing & fetching Datasources, Workbooks, Views & Custom Views information from get/export URLs + """ + + def __init__(self, args): + super().__init__(args) + + @staticmethod + def get_view_url_from_names(wb_name, view_name): + return "{}/sheets/{}".format(wb_name, view_name) + + @staticmethod + def parse_export_url_to_workbook_view_and_custom_view(logger, url): + # input should be workbook_name/view_name or /workbook_name/view_name + # or workbook_name/view_name/custom_view_id/custom_view_name + name_parts = DatasourcesWorkbooksAndViewsUrlParser.validate_and_extract_url_parts(logger, url) + if len(name_parts) == 2: + workbook = name_parts[0] + view = DatasourcesWorkbooksAndViewsUrlParser.get_view_url_from_names(workbook, name_parts[1]) + return view, workbook, None, None + elif len(name_parts) == 4: + workbook = name_parts[0] + view = DatasourcesWorkbooksAndViewsUrlParser.get_view_url_from_names(workbook, name_parts[1]) + custom_view_id = name_parts[2] + DatasourcesWorkbooksAndViewsUrlParser.verify_valid_custom_view_id(logger, custom_view_id) + custom_view_name = name_parts[3] + return view, workbook, custom_view_id, custom_view_name + else: + return None, None, None, None + + @staticmethod + def validate_and_extract_url_parts(logger, url): + logger.info(_("export.status").format(url)) + if " " in url: + Errors.exit_with_error(logger, _("export.errors.white_space_workbook_view")) + if "?" in url: + url = url.split("?")[0] + url = url.lstrip("/") # strip opening / if present + return url.split("/") + + @staticmethod + def get_export_item_and_server_content_type_from_export_url(view_content_url, logger, server, custom_view_id): + return DatasourcesWorkbooksAndViewsUrlParser.get_content_and_server_content_type_from_url( + logger, server, view_content_url, custom_view_id + ) + + ################### GetURL Methods ############################## + + @staticmethod + def explain_expected_get_url(logger, url: str, command: str): + view_example = "/views//[.ext]" + custom_view_example = "/views////[.ext]" + wb_example = "/workbooks/[.ext]" + ds_example = "/datasources/ "wb-name", datasource/ds-name -> ds-name + url = url.lstrip("/") # strip opening / if present + name_parts = url.split("/") + if len(name_parts) != 2: + DatasourcesWorkbooksAndViewsUrlParser.explain_expected_get_url(logger, url, "GetUrl") + resource_name_with_params = name_parts[::-1][0] # last part + resource_name_with_ext = DatasourcesWorkbooksAndViewsUrlParser.strip_query_params(resource_name_with_params) + resource_name = DatasourcesWorkbooksAndViewsUrlParser.get_name_without_possible_extension( + resource_name_with_ext + ) + return resource_name + + @staticmethod + def get_view_url_from_get_url(logger, url): # "views/wb-name/view-name" -> wb-name/sheets/view-name + name_parts = url.split("/") # ['views', 'wb-name', 'view-name'] + if len(name_parts) != 3: + DatasourcesWorkbooksAndViewsUrlParser.explain_expected_get_url(logger, url, "GetUrl") + workbook_name = name_parts[1] + view_name = name_parts[::-1][0] + view_name = DatasourcesWorkbooksAndViewsUrlParser.strip_query_params(view_name) + view_name = DatasourcesWorkbooksAndViewsUrlParser.get_name_without_possible_extension(view_name) + return DatasourcesWorkbooksAndViewsUrlParser.get_view_url_from_names(workbook_name, view_name) + + @staticmethod + def get_custom_view_parts_from_get_url(logger, url): + name_parts = url.split("/") # ['views', 'wb-name', 'view-name', 'custom-view-id', 'custom-view-name'] + if len(name_parts) != 5: + DatasourcesWorkbooksAndViewsUrlParser.explain_expected_get_url(logger, url, "GetUrl") + workbook_name = name_parts[1] + view_name = name_parts[2] + custom_view_id = name_parts[3] + DatasourcesWorkbooksAndViewsUrlParser.verify_valid_custom_view_id(logger, custom_view_id) + custom_view_name = name_parts[::-1][0] + custom_view_name = DatasourcesWorkbooksAndViewsUrlParser.strip_query_params(custom_view_name) + custom_view_name = DatasourcesWorkbooksAndViewsUrlParser.get_name_without_possible_extension(custom_view_name) + return ( + DatasourcesWorkbooksAndViewsUrlParser.get_view_url_from_names(workbook_name, view_name), + custom_view_id, + custom_view_name, + ) + + @staticmethod + def parse_get_view_url_to_view_and_custom_view_parts(logger, url): + # input should be views/workbook_name/view_name + # or views/workbook_name/view_name/custom_view_id/custom_view_name + name_parts = DatasourcesWorkbooksAndViewsUrlParser.validate_and_extract_url_parts(logger, url) + if len(name_parts) == 3: + return DatasourcesWorkbooksAndViewsUrlParser.get_view_url_from_get_url(logger, url), None, None + elif len(name_parts) == 5: + return DatasourcesWorkbooksAndViewsUrlParser.get_custom_view_parts_from_get_url(logger, url) + else: + DatasourcesWorkbooksAndViewsUrlParser.explain_expected_get_url(logger, url, "GetUrl") + + @staticmethod + def get_url_item_and_item_type_from_view_url(logger, url, server): + ( + view_url, + custom_view_id, + custom_view_name, + ) = DatasourcesWorkbooksAndViewsUrlParser.parse_get_view_url_to_view_and_custom_view_parts(logger, url) + + return DatasourcesWorkbooksAndViewsUrlParser.get_content_and_server_content_type_from_url( + logger, server, view_url, custom_view_id + ) + + @staticmethod + def get_content_and_server_content_type_from_url(logger, server, view_content_url, custom_view_id): + item = DatasourcesAndWorkbooks.get_view_by_content_url(logger, server, view_content_url) + server_content_type = server.views + + if custom_view_id: + custom_view_item = DatasourcesAndWorkbooks.get_custom_view_by_id(logger, server, custom_view_id) + if custom_view_item.view.id != item.id: + Errors.exit_with_error(logger, "Invalid custom view URL provided") + server_content_type = server.custom_views + item = custom_view_item + return item, server_content_type + + @staticmethod + def verify_valid_custom_view_id(logger, custom_view_id): + try: + UUID(custom_view_id) + except ValueError: + Errors.exit_with_error(logger, _("export.errors.requires_valid_custom_view_uuid")) diff --git a/tabcmd/commands/datasources_and_workbooks/export_command.py b/tabcmd/commands/datasources_and_workbooks/export_command.py index 23012b51..5781c137 100644 --- a/tabcmd/commands/datasources_and_workbooks/export_command.py +++ b/tabcmd/commands/datasources_and_workbooks/export_command.py @@ -1,10 +1,13 @@ import tableauserverclient as TSC +from uuid import UUID + 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 +from .datasources_workbooks_views_url_parser import DatasourcesWorkbooksAndViewsUrlParser pagesize = TSC.PDFRequestOptions.PageType # type alias for brevity @@ -76,7 +79,12 @@ def run_command(args): logger.debug(_("tabcmd.launching")) session = Session() server = session.create_session(args, logger) - view_content_url, wb_content_url = ExportCommand.parse_export_url_to_workbook_and_view(logger, args.url) + ( + view_content_url, + wb_content_url, + custom_view_id, + custom_view_name, + ) = DatasourcesWorkbooksAndViewsUrlParser.parse_export_url_to_workbook_view_and_custom_view(logger, args.url) logger.debug(["view_url:", view_content_url, "workbook:", wb_content_url]) if not view_content_url and not wb_content_url: view_example = "/workbook_name/view_name" @@ -92,19 +100,23 @@ def run_command(args): 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) + elif args.pdf or args.png or args.csv: # it's a view or custom_view + ( + export_item, + server_content_type, + ) = DatasourcesWorkbooksAndViewsUrlParser.get_export_item_and_server_content_type_from_export_url( + view_content_url, logger, server, custom_view_id + ) if args.pdf: - output = ExportCommand.download_view_pdf(server, view_item, args, logger) - default_filename = "{}.pdf".format(view_item.name) + output = ExportCommand.download_view_pdf(server_content_type, export_item, args, logger) + default_filename = "{}.pdf".format(export_item.name) elif args.csv: - output = ExportCommand.download_csv(server, view_item, args, logger) - default_filename = "{}.csv".format(view_item.name) + output = ExportCommand.download_csv(server_content_type, export_item, args, logger) + default_filename = "{}.csv".format(export_item.name) elif args.png: - output = ExportCommand.download_png(server, view_item, args, logger) - - default_filename = "{}.png".format(view_item.name) + output = ExportCommand.download_png(server_content_type, export_item, args, logger) + default_filename = "{}.png".format(export_item.name) except TSC.ServerResponseError as e: Errors.exit_with_error(logger, _("publish.errors.unexpected_server_response").format(""), e) @@ -142,51 +154,33 @@ def download_wb_pdf(server, workbook_item, args, logger): return workbook_item.pdf @staticmethod - def download_view_pdf(server, view_item, args, logger): + def download_view_pdf(server_content_type, export_item, args, logger): logger.debug(args.url) pdf_options = TSC.PDFRequestOptions(maxage=1) ExportCommand.apply_values_from_url_params(logger, pdf_options, args.url) ExportCommand.apply_filters_from_args(pdf_options, args, logger) ExportCommand.apply_pdf_options(logger, pdf_options, args) logger.debug(pdf_options.get_query_params()) - server.views.populate_pdf(view_item, pdf_options) - return view_item.pdf + server_content_type.populate_pdf(export_item, pdf_options) + return export_item.pdf @staticmethod - def download_csv(server, view_item, args, logger): + def download_csv(server_content_type, export_item, args, logger): logger.debug(args.url) csv_options = TSC.CSVRequestOptions(maxage=1) ExportCommand.apply_values_from_url_params(logger, csv_options, args.url) ExportCommand.apply_filters_from_args(csv_options, args, logger) logger.debug(csv_options.get_query_params()) - server.views.populate_csv(view_item, csv_options) - return view_item.csv + server_content_type.populate_csv(export_item, csv_options) + return export_item.csv @staticmethod - def download_png(server, view_item, args, logger): + def download_png(server_content_type, export_item, args, logger): logger.debug(args.url) image_options = TSC.ImageRequestOptions(maxage=1) ExportCommand.apply_values_from_url_params(logger, image_options, args.url) ExportCommand.apply_filters_from_args(image_options, args, logger) DatasourcesAndWorkbooks.apply_png_options(logger, image_options, args) logger.debug(image_options.get_query_params()) - server.views.populate_image(view_item, image_options) - return view_item.image - - @staticmethod - 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")) - 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("/") - if len(name_parts) != 2: - return None, None - workbook = name_parts[0] - view = "{}/sheets/{}".format(workbook, name_parts[1]) - return view, workbook + server_content_type.populate_image(export_item, image_options) + return export_item.image diff --git a/tabcmd/commands/datasources_and_workbooks/get_url_command.py b/tabcmd/commands/datasources_and_workbooks/get_url_command.py index 9f84c242..05e52a8f 100644 --- a/tabcmd/commands/datasources_and_workbooks/get_url_command.py +++ b/tabcmd/commands/datasources_and_workbooks/get_url_command.py @@ -1,8 +1,6 @@ import inspect -import os import tableauserverclient as TSC -from tableauserverclient import ServerResponseError from tabcmd.commands.auth.session import Session from tabcmd.commands.constants import Errors @@ -10,6 +8,7 @@ from tabcmd.execution.localize import _ from tabcmd.execution.logger_config import log from .datasources_and_workbooks_command import DatasourcesAndWorkbooks +from .datasources_workbooks_views_url_parser import DatasourcesWorkbooksAndViewsUrlParser class GetUrl(DatasourcesAndWorkbooks): @@ -48,7 +47,7 @@ def run_command(args): url = args.url.lstrip("/") # strip opening / if present content_type = GetUrl.evaluate_content_type(logger, url) - file_type = GetUrl.get_file_type_from_filename(logger, url, args.filename) + file_type = DatasourcesWorkbooksAndViewsUrlParser.get_file_type_from_filename(logger, url, args.filename) GetUrl.get_content_as_file(file_type, content_type, logger, args, server, url) @@ -65,79 +64,6 @@ def evaluate_content_type(logger, url): return content_type Errors.exit_with_error(logger, message=_("bad_request.detail.invalid_content_type").format(url)) - @staticmethod - def explain_expected_url(logger, url: str, command: str): - view_example = "/views//[.ext]" - wb_example = "/workbooks/[.ext]" - ds_example = "/datasources/ "wb-name", 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") - 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, url, "GetUrl") - 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) - return DatasourcesAndWorkbooks.get_view_url_from_names(workbook_name, view_name) - @staticmethod def filename_from_args(file_argument, item_name, filetype): if file_argument is None: @@ -157,71 +83,74 @@ def get_content_as_file(file_type, content_type, logger, args, server, 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) + ( + get_url_item, + server_content_type, + ) = DatasourcesWorkbooksAndViewsUrlParser.get_url_item_and_item_type_from_view_url(logger, url, server) + if file_type == "pdf": - return GetUrl.generate_pdf(logger, server, args, view_url) + return GetUrl.generate_pdf(logger, server_content_type, args, get_url_item) elif file_type == "png": - return GetUrl.generate_png(logger, server, args, view_url) + return GetUrl.generate_png(logger, server_content_type, args, get_url_item) elif file_type == "csv": - return GetUrl.generate_csv(logger, server, args, view_url) + return GetUrl.generate_csv(logger, server_content_type, args, get_url_item) # all the known options above will return early. If we get here we are confused. Errors.exit_with_error(logger, message=_("get.extension.not_found")) @staticmethod - def generate_pdf(logger, server, args, view_url): + def generate_pdf(logger, server_content_type, args, get_url_item): 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)) + logger.debug(_("content_type.view") + ": {}".format(get_url_item.name)) req_option_pdf = TSC.PDFRequestOptions(maxage=1) DatasourcesAndWorkbooks.apply_values_from_url_params(logger, req_option_pdf, args.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) + server_content_type.populate_pdf(get_url_item, req_option_pdf) + filename = GetUrl.filename_from_args(args.filename, get_url_item.name, "pdf") + DatasourcesAndWorkbooks.save_to_file(logger, get_url_item.pdf, filename) except Exception as e: Errors.exit_with_error(logger, exception=e) @staticmethod - def generate_png(logger, server, args, view_url): + def generate_png(logger, server_content_type, args, get_url_item): 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)) + logger.debug(_("content_type.view") + ": {}".format(get_url_item.name)) req_option_csv = TSC.ImageRequestOptions(maxage=1) DatasourcesAndWorkbooks.apply_values_from_url_params(logger, req_option_csv, args.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) + server_content_type.populate_image(get_url_item, req_option_csv) + filename = GetUrl.filename_from_args(args.filename, get_url_item.name, "png") + DatasourcesAndWorkbooks.save_to_file(logger, get_url_item.image, filename) except Exception as e: Errors.exit_with_error(logger, exception=e) @staticmethod - def generate_csv(logger, server, args, view_url): + def generate_csv(logger, server_content_type, args, get_url_item): 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)) + logger.debug(_("content_type.view") + ": {}".format(get_url_item.name)) req_option_csv = TSC.CSVRequestOptions(maxage=1) DatasourcesAndWorkbooks.apply_values_from_url_params(logger, req_option_csv, args.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) + server_content_type.populate_csv(get_url_item, req_option_csv) + file_name_with_path = GetUrl.filename_from_args(args.filename, get_url_item.name, "csv") + DatasourcesAndWorkbooks.save_to_data_file(logger, get_url_item.csv, file_name_with_path) except Exception as e: Errors.exit_with_error(logger, exception=e) @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) + workbook_name = DatasourcesWorkbooksAndViewsUrlParser.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)) file_name_with_path = GetUrl.filename_from_args(args.filename, workbook_name, file_extension) # the download method will add an extension. How do I tell which one? - file_name_with_path = GetUrl.get_name_without_possible_extension(file_name_with_path) + file_name_with_path = DatasourcesWorkbooksAndViewsUrlParser.get_name_without_possible_extension( + file_name_with_path + ) file_name_with_ext = "{}.{}".format(file_name_with_path, file_extension) logger.debug("Saving as {}".format(file_name_with_ext)) - server.workbooks.download(target_workbook.id, filepath=file_name_with_path, no_extract=False) + server.workbooks.download(target_workbook.id, filepath=file_name_with_path, include_extract=True) logger.info(_("export.success").format(target_workbook.name, file_name_with_ext)) except Exception as e: Errors.exit_with_error(logger, exception=e) @@ -229,16 +158,18 @@ def generate_twb(logger, server, args, file_extension, url): @staticmethod def generate_tds(logger, server, args, file_extension): logger.trace("Entered method " + inspect.stack()[0].function) - datasource_name = GetUrl.get_resource_name(args.url, logger) + datasource_name = DatasourcesWorkbooksAndViewsUrlParser.get_resource_name(args.url, logger) try: target_datasource = GetUrl.get_ds_by_content_url(logger, server, datasource_name) logger.debug(_("content_type.datasource") + ": {}".format(datasource_name)) file_name_with_path = GetUrl.filename_from_args(args.filename, datasource_name, file_extension) # the download method will add an extension - file_name_with_path = GetUrl.get_name_without_possible_extension(file_name_with_path) + file_name_with_path = DatasourcesWorkbooksAndViewsUrlParser.get_name_without_possible_extension( + file_name_with_path + ) file_name_with_ext = "{}.{}".format(file_name_with_path, file_extension) logger.debug("Saving as {}".format(file_name_with_ext)) - server.datasources.download(target_datasource.id, filepath=file_name_with_path, no_extract=False) + server.datasources.download(target_datasource.id, filepath=file_name_with_path, include_extract=True) logger.info(_("export.success").format(target_datasource.name, file_name_with_ext)) except Exception as e: Errors.exit_with_error(logger, exception=e) diff --git a/tabcmd/commands/server.py b/tabcmd/commands/server.py index d315e6dd..4091c0ba 100644 --- a/tabcmd/commands/server.py +++ b/tabcmd/commands/server.py @@ -55,7 +55,7 @@ def get_items_by_name(logger, item_endpoint, item_name: str, container: Optional all_items, pagination_item = item_endpoint.get(req_option) if all_items is None or all_items == []: raise TSC.ServerResponseError( - code=404, + code="404", summary=_("errors.xmlapi.not_found"), detail=_("errors.xmlapi.not_found") + ": " + item_log_name, ) diff --git a/tabcmd/locales/de/tabcmd_messages_de.properties b/tabcmd/locales/de/tabcmd_messages_de.properties index 45ca19b6..d43cbf80 100644 --- a/tabcmd/locales/de/tabcmd_messages_de.properties +++ b/tabcmd/locales/de/tabcmd_messages_de.properties @@ -165,6 +165,7 @@ export.errors.need_country_and_languge=Die Optionen --Land und --Sprache müssen export.errors.white_space_workbook_view=Der Name der zu exportierenden Arbeitsmappe oder Ansicht darf keine Leerzeichen enthalten. Geben Sie den normalisierten Namen der Arbeitsmappe oder Ansicht an, wie er in der URL angezeigt wird. export.errors.requires_workbook_view_name=Für den Befehl „{0}“ ist ein /-Name erforderlich. export.errors.requires_workbook_view_param=Für den Befehl „{0}“ ist ein /-Parameter erforderlich, und es muss mindestens ein Schrägstrich (/) in diesem Parameter vorhanden sein. +export.errors.requires_valid_custom_view_uuid=The URL for custom views must contain a valid custom view uuid export.options.country=Wenn das standardmäßige Gebietsschema des Benutzers nicht verwendet wird, muss die Landesabkürzung für Gebietsschema (zu finden in der IANA Language Subtag Registry) mit --Sprache verwendet werden. export.options.csv=Daten im CSV-Format (Standard) exportieren export.options.fullpdf=Visuelle Ansichten im PDF-Format (wenn die Arbeitsmappe mit Registerkarten veröffentlicht wurde) exportieren diff --git a/tabcmd/locales/en/tabcmd_messages_en.properties b/tabcmd/locales/en/tabcmd_messages_en.properties index 05e20f34..ee0a1e1a 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_valid_custom_view_uuid=The URL for custom views must contain a valid custom view uuid 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) diff --git a/tabcmd/locales/es/tabcmd_messages_es.properties b/tabcmd/locales/es/tabcmd_messages_es.properties index c77c2082..623799bb 100644 --- a/tabcmd/locales/es/tabcmd_messages_es.properties +++ b/tabcmd/locales/es/tabcmd_messages_es.properties @@ -165,6 +165,7 @@ export.errors.need_country_and_languge=Las opciones --country y --language se de export.errors.white_space_workbook_view=El nombre del libro de trabajo o la vista de la exportación no puede incluir espacios. Use el nombre normalizado del libro de trabajo o la vista tal como aparece en la URL. export.errors.requires_workbook_view_name=El comando “{0}” requiere un nombre de / export.errors.requires_workbook_view_param=El comando “{0}” requiere un parámetro de /, que debe contener al menos una barra (/) +export.errors.requires_valid_custom_view_uuid=The URL for custom views must contain a valid custom view uuid export.options.country=Si no se utiliza la configuración regional predeterminada del usuario, se debe usar la abreviatura de la configuración regional del país (que se puede encontrar en el registro de subcategorías de idioma de la IANA). con el comando --language export.options.csv=Exportar datos en formato CSV (predeterminado) export.options.fullpdf=Exportar las vistas visuales en formato PDF (si el libro de trabajo se ha publicado con pestañas) diff --git a/tabcmd/locales/fr/tabcmd_messages_fr.properties b/tabcmd/locales/fr/tabcmd_messages_fr.properties index 854510f9..bd88a646 100644 --- a/tabcmd/locales/fr/tabcmd_messages_fr.properties +++ b/tabcmd/locales/fr/tabcmd_messages_fr.properties @@ -165,6 +165,7 @@ export.errors.need_country_and_languge=Les options --country et --language doive export.errors.white_space_workbook_view=Le nom du classeur ou de la vue à exporter ne peut pas inclure d’espaces. Utilisez le nom normalisé du classeur ou de la vue tel qu’il apparaît dans l’URL. export.errors.requires_workbook_view_name=La commande “{0}” nécessite un nom de / export.errors.requires_workbook_view_param=La commande “{0}” nécessite un paramètre de / contenant au moins une barre oblique (/) +export.errors.requires_valid_custom_view_uuid=The URL for custom views must contain a valid custom view uuid export.options.country=Si les paramètres régionaux par défaut de l’utilisateur sont utilisés, abréviation du pays pour les paramètres régionaux (se trouve dans IANA Language Subtag Registry). À utiliser avec --language export.options.csv=Exporter des données dans le format CSV (par défaut) export.options.fullpdf=Exporter des vues dans le format PDF (si le classeur a été publié avec des onglets) diff --git a/tabcmd/locales/ga/tabcmd_messages_ga.properties b/tabcmd/locales/ga/tabcmd_messages_ga.properties index 776c45d1..ce4b6193 100644 --- a/tabcmd/locales/ga/tabcmd_messages_ga.properties +++ b/tabcmd/locales/ga/tabcmd_messages_ga.properties @@ -165,6 +165,7 @@ export.errors.need_country_and_languge=bf52-表:The options --country and --lang export.errors.white_space_workbook_view=e1f6-表: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=cafc-表:The ‘{0}’ command requires a / name|桜 export.errors.requires_workbook_view_param=dbd2-表:The ‘{0}’ command requires a / parameter, and there must be at least one slash (/) in this parameter|桜 +export.errors.requires_valid_custom_view_uuid=The URL for custom views must contain a valid custom view uuid export.options.country=33e3-表: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=4e37-表:Export data in CSV format (default)|桜 export.options.fullpdf=389e-表:Export visual views in PDF format (if workbook was published with tabs)|桜 diff --git a/tabcmd/locales/it/tabcmd_messages_it.properties b/tabcmd/locales/it/tabcmd_messages_it.properties index 447056f9..34e5d247 100644 --- a/tabcmd/locales/it/tabcmd_messages_it.properties +++ b/tabcmd/locales/it/tabcmd_messages_it.properties @@ -165,6 +165,7 @@ export.errors.need_country_and_languge=Le opzioni --country e --language devono export.errors.white_space_workbook_view=Il nome della cartella di lavoro o della vista da esportare non può includere spazi. Utilizza il nome normalizzato della cartella di lavoro o della vista come appare nell’URL. export.errors.requires_workbook_view_name=Il comando “{0}” richiede un nome / export.errors.requires_workbook_view_param=Il comando “{0}” richiede un parametro / e ci deve essere almeno una barra (/) in questo parametro +export.errors.requires_valid_custom_view_uuid=The URL for custom views must contain a valid custom view uuid export.options.country=Se non vengono utilizzate le impostazioni locali predefinite dell’utente, l’abbreviazione del paese per le impostazioni locali (disponibile nel registro dei sottotag delle lingue IANA). Da utilizzare con --language export.options.csv=Esporta i dati in formato CSV (predefinito) export.options.fullpdf=Esporta viste visive in formato PDF (se la cartella di lavoro è stata pubblicata con schede) diff --git a/tabcmd/locales/ja/tabcmd_messages_ja.properties b/tabcmd/locales/ja/tabcmd_messages_ja.properties index e59ff9c1..17853b75 100644 --- a/tabcmd/locales/ja/tabcmd_messages_ja.properties +++ b/tabcmd/locales/ja/tabcmd_messages_ja.properties @@ -165,6 +165,7 @@ export.errors.need_country_and_languge=オプションの「--country」と「-- export.errors.white_space_workbook_view=エクスポートするワークブックやビューの名前にスペースを含めることはできません。URL に表示されるワークブックまたはビューの標準化された名前を使用してください。 export.errors.requires_workbook_view_name=「{0}」 コマンドには / 名が必要です export.errors.requires_workbook_view_param=「{0}」 コマンドには / パラメーターが必要です。このパラメーターには、少なくとも 1 個のスラッシュ (/) が必要です +export.errors.requires_valid_custom_view_uuid=The URL for custom views must contain a valid custom view uuid export.options.country=既定のユーザー ロケールを使用しない場合は、ロケールの国の略名 (IANA Language Subtag Registry から検索) を使用します。「--language」とセットで使用する必要があります export.options.csv=データを CSV 形式でエクスポート (既定) export.options.fullpdf=ビジュアル ビューを PDF 形式でエクスポート (ワークブックがタブ付きでパブリッシュされた場合) diff --git a/tabcmd/locales/ko/tabcmd_messages_ko.properties b/tabcmd/locales/ko/tabcmd_messages_ko.properties index 4f1ab1ec..04c5b757 100644 --- a/tabcmd/locales/ko/tabcmd_messages_ko.properties +++ b/tabcmd/locales/ko/tabcmd_messages_ko.properties @@ -165,6 +165,7 @@ export.errors.need_country_and_languge=--country 옵션과 --language 옵션은 export.errors.white_space_workbook_view=내보낼 통합 문서 또는 뷰의 이름에 공백을 포함할 수 없습니다. 통합 문서 또는 뷰의 정규화된 이름을 URL에 표시되는 대로 사용하십시오. export.errors.requires_workbook_view_name=’{0}’ 명령에는 / 이름이 필요합니다. export.errors.requires_workbook_view_param=’{0}’ 명령에는 / 매개 변수가 필요하고, 이 매개 변수에는 하나 이상의 슬래시(/)가 있어야 합니다. +export.errors.requires_valid_custom_view_uuid=The URL for custom views must contain a valid custom view uuid export.options.country=사용자의 기본 로캘을 사용하지 않는 경우 로캘의 국가 약어입니다(IANA Language Subtag Registry에서 찾을 수 있음). --language와 함께 사용해야 합니다. export.options.csv=CSV 형식으로 데이터 내보내기(기본값) export.options.fullpdf=PDF 형식으로 시각적 뷰 내보내기(통합 문서가 탭과 함께 게시된 경우) diff --git a/tabcmd/locales/pt/tabcmd_messages_pt.properties b/tabcmd/locales/pt/tabcmd_messages_pt.properties index 59ae77f2..59322906 100644 --- a/tabcmd/locales/pt/tabcmd_messages_pt.properties +++ b/tabcmd/locales/pt/tabcmd_messages_pt.properties @@ -165,6 +165,7 @@ export.errors.need_country_and_languge=As opções --país e --idioma devem ser export.errors.white_space_workbook_view=O nome da pasta de trabalho ou da exibição a ser exportada não pode incluir espaços. Use o nome normalizado da pasta de trabalho ou da exibição, conforme aparece na URL. export.errors.requires_workbook_view_name=O comando “{0}” requer um nome / export.errors.requires_workbook_view_param=O comando “{0}” requer um parâmetro /, e deve haver pelo menos uma barra (/) neste parâmetro +export.errors.requires_valid_custom_view_uuid=The URL for custom views must contain a valid custom view uuid export.options.country=Se não estiver usando a localidade padrão do usuário, a abreviação de país da localidade (encontre no Registro de marca secundária de idioma IANA). Deve usar com o --idioma export.options.csv=Exporte dados em formato CSV (padrão) export.options.fullpdf=Exporte exibições visuais em formato PDF (se a pasta de trabalho foi publicada com guias) diff --git a/tabcmd/locales/sv/tabcmd_messages_sv.properties b/tabcmd/locales/sv/tabcmd_messages_sv.properties index ff27d31b..235de246 100644 --- a/tabcmd/locales/sv/tabcmd_messages_sv.properties +++ b/tabcmd/locales/sv/tabcmd_messages_sv.properties @@ -165,6 +165,7 @@ export.errors.need_country_and_languge=Alternativen --country och --language må export.errors.white_space_workbook_view=Namnet på den arbetsbok eller vy som ska exporteras får inte innehålla blanksteg. Använd arbetsbokens eller vyns normaliserade namn så som det visas i URL:en. export.errors.requires_workbook_view_name=Kommandot ”{0}” kräver ett namn på / export.errors.requires_workbook_view_param=Kommandot ”{0}” kräver en /-parameter, och parametern måste innehålla minst ett snedstreck (/) +export.errors.requires_valid_custom_view_uuid=The URL for custom views must contain a valid custom view uuid export.options.country=Om du inte använder användarens standardspråkzon, så ange språkzonens landsförkortning (finns i IANA Language Subtag Registry). Måste användas med --language export.options.csv=Exportera data i CSV-format (standard) export.options.fullpdf=Exportera visuella vyer i PDF-format (om arbetsboken publicerades med flikar) diff --git a/tabcmd/locales/zh/tabcmd_messages_zh.properties b/tabcmd/locales/zh/tabcmd_messages_zh.properties index 5985a9f8..43fd14c5 100644 --- a/tabcmd/locales/zh/tabcmd_messages_zh.properties +++ b/tabcmd/locales/zh/tabcmd_messages_zh.properties @@ -165,6 +165,7 @@ export.errors.need_country_and_languge=选项 --country 和 --language 必须一 export.errors.white_space_workbook_view=要导出的工作簿或视图的名称不能包括空格。请使用工作簿或视图出现在 URL 中时的标准化名称。 export.errors.requires_workbook_view_name=“{0}”命令需要一个 / 名称 export.errors.requires_workbook_view_param=“{0}”命令需要一个 / 参数,而且在此参数中至少必须有一个斜杠(/) +export.errors.requires_valid_custom_view_uuid=The URL for custom views must contain a valid custom view uuid export.options.country=如果未使用用户的默认区域设置,则为区域设置的国家/地区缩写(在 IANA 语言子标记注册表中查找)。必须与 --language 一起使用 export.options.csv=将数据导出为 CSV 格式(默认) export.options.fullpdf=将可视化视图导出为 PDF 格式(如果发布了带选项卡的工作簿) diff --git a/tests/commands/test_datasources_and_workbooks_command.py b/tests/commands/test_datasources_and_workbooks_command.py index 0cbf765b..446822dc 100644 --- a/tests/commands/test_datasources_and_workbooks_command.py +++ b/tests/commands/test_datasources_and_workbooks_command.py @@ -14,6 +14,7 @@ getter = MagicMock() getter.get = MagicMock("get", return_value=([fake_item], 1)) +getter.get_by_id = MagicMock("get_by_id", return_value=([fake_item], 1)) mock_args = argparse.Namespace() @@ -91,3 +92,9 @@ def test_get_view_by_content_url(self, mock_server): DatasourcesAndWorkbooks.get_view_by_content_url(mock_logger, mock_server, content_url) getter.get.assert_called() # should also assert the filter on content url + + def test_get_custom_view_by_id(self, mock_server): + mock_server.custom_views = getter + custom_view_id = "cv-id" + DatasourcesAndWorkbooks.get_custom_view_by_id(mock_logger, mock_server, custom_view_id) + getter.get_by_id.assert_called() diff --git a/tests/commands/test_geturl_utils.py b/tests/commands/test_geturl_utils.py index ec63e4df..a7a4b2f2 100644 --- a/tests/commands/test_geturl_utils.py +++ b/tests/commands/test_geturl_utils.py @@ -1,11 +1,12 @@ import unittest -from typing import Iterator +import uuid 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.datasources_and_workbooks.datasources_workbooks_views_url_parser import * from tabcmd.commands.server import Server mock_args = argparse.Namespace() @@ -23,6 +24,12 @@ fake_item.name = "fake-name" fake_item.id = "fake-id" +fake_cv_id = str(uuid.uuid4()) +fake_cv_item = mock.MagicMock(TSC.CustomViewItem) +fake_cv_item.name = "custom-view-name" +fake_cv_item.id = fake_cv_id +fake_cv_item.view.id = fake_item.id + class FileHandling(unittest.TestCase): @@ -38,13 +45,13 @@ class FileHandling(unittest.TestCase): def test_get_view_with_chars_in_save_name(self): filename = "C:\\chase.culver\\docs\\downloaded.twbx" # W-13757625 fails if file path contains . url = None - filetype = GetUrl.get_file_type_from_filename(mock_logger, filename, url) + filetype = DatasourcesWorkbooksAndViewsUrlParser.get_file_type_from_filename(mock_logger, filename, url) assert filetype == "twbx", filetype def test_evaluate_file_name_pdf(self): filename = "filename.pdf" url = None - filetype = GetUrl.get_file_type_from_filename(mock_logger, filename, url) + filetype = DatasourcesWorkbooksAndViewsUrlParser.get_file_type_from_filename(mock_logger, filename, url) assert filetype == "pdf", filetype def test_evaluate_file_name_url(self): @@ -69,23 +76,161 @@ def test_evaluate_file_name_url_no_ext_fails(self): def test_get_view_without_extension_that_does_have_one(self): filename = "viewname.pdf" - assert GetUrl.get_name_without_possible_extension(filename) == "viewname" + assert DatasourcesWorkbooksAndViewsUrlParser.get_name_without_possible_extension(filename) == "viewname" def test_get_view_without_extension_that_doesnt_have_one(self): filename = "viewname" - assert GetUrl.get_name_without_possible_extension(filename) == filename + assert DatasourcesWorkbooksAndViewsUrlParser.get_name_without_possible_extension(filename) == filename # handling our specific url-ish identifiers: /workbook/wb-name, etc class GeturlTests(unittest.TestCase): def test_get_workbook_name(self): - assert GetUrl.get_resource_name("workbooks/wbname", mock_logger) == "wbname" + assert DatasourcesWorkbooksAndViewsUrlParser.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" + assert ( + DatasourcesWorkbooksAndViewsUrlParser.get_view_url_from_get_url(mock_logger, "views/wb-name/view-name") + == "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" + assert ( + DatasourcesWorkbooksAndViewsUrlParser.get_view_url_from_get_url( + mock_logger, "views/wb-name/view-name?:refresh=y" + ) + == "wb-name/sheets/view-name" + ) + + def test_get_url_parts_from_custom_view_url(self): + cv_uuid = str(uuid.uuid4()) + custom_view_url = "views/wb-name/view-name/" + cv_uuid + "/custom-view-name" + ( + view_url, + custom_view_id, + custom_view_name, + ) = DatasourcesWorkbooksAndViewsUrlParser.get_custom_view_parts_from_get_url(mock_logger, custom_view_url) + assert view_url == "wb-name/sheets/view-name" + assert custom_view_id == cv_uuid + assert custom_view_name == "custom-view-name" + + def test_get_url_parts_from_custom_view_url_invalid_cv_id(self): + custom_view_url = "views/wb-name/view-name/cv_uuid/custom-view-name" + with self.assertRaises(SystemExit): + DatasourcesWorkbooksAndViewsUrlParser.get_custom_view_parts_from_get_url(mock_logger, custom_view_url) + + def test_get_url_parts_from_custom_view_url_bad_url(self): + custom_view_url = "views/wb-name/view-name/cv_uuid/custom-view-name/kitty" + with self.assertRaises(SystemExit): + DatasourcesWorkbooksAndViewsUrlParser.get_custom_view_parts_from_get_url(mock_logger, custom_view_url) + + def test_get_url_parts_from_custom_view_url_with_url_params(self): + cv_uuid = str(uuid.uuid4()) + custom_view_url = "views/wb-name/view-name/" + cv_uuid + "/custom-view-name?:refresh=yes" + ( + view_url, + custom_view_id, + custom_view_name, + ) = DatasourcesWorkbooksAndViewsUrlParser.get_custom_view_parts_from_get_url(mock_logger, custom_view_url) + assert view_url == "wb-name/sheets/view-name" + assert custom_view_id == cv_uuid + assert custom_view_name == "custom-view-name" + + def test_get_url_parts_from_custom_view_url_with_file_extension(self): + cv_uuid = str(uuid.uuid4()) + custom_view_url = "views/wb-name/view-name/" + cv_uuid + "/custom-view-name.png" + ( + view_url, + custom_view_id, + custom_view_name, + ) = DatasourcesWorkbooksAndViewsUrlParser.get_custom_view_parts_from_get_url(mock_logger, custom_view_url) + assert view_url == "wb-name/sheets/view-name" + assert custom_view_id == cv_uuid + assert custom_view_name == "custom-view-name" + + def test_parse_get_url_to_view_parts(self): + url = "views/wb-name/view-name" + ( + view_url, + cv_id, + cv_name, + ) = DatasourcesWorkbooksAndViewsUrlParser.parse_get_view_url_to_view_and_custom_view_parts(mock_logger, url) + assert view_url == "wb-name/sheets/view-name" + assert cv_id is None + assert cv_name is None + + def test_parse_get_url_to_view_parts_with_params(self): + url = "views/wb-name/view-name?params=1" + ( + view_url, + cv_id, + cv_name, + ) = DatasourcesWorkbooksAndViewsUrlParser.parse_get_view_url_to_view_and_custom_view_parts(mock_logger, url) + assert view_url == "wb-name/sheets/view-name" + assert cv_id is None + assert cv_name is None + + def test_parse_get_url_to_view_parts_with_spaces(self): + url = "views/wb name/view-name" + with self.assertRaises(SystemExit): + DatasourcesWorkbooksAndViewsUrlParser.parse_get_view_url_to_view_and_custom_view_parts(mock_logger, url) + + def test_parse_get_url_to_view_parts_without_slashes(self): + url = "views\wb name\\view-name" + with self.assertRaises(SystemExit): + DatasourcesWorkbooksAndViewsUrlParser.parse_get_view_url_to_view_and_custom_view_parts(mock_logger, url) + + def test_parse_get_url_to_custom_view_parts(self): + cv_uuid = str(uuid.uuid4()) + custom_view_url = "views/wb-name/view-name/" + cv_uuid + "/custom-view-name" + ( + view_url, + custom_view_id, + custom_view_name, + ) = DatasourcesWorkbooksAndViewsUrlParser.parse_get_view_url_to_view_and_custom_view_parts( + mock_logger, custom_view_url + ) + assert view_url == "wb-name/sheets/view-name" + assert custom_view_id == cv_uuid + assert custom_view_name == "custom-view-name" + + def test_parse_get_url_to_custom_view_parts_with_file_extension(self): + cv_uuid = str(uuid.uuid4()) + custom_view_url = "views/wb-name/view-name/" + cv_uuid + "/custom-view-name.png" + ( + view_url, + custom_view_id, + custom_view_name, + ) = DatasourcesWorkbooksAndViewsUrlParser.parse_get_view_url_to_view_and_custom_view_parts( + mock_logger, custom_view_url + ) + assert view_url == "wb-name/sheets/view-name" + assert custom_view_id == cv_uuid + assert custom_view_name == "custom-view-name" + + @mock.patch("tableauserverclient.Server") + def test_get_url_item_and_item_type_from_view_url(self, mock_server): + view_url = "views/wb-name/view-name" + mock_server.views = mock.MagicMock() + mock_server.views.get = mock.MagicMock("get", return_value=([fake_item], 1)) + view_item, server_content_type = DatasourcesWorkbooksAndViewsUrlParser.get_url_item_and_item_type_from_view_url( + mock_logger, view_url, mock_server + ) + assert view_item == fake_item + assert server_content_type == mock_server.views + + @mock.patch("tableauserverclient.Server") + def test_get_url_item_and_item_type_from_custom_view_url(self, mock_server): + view_url = "views/wb-name/view-name/" + fake_cv_id + "/custom-view-name" + mock_server.views = mock.MagicMock() + mock_server.views.get = mock.MagicMock("get", return_value=([fake_item], 1)) + mock_server.custom_views = mock.MagicMock() + mock_server.custom_views.get_by_id = mock.MagicMock("get_by_id", return_value=fake_cv_item) + cv_item, server_content_type = DatasourcesWorkbooksAndViewsUrlParser.get_url_item_and_item_type_from_view_url( + mock_logger, view_url, mock_server + ) + assert cv_item == fake_cv_item + assert server_content_type == mock_server.custom_views """ GetUrl.get_view(url) @@ -109,23 +254,109 @@ class ExportTests(unittest.TestCase): fake_item.pdf = mock.MagicMock("bytes") fake_item.png = mock.MagicMock("bytes") - def test_parse_export_url_to_workbook_and_view(self): + def test_parse_export_url_to_workbook_view_and_custom_view(self): wb_url = "wb-name/view-name" - view, wb = ExportCommand.parse_export_url_to_workbook_and_view(mock_logger, wb_url) + ( + view, + wb, + cv_id, + cv_name, + ) = DatasourcesWorkbooksAndViewsUrlParser.parse_export_url_to_workbook_view_and_custom_view(mock_logger, wb_url) assert view == "wb-name/sheets/view-name" assert wb == "wb-name" + assert cv_id is None + assert cv_name is None - def test_parse_export_url_to_workbook_and_view_with_start_slash(self): + def test_parse_export_url_to_workbook_view_and_custom_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) + ( + view, + wb, + cv_id, + cv_name, + ) = DatasourcesWorkbooksAndViewsUrlParser.parse_export_url_to_workbook_view_and_custom_view(mock_logger, wb_url) assert view == "wb-name/sheets/view-name" assert wb == "wb-name" + assert cv_id is None + assert cv_name is None - def test_parse_export_url_to_workbook_and_view_bad_url(self): + def test_parse_export_url_to_workbook_view_and_custom_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) + ( + view, + wb, + cv_id, + cv_name, + ) = DatasourcesWorkbooksAndViewsUrlParser.parse_export_url_to_workbook_view_and_custom_view(mock_logger, wb_url) + assert view is None + assert wb is None + assert cv_id is None + assert cv_name is None + + def test_parse_export_url_to_workbook_view_and_custom_view_with_cv_parts(self): + cv_uuid = str(uuid.uuid4()) + custom_view_name = "custom-view-name" + wb_url = "/wb-name/view-name/" + cv_uuid + "/" + custom_view_name + ( + view, + wb, + cv_id, + cv_name, + ) = DatasourcesWorkbooksAndViewsUrlParser.parse_export_url_to_workbook_view_and_custom_view(mock_logger, wb_url) + assert view == "wb-name/sheets/view-name" + assert wb == "wb-name" + assert cv_id == cv_uuid + assert cv_name == custom_view_name + + def test_parse_export_url_to_workbook_view_and_custom_view_with_bad_cv_parts(self): + cv_uuid = str(uuid.uuid4()) + custom_view_name = "custom-view-name" + wb_url = "/wb-name/view-name/" + cv_uuid + "/" + custom_view_name + "/kitty" + ( + view, + wb, + cv_id, + cv_name, + ) = DatasourcesWorkbooksAndViewsUrlParser.parse_export_url_to_workbook_view_and_custom_view(mock_logger, wb_url) assert view is None assert wb is None + assert cv_id is None + assert cv_name is None + + def test_parse_export_url_to_workbook_view_and_custom_view_with_invalid_cv_id(self): + wb_url = "/wb-name/view-name/cv-id/cv-name" + with self.assertRaises(SystemExit): + DatasourcesWorkbooksAndViewsUrlParser.parse_export_url_to_workbook_view_and_custom_view(mock_logger, wb_url) + + @mock.patch("tableauserverclient.Server") + def test_get_export_item_and_item_type_for_view(self, mock_server): + view_url = "wb-name/sheets/view-name" + mock_server.views = mock.MagicMock() + mock_server.views.get = mock.MagicMock("get", return_value=([fake_item], 1)) + ( + view_item, + server_content_type, + ) = DatasourcesWorkbooksAndViewsUrlParser.get_export_item_and_server_content_type_from_export_url( + view_url, mock_logger, mock_server, None + ) + assert view_item == fake_item + assert server_content_type == mock_server.views + + @mock.patch("tableauserverclient.Server") + def test_get_export_item_and_item_type_for_custom_view(self, mock_server): + view_url = "wb-name/sheets/view-name" + mock_server.views = mock.MagicMock() + mock_server.views.get = mock.MagicMock("get", return_value=([fake_item], 1)) + mock_server.custom_views = mock.MagicMock() + mock_server.custom_views.get_by_id = mock.MagicMock("get_by_id", return_value=fake_cv_item) + ( + cv_item, + server_content_type, + ) = DatasourcesWorkbooksAndViewsUrlParser.get_export_item_and_server_content_type_from_export_url( + view_url, mock_logger, mock_server, fake_cv_id + ) + assert cv_item == fake_cv_item + assert server_content_type == mock_server.custom_views @mock.patch("tableauserverclient.Server") def test_download_csv(self, mock_server):