From c897906e54b059995de1077dfbed6e50bf0baf26 Mon Sep 17 00:00:00 2001 From: Jac Date: Mon, 4 Mar 2024 14:27:10 -0800 Subject: [PATCH 1/8] Auto-trigger build and upload binaries when a release is created Tested in another repo: https://github.com/jacalata/tabcmd/actions --- .github/workflows/package.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 34d84b67..5d7132d9 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: @@ -70,3 +71,12 @@ jobs: with: name: ${{ matrix.OUT_FILE_NAME }} path: ./dist/${{ matrix.TARGET }}/${{ matrix.OUT_FILE_NAME }} + + - 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 From e3aab5ef2d3f89282b3be80364e2eebc7212d415 Mon Sep 17 00:00:00 2001 From: Renoy John Date: Tue, 12 Nov 2024 16:46:01 -0800 Subject: [PATCH 2/8] Adding support for export for custom views to pdf,png & csv formats --- pyproject.toml | 2 +- .../datasources_and_workbooks_command.py | 12 ++ .../export_command.py | 85 ++++++--- .../get_url_command.py | 104 ++++++++--- .../locales/en/tabcmd_messages_en.properties | 1 + .../test_datasources_and_workbooks_command.py | 7 + tests/commands/test_geturl_utils.py | 173 +++++++++++++++++- 7 files changed, 325 insertions(+), 59 deletions(-) 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..f125a33e 100644 --- a/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py +++ b/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py @@ -1,6 +1,7 @@ import urllib import tableauserverclient as TSC +from tableauserverclient import CustomViewItem from tabcmd.commands.constants import Errors from tabcmd.commands.server import Server @@ -152,3 +153,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 \ No newline at end of file diff --git a/tabcmd/commands/datasources_and_workbooks/export_command.py b/tabcmd/commands/datasources_and_workbooks/export_command.py index 23012b51..f4c251be 100644 --- a/tabcmd/commands/datasources_and_workbooks/export_command.py +++ b/tabcmd/commands/datasources_and_workbooks/export_command.py @@ -1,5 +1,7 @@ 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 _ @@ -76,7 +78,8 @@ 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 = ( + ExportCommand.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 +95,19 @@ 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 = ExportCommand.get_export_item_and_server_content_type( + 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) @@ -120,6 +123,20 @@ def run_command(args): except Exception as e: Errors.exit_with_error(logger, "Error saving to file", e) + @staticmethod + def get_export_item_and_server_content_type(view_content_url, logger, server, custom_view_id): + export_item = ExportCommand.get_view_by_content_url(logger, server, view_content_url) + server_content_type = server.views + + if custom_view_id: + custom_view_item = ExportCommand.get_custom_view_by_id(logger, server, custom_view_id) + if custom_view_item.view.id != export_item.id: + Errors.exit_with_error(logger, "Invalid custom view URL provided") + server_content_type = server.custom_views + export_item = custom_view_item + + return export_item, server_content_type + @staticmethod def apply_filters_from_args(request_options: TSC.PDFRequestOptions, args, logger=None) -> None: if args.filter: @@ -142,51 +159,67 @@ 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 + server_content_type.populate_image(export_item, image_options) + return export_item.image @staticmethod - def parse_export_url_to_workbook_and_view(logger, url): + def parse_export_url_to_workbook_view_and_custom_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 + # or workbook_name/view_name/custom_view_id/custom_view_name url = url.lstrip("/") # strip opening / if present if not url.find("/"): - return None, None + return None, None, 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 + if len(name_parts) == 2: + workbook = name_parts[0] + view = "{}/sheets/{}".format(workbook, name_parts[1]) + return view, workbook, None, None + elif len(name_parts) == 4: + workbook = name_parts[0] + view = "{}/sheets/{}".format(workbook, name_parts[1]) + custom_view_id = name_parts[2] + ExportCommand.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 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")) \ No newline at end of file diff --git a/tabcmd/commands/datasources_and_workbooks/get_url_command.py b/tabcmd/commands/datasources_and_workbooks/get_url_command.py index 9f84c242..a2ce817b 100644 --- a/tabcmd/commands/datasources_and_workbooks/get_url_command.py +++ b/tabcmd/commands/datasources_and_workbooks/get_url_command.py @@ -2,7 +2,6 @@ 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 +9,7 @@ from tabcmd.execution.localize import _ from tabcmd.execution.logger_config import log from .datasources_and_workbooks_command import DatasourcesAndWorkbooks +from .export_command import ExportCommand class GetUrl(DatasourcesAndWorkbooks): @@ -68,11 +68,13 @@ def evaluate_content_type(logger, url): @staticmethod def explain_expected_url(logger, url: str, command: str): view_example = "/views//[.ext]" + custom_view_example = "/views////[.ext]" wb_example = "/workbooks/[.ext]" ds_example = "/datasources/ wb-name/sheets/vi view_name = GetUrl.get_name_without_possible_extension(view_name) return DatasourcesAndWorkbooks.get_view_url_from_names(workbook_name, view_name) + @staticmethod + def get_url_parts_from_custom_view_url(url, logger): + name_parts = url.split("/") # ['views', 'wb-name', 'view-name', 'custom-view-id', 'custom-view-name'] + if len(name_parts) != 5: + GetUrl.explain_expected_url(logger, url, "GetUrl") + workbook_name = name_parts[1] + view_name = name_parts[2] + custom_view_id = name_parts[3] + ExportCommand.verify_valid_custom_view_id(logger, custom_view_id) + custom_view_name = name_parts[::-1][0] + custom_view_name = GetUrl.strip_query_params(custom_view_name) + custom_view_name = GetUrl.get_name_without_possible_extension(custom_view_name) + return (DatasourcesAndWorkbooks.get_view_url_from_names(workbook_name, view_name), custom_view_id, + custom_view_name) + @staticmethod def filename_from_args(file_argument, item_name, filetype): if file_argument is None: @@ -157,55 +174,53 @@ 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 = GetUrl.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) @@ -221,7 +236,7 @@ def generate_twb(logger, server, args, file_extension, url): file_name_with_path = GetUrl.get_name_without_possible_extension(file_name_with_path) file_name_with_ext = "{}.{}".format(file_name_with_path, file_extension) logger.debug("Saving as {}".format(file_name_with_ext)) - server.workbooks.download(target_workbook.id, filepath=file_name_with_path, no_extract=False) + server.workbooks.download(target_workbook.id, filepath=file_name_with_path, include_extract=False) logger.info(_("export.success").format(target_workbook.name, file_name_with_ext)) except Exception as e: Errors.exit_with_error(logger, exception=e) @@ -238,7 +253,44 @@ def generate_tds(logger, server, args, file_extension): file_name_with_path = GetUrl.get_name_without_possible_extension(file_name_with_path) file_name_with_ext = "{}.{}".format(file_name_with_path, file_extension) logger.debug("Saving as {}".format(file_name_with_ext)) - server.datasources.download(target_datasource.id, filepath=file_name_with_path, no_extract=False) + server.datasources.download(target_datasource.id, filepath=file_name_with_path, include_extract=False) logger.info(_("export.success").format(target_datasource.name, file_name_with_ext)) except Exception as e: Errors.exit_with_error(logger, exception=e) + + @staticmethod + def parse_get_view_url_to_view_and_custom_view_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] + # input should be views/workbook_name/view_name + # or views/workbook_name/view_name/custom_view_id/custom_view_name + url = url.lstrip("/") # strip opening / if present + if not url.find("/"): + GetUrl.explain_expected_url(logger, url, "GetUrl") + name_parts = url.split("/") + if len(name_parts) == 3: + return GetUrl.get_view_url(url, logger), None, None + elif len(name_parts) == 5: + return GetUrl.get_url_parts_from_custom_view_url(url, logger) + else: + GetUrl.explain_expected_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 = GetUrl.parse_get_view_url_to_view_and_custom_view_parts( + logger, url) + + get_url_item = GetUrl.get_view_by_content_url(logger, server, view_url) + get_url_item_type = server.views + + if custom_view_id: + custom_view_item = GetUrl.get_custom_view_by_id(logger, server, custom_view_id) + if custom_view_item.view.id != get_url_item.id: + Errors.exit_with_error(logger, "invalid custom view id provided") + get_url_item = custom_view_item + get_url_item_type = server.custom_views + + return get_url_item, get_url_item_type \ No newline at end of file diff --git a/tabcmd/locales/en/tabcmd_messages_en.properties b/tabcmd/locales/en/tabcmd_messages_en.properties index 05e20f34..e0d4fb51 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 luid 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/tests/commands/test_datasources_and_workbooks_command.py b/tests/commands/test_datasources_and_workbooks_command.py index 0cbf765b..ae42ad4a 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() \ No newline at end of file diff --git a/tests/commands/test_geturl_utils.py b/tests/commands/test_geturl_utils.py index ec63e4df..1e335e26 100644 --- a/tests/commands/test_geturl_utils.py +++ b/tests/commands/test_geturl_utils.py @@ -1,4 +1,5 @@ import unittest +import uuid from typing import Iterator from unittest import mock @@ -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): @@ -87,6 +94,107 @@ def test_view_name(self): 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" + 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 = GetUrl.get_url_parts_from_custom_view_url(custom_view_url, + None) + 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): + GetUrl.get_url_parts_from_custom_view_url(custom_view_url, mock_logger) + + 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): + GetUrl.get_url_parts_from_custom_view_url(custom_view_url, mock_logger) + + 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 = GetUrl.get_url_parts_from_custom_view_url(custom_view_url, + None) + 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 = GetUrl.get_url_parts_from_custom_view_url(custom_view_url, + None) + 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 = GetUrl.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 = GetUrl.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): + GetUrl.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): + GetUrl.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 = GetUrl.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 = GetUrl.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 = GetUrl.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 = GetUrl.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) GetUrl.get_view_without_extension(view_name) @@ -109,23 +217,76 @@ 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 = ExportCommand.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 = ExportCommand.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 = ExportCommand.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 = ExportCommand.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 = ExportCommand.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): + ExportCommand.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 = ExportCommand.get_export_item_and_server_content_type( + 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 = ExportCommand.get_export_item_and_server_content_type( + 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): From 98c8b985d3292056eea2782a880034cba3d54fed Mon Sep 17 00:00:00 2001 From: Renoy John Date: Wed, 13 Nov 2024 10:17:24 -0800 Subject: [PATCH 3/8] updating the minimum python version for github actions to 3.9 --- .github/workflows/generate-metadata.yml | 2 +- .github/workflows/package.yml | 2 +- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/run-tests.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) 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..8adc9607 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -58,7 +58,7 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: 3.9 - name: Install dependencies and build run: | 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-tests.yml b/.github/workflows/run-tests.yml index 06e4d87f..1364d47a 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'] runs-on: ${{ matrix.os }} From 44ff8d8cf570bb27c23cf7a05f5c9e70e843118f Mon Sep 17 00:00:00 2001 From: Renoy John Date: Wed, 13 Nov 2024 11:30:33 -0800 Subject: [PATCH 4/8] Fixing an ERROR with mypy run --- tabcmd/commands/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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, ) From 466bf67442f46dc4c57e70ae3d72c20c7d080853 Mon Sep 17 00:00:00 2001 From: Renoy John Date: Wed, 13 Nov 2024 11:39:52 -0800 Subject: [PATCH 5/8] Formatting changes with black --- .../datasources_and_workbooks_command.py | 2 +- .../export_command.py | 13 ++++++--- .../get_url_command.py | 17 +++++++----- .../test_datasources_and_workbooks_command.py | 2 +- tests/commands/test_geturl_utils.py | 27 ++++++++++--------- 5 files changed, 37 insertions(+), 24 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 f125a33e..21acfc3a 100644 --- a/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py +++ b/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py @@ -163,4 +163,4 @@ def get_custom_view_by_id(logger, server, custom_view_id) -> TSC.CustomViewItem: 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 \ No newline at end of file + return matching_custom_view diff --git a/tabcmd/commands/datasources_and_workbooks/export_command.py b/tabcmd/commands/datasources_and_workbooks/export_command.py index f4c251be..787a16a8 100644 --- a/tabcmd/commands/datasources_and_workbooks/export_command.py +++ b/tabcmd/commands/datasources_and_workbooks/export_command.py @@ -78,8 +78,12 @@ def run_command(args): logger.debug(_("tabcmd.launching")) session = Session() server = session.create_session(args, logger) - view_content_url, wb_content_url, custom_view_id, custom_view_name = ( - ExportCommand.parse_export_url_to_workbook_view_and_custom_view(logger, args.url)) + ( + view_content_url, + wb_content_url, + custom_view_id, + custom_view_name, + ) = ExportCommand.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" @@ -97,7 +101,8 @@ def run_command(args): elif args.pdf or args.png or args.csv: # it's a view or custom_view export_item, server_content_type = ExportCommand.get_export_item_and_server_content_type( - view_content_url, logger, server, custom_view_id) + view_content_url, logger, server, custom_view_id + ) if args.pdf: output = ExportCommand.download_view_pdf(server_content_type, export_item, args, logger) @@ -222,4 +227,4 @@ 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")) \ No newline at end of file + Errors.exit_with_error(logger, _("export.errors.requires_valid_custom_view_uuid")) diff --git a/tabcmd/commands/datasources_and_workbooks/get_url_command.py b/tabcmd/commands/datasources_and_workbooks/get_url_command.py index a2ce817b..2ba88ba9 100644 --- a/tabcmd/commands/datasources_and_workbooks/get_url_command.py +++ b/tabcmd/commands/datasources_and_workbooks/get_url_command.py @@ -73,8 +73,9 @@ def explain_expected_url(logger, url: str, command: str): ds_example = "/datasources/ Date: Mon, 18 Nov 2024 21:58:38 -0800 Subject: [PATCH 6/8] Addressing code review comments --- .github/workflows/run-e2-tests.yml | 2 +- .../datasources_and_workbooks_command.py | 1 - .../datasources_workbooks_views_url_parser.py | 191 ++++++++++++++++++ .../export_command.py | 56 +---- .../get_url_command.py | 155 ++------------ .../locales/de/tabcmd_messages_de.properties | 1 + .../locales/en/tabcmd_messages_en.properties | 2 +- .../locales/es/tabcmd_messages_es.properties | 1 + .../locales/fr/tabcmd_messages_fr.properties | 1 + .../locales/ga/tabcmd_messages_ga.properties | 1 + .../locales/it/tabcmd_messages_it.properties | 1 + .../locales/ja/tabcmd_messages_ja.properties | 1 + .../locales/ko/tabcmd_messages_ko.properties | 1 + .../locales/pt/tabcmd_messages_pt.properties | 1 + .../locales/sv/tabcmd_messages_sv.properties | 1 + .../locales/zh/tabcmd_messages_zh.properties | 1 + tests/commands/test_geturl_utils.py | 85 ++++---- 17 files changed, 268 insertions(+), 234 deletions(-) create mode 100644 tabcmd/commands/datasources_and_workbooks/datasources_workbooks_views_url_parser.py 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/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py b/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py index 21acfc3a..5a9cfe17 100644 --- a/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py +++ b/tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py @@ -1,7 +1,6 @@ import urllib import tableauserverclient as TSC -from tableauserverclient import CustomViewItem from tabcmd.commands.constants import Errors from tabcmd.commands.server import Server 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..eabdafa0 --- /dev/null +++ b/tabcmd/commands/datasources_and_workbooks/datasources_workbooks_views_url_parser.py @@ -0,0 +1,191 @@ +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")) \ No newline at end of file diff --git a/tabcmd/commands/datasources_and_workbooks/export_command.py b/tabcmd/commands/datasources_and_workbooks/export_command.py index 787a16a8..b1c551c4 100644 --- a/tabcmd/commands/datasources_and_workbooks/export_command.py +++ b/tabcmd/commands/datasources_and_workbooks/export_command.py @@ -7,6 +7,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 pagesize = TSC.PDFRequestOptions.PageType # type alias for brevity @@ -83,7 +84,7 @@ def run_command(args): wb_content_url, custom_view_id, custom_view_name, - ) = ExportCommand.parse_export_url_to_workbook_view_and_custom_view(logger, args.url) + ) = 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" @@ -100,9 +101,10 @@ def run_command(args): default_filename = "{}.pdf".format(workbook_item.name) elif args.pdf or args.png or args.csv: # it's a view or custom_view - export_item, server_content_type = ExportCommand.get_export_item_and_server_content_type( + 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_content_type, export_item, args, logger) @@ -128,20 +130,6 @@ def run_command(args): except Exception as e: Errors.exit_with_error(logger, "Error saving to file", e) - @staticmethod - def get_export_item_and_server_content_type(view_content_url, logger, server, custom_view_id): - export_item = ExportCommand.get_view_by_content_url(logger, server, view_content_url) - server_content_type = server.views - - if custom_view_id: - custom_view_item = ExportCommand.get_custom_view_by_id(logger, server, custom_view_id) - if custom_view_item.view.id != export_item.id: - Errors.exit_with_error(logger, "Invalid custom view URL provided") - server_content_type = server.custom_views - export_item = custom_view_item - - return export_item, server_content_type - @staticmethod def apply_filters_from_args(request_options: TSC.PDFRequestOptions, args, logger=None) -> None: if args.filter: @@ -194,37 +182,3 @@ def download_png(server_content_type, export_item, args, logger): logger.debug(image_options.get_query_params()) server_content_type.populate_image(export_item, image_options) return export_item.image - - @staticmethod - def parse_export_url_to_workbook_view_and_custom_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 - # or workbook_name/view_name/custom_view_id/custom_view_name - url = url.lstrip("/") # strip opening / if present - if not url.find("/"): - return None, None, None, None - name_parts = url.split("/") - if len(name_parts) == 2: - workbook = name_parts[0] - view = "{}/sheets/{}".format(workbook, name_parts[1]) - return view, workbook, None, None - elif len(name_parts) == 4: - workbook = name_parts[0] - view = "{}/sheets/{}".format(workbook, name_parts[1]) - custom_view_id = name_parts[2] - ExportCommand.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 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/get_url_command.py b/tabcmd/commands/datasources_and_workbooks/get_url_command.py index 2ba88ba9..2e4b75d3 100644 --- a/tabcmd/commands/datasources_and_workbooks/get_url_command.py +++ b/tabcmd/commands/datasources_and_workbooks/get_url_command.py @@ -1,5 +1,4 @@ import inspect -import os import tableauserverclient as TSC @@ -9,8 +8,7 @@ from tabcmd.execution.localize import _ from tabcmd.execution.logger_config import log from .datasources_and_workbooks_command import DatasourcesAndWorkbooks -from .export_command import ExportCommand - +from .datasources_workbooks_views_url_parser import DatasourcesWorkbooksAndViewsUrlParser class GetUrl(DatasourcesAndWorkbooks): """ @@ -48,7 +46,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,100 +63,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]" - 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: - 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 get_url_parts_from_custom_view_url(url, logger): - name_parts = url.split("/") # ['views', 'wb-name', 'view-name', 'custom-view-id', 'custom-view-name'] - if len(name_parts) != 5: - GetUrl.explain_expected_url(logger, url, "GetUrl") - workbook_name = name_parts[1] - view_name = name_parts[2] - custom_view_id = name_parts[3] - ExportCommand.verify_valid_custom_view_id(logger, custom_view_id) - custom_view_name = name_parts[::-1][0] - custom_view_name = GetUrl.strip_query_params(custom_view_name) - custom_view_name = GetUrl.get_name_without_possible_extension(custom_view_name) - return ( - DatasourcesAndWorkbooks.get_view_url_from_names(workbook_name, view_name), - custom_view_id, - custom_view_name, - ) - @staticmethod def filename_from_args(file_argument, item_name, filetype): if file_argument is None: @@ -178,7 +82,8 @@ 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": - get_url_item, server_content_type = GetUrl.get_url_item_and_item_type_from_view_url(logger, url, server) + 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_content_type, args, get_url_item) @@ -231,16 +136,17 @@ def generate_csv(logger, server_content_type, args, get_url_item): @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, include_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) @@ -248,54 +154,17 @@ 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, include_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) - - @staticmethod - def parse_get_view_url_to_view_and_custom_view_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] - # input should be views/workbook_name/view_name - # or views/workbook_name/view_name/custom_view_id/custom_view_name - url = url.lstrip("/") # strip opening / if present - if not url.find("/"): - GetUrl.explain_expected_url(logger, url, "GetUrl") - name_parts = url.split("/") - if len(name_parts) == 3: - return GetUrl.get_view_url(url, logger), None, None - elif len(name_parts) == 5: - return GetUrl.get_url_parts_from_custom_view_url(url, logger) - else: - GetUrl.explain_expected_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 = GetUrl.parse_get_view_url_to_view_and_custom_view_parts( - logger, url - ) - - get_url_item = GetUrl.get_view_by_content_url(logger, server, view_url) - get_url_item_type = server.views - - if custom_view_id: - custom_view_item = GetUrl.get_custom_view_by_id(logger, server, custom_view_id) - if custom_view_item.view.id != get_url_item.id: - Errors.exit_with_error(logger, "invalid custom view id provided") - get_url_item = custom_view_item - get_url_item_type = server.custom_views - - return get_url_item, get_url_item_type 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 e0d4fb51..ee0a1e1a 100644 --- a/tabcmd/locales/en/tabcmd_messages_en.properties +++ b/tabcmd/locales/en/tabcmd_messages_en.properties @@ -165,7 +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 luid +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_geturl_utils.py b/tests/commands/test_geturl_utils.py index 6b3f38ef..49fbd311 100644 --- a/tests/commands/test_geturl_utils.py +++ b/tests/commands/test_geturl_utils.py @@ -1,12 +1,12 @@ import unittest import uuid -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.datasources_and_workbooks.datasources_workbooks_views_url_parser import * from tabcmd.commands.server import Server mock_args = argparse.Namespace() @@ -45,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): @@ -76,28 +76,31 @@ 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 = GetUrl.get_url_parts_from_custom_view_url(custom_view_url, None) + 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" @@ -105,17 +108,18 @@ def test_get_url_parts_from_custom_view_url(self): 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): - GetUrl.get_url_parts_from_custom_view_url(custom_view_url, mock_logger) + 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): - GetUrl.get_url_parts_from_custom_view_url(custom_view_url, mock_logger) + 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 = GetUrl.get_url_parts_from_custom_view_url(custom_view_url, None) + 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" @@ -123,21 +127,24 @@ def test_get_url_parts_from_custom_view_url_with_url_params(self): 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 = GetUrl.get_url_parts_from_custom_view_url(custom_view_url, None) + 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 = GetUrl.parse_get_view_url_to_view_and_custom_view_parts(mock_logger, url) + 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 = GetUrl.parse_get_view_url_to_view_and_custom_view_parts(mock_logger, url) + 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 @@ -145,19 +152,18 @@ def test_parse_get_url_to_view_parts_with_params(self): def test_parse_get_url_to_view_parts_with_spaces(self): url = "views/wb name/view-name" with self.assertRaises(SystemExit): - GetUrl.parse_get_view_url_to_view_and_custom_view_parts(mock_logger, url) + 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): - GetUrl.parse_get_view_url_to_view_and_custom_view_parts(mock_logger, url) + 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 = GetUrl.parse_get_view_url_to_view_and_custom_view_parts( - mock_logger, custom_view_url - ) + 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" @@ -165,9 +171,9 @@ def test_parse_get_url_to_custom_view_parts(self): 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 = GetUrl.parse_get_view_url_to_view_and_custom_view_parts( - mock_logger, custom_view_url - ) + 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" @@ -177,7 +183,7 @@ 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 = GetUrl.get_url_item_and_item_type_from_view_url( + 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 @@ -190,7 +196,7 @@ def test_get_url_item_and_item_type_from_custom_view_url(self, mock_server): 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 = GetUrl.get_url_item_and_item_type_from_view_url( + 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 @@ -220,7 +226,8 @@ class ExportTests(unittest.TestCase): def test_parse_export_url_to_workbook_view_and_custom_view(self): wb_url = "wb-name/view-name" - view, wb, cv_id, cv_name = ExportCommand.parse_export_url_to_workbook_view_and_custom_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 @@ -228,7 +235,8 @@ def test_parse_export_url_to_workbook_view_and_custom_view(self): def test_parse_export_url_to_workbook_view_and_custom_view_with_start_slash(self): wb_url = "/wb-name/view-name" - view, wb, cv_id, cv_name = ExportCommand.parse_export_url_to_workbook_view_and_custom_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 @@ -236,7 +244,8 @@ def test_parse_export_url_to_workbook_view_and_custom_view_with_start_slash(self def test_parse_export_url_to_workbook_view_and_custom_view_bad_url(self): wb_url = "wb-name/view-name/kitty" - view, wb, cv_id, cv_name = ExportCommand.parse_export_url_to_workbook_view_and_custom_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 @@ -246,7 +255,8 @@ 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 = ExportCommand.parse_export_url_to_workbook_view_and_custom_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 == cv_uuid @@ -256,7 +266,8 @@ def test_parse_export_url_to_workbook_view_and_custom_view_with_bad_cv_parts(sel 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 = ExportCommand.parse_export_url_to_workbook_view_and_custom_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 @@ -265,16 +276,16 @@ def test_parse_export_url_to_workbook_view_and_custom_view_with_bad_cv_parts(sel 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): - ExportCommand.parse_export_url_to_workbook_view_and_custom_view(mock_logger, wb_url) + 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 = ExportCommand.get_export_item_and_server_content_type( - view_url, mock_logger, mock_server, None - ) + 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 @@ -285,9 +296,9 @@ def test_get_export_item_and_item_type_for_custom_view(self, mock_server): 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 = ExportCommand.get_export_item_and_server_content_type( - view_url, mock_logger, mock_server, fake_cv_id - ) + 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 From 5aaca9e24aba4f947054f15838efe79fc489e48c Mon Sep 17 00:00:00 2001 From: Renoy John Date: Mon, 18 Nov 2024 22:01:53 -0800 Subject: [PATCH 7/8] fixing black formatting issue --- .../datasources_workbooks_views_url_parser.py | 19 ++- .../export_command.py | 8 +- .../get_url_command.py | 13 +- tests/commands/test_geturl_utils.py | 126 +++++++++++++----- 4 files changed, 118 insertions(+), 48 deletions(-) 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 index eabdafa0..e5d3e4db 100644 --- a/tabcmd/commands/datasources_and_workbooks/datasources_workbooks_views_url_parser.py +++ b/tabcmd/commands/datasources_and_workbooks/datasources_workbooks_views_url_parser.py @@ -7,6 +7,7 @@ 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 @@ -51,7 +52,8 @@ def validate_and_extract_url_parts(logger, url): @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) + logger, server, view_content_url, custom_view_id + ) ################### GetURL Methods ############################## @@ -118,7 +120,8 @@ def get_resource_name(url: str, logger): # workbooks/wb-name" -> "wb-name", dat 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) + resource_name_with_ext + ) return resource_name @staticmethod @@ -164,11 +167,15 @@ def parse_get_view_url_to_view_and_custom_view_parts(logger, url): @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)) + ( + 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) + 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): @@ -188,4 +195,4 @@ 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")) \ No newline at end of file + 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 b1c551c4..5781c137 100644 --- a/tabcmd/commands/datasources_and_workbooks/export_command.py +++ b/tabcmd/commands/datasources_and_workbooks/export_command.py @@ -101,10 +101,12 @@ def run_command(args): default_filename = "{}.pdf".format(workbook_item.name) 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( + ( + 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_content_type, export_item, args, logger) diff --git a/tabcmd/commands/datasources_and_workbooks/get_url_command.py b/tabcmd/commands/datasources_and_workbooks/get_url_command.py index 2e4b75d3..05e52a8f 100644 --- a/tabcmd/commands/datasources_and_workbooks/get_url_command.py +++ b/tabcmd/commands/datasources_and_workbooks/get_url_command.py @@ -10,6 +10,7 @@ from .datasources_and_workbooks_command import DatasourcesAndWorkbooks from .datasources_workbooks_views_url_parser import DatasourcesWorkbooksAndViewsUrlParser + class GetUrl(DatasourcesAndWorkbooks): """ This command gets the resource from Tableau Server that's represented @@ -82,8 +83,10 @@ 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": - get_url_item, server_content_type = ( - DatasourcesWorkbooksAndViewsUrlParser.get_url_item_and_item_type_from_view_url(logger, url, server)) + ( + 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_content_type, args, get_url_item) @@ -143,7 +146,8 @@ def generate_twb(logger, server, args, file_extension, url): 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 = DatasourcesWorkbooksAndViewsUrlParser.get_name_without_possible_extension( - file_name_with_path) + 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, include_extract=True) @@ -161,7 +165,8 @@ def generate_tds(logger, server, args, file_extension): 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 = DatasourcesWorkbooksAndViewsUrlParser.get_name_without_possible_extension( - file_name_with_path) + 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, include_extract=True) diff --git a/tests/commands/test_geturl_utils.py b/tests/commands/test_geturl_utils.py index 49fbd311..a7a4b2f2 100644 --- a/tests/commands/test_geturl_utils.py +++ b/tests/commands/test_geturl_utils.py @@ -89,18 +89,27 @@ def test_get_workbook_name(self): assert DatasourcesWorkbooksAndViewsUrlParser.get_resource_name("workbooks/wbname", mock_logger) == "wbname" def test_view_name(self): - assert DatasourcesWorkbooksAndViewsUrlParser.get_view_url_from_get_url(mock_logger, - "views/wb-name/view-name") == "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 DatasourcesWorkbooksAndViewsUrlParser.get_view_url_from_get_url(mock_logger, - "views/wb-name/view-name?:refresh=y") == "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)) + ( + 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" @@ -118,8 +127,11 @@ def test_get_url_parts_from_custom_view_url_bad_url(self): 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)) + ( + 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" @@ -127,24 +139,33 @@ def test_get_url_parts_from_custom_view_url_with_url_params(self): 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)) + ( + 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)) + ( + 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)) + ( + 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 @@ -162,8 +183,13 @@ def test_parse_get_url_to_view_parts_without_slashes(self): 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)) + ( + 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" @@ -171,9 +197,13 @@ def test_parse_get_url_to_custom_view_parts(self): 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)) + ( + 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" @@ -226,8 +256,12 @@ class ExportTests(unittest.TestCase): def test_parse_export_url_to_workbook_view_and_custom_view(self): wb_url = "wb-name/view-name" - view, wb, cv_id, cv_name = (DatasourcesWorkbooksAndViewsUrlParser. - parse_export_url_to_workbook_view_and_custom_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 @@ -235,8 +269,12 @@ def test_parse_export_url_to_workbook_view_and_custom_view(self): def test_parse_export_url_to_workbook_view_and_custom_view_with_start_slash(self): wb_url = "/wb-name/view-name" - view, wb, cv_id, cv_name = (DatasourcesWorkbooksAndViewsUrlParser. - parse_export_url_to_workbook_view_and_custom_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 @@ -244,8 +282,12 @@ def test_parse_export_url_to_workbook_view_and_custom_view_with_start_slash(self def test_parse_export_url_to_workbook_view_and_custom_view_bad_url(self): wb_url = "wb-name/view-name/kitty" - view, wb, cv_id, cv_name = (DatasourcesWorkbooksAndViewsUrlParser. - parse_export_url_to_workbook_view_and_custom_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 @@ -255,8 +297,12 @@ 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)) + ( + 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 @@ -266,8 +312,12 @@ def test_parse_export_url_to_workbook_view_and_custom_view_with_bad_cv_parts(sel 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)) + ( + 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 @@ -283,9 +333,12 @@ 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)) + ( + 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 @@ -296,9 +349,12 @@ def test_get_export_item_and_item_type_for_custom_view(self, mock_server): 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)) + ( + 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 From 00ae3e6ce904b173c7d29ecdda08a4c13297fc79 Mon Sep 17 00:00:00 2001 From: Renoy John Date: Mon, 18 Nov 2024 22:06:38 -0800 Subject: [PATCH 8/8] Adding more python versions to run-tests --- .github/workflows/run-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 1364d47a..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.9', '3.10', '3'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3'] runs-on: ${{ matrix.os }}