diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 34d84b67..5e1ffdc0 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -28,11 +28,21 @@ jobs: pyinstaller tabcmd-windows.spec --clean --noconfirm --distpath ./dist/windows OUT_FILE_NAME: tabcmd.exe ASSET_MIME: application/vnd.microsoft.portable-executable + - os: macos-13 + TARGET: macos + CMD_BUILD: > + pyinstaller tabcmd-mac.spec --clean --noconfirm --distpath ./dist/macos/ + BUNDLE_NAME: tabcmd.app + OUT_FILE_NAME: tabcmd.app + APP_BINARY_FILE_NAME: tabcmd + ASSET_MIME: application/zip - os: macos-latest TARGET: macos CMD_BUILD: > - pyinstaller tabcmd-mac.spec --clean --noconfirm --distpath ./dist/macos + pyinstaller tabcmd-mac.spec --clean --noconfirm --distpath ./dist/macos/ + BUNDLE_NAME: tabcmd_arm64.app OUT_FILE_NAME: tabcmd.app + APP_BINARY_FILE_NAME: tabcmd ASSET_MIME: application/zip - os: ubuntu-latest TARGET: ubuntu @@ -64,9 +74,30 @@ jobs: run: ${{matrix.CMD_BUILD}} - name: Validate package for ${{matrix.TARGET}} + if: matrix.TARGET != 'macos' run: ./dist/${{ matrix.TARGET }}/${{matrix.OUT_FILE_NAME}} - - uses: actions/upload-artifact@v4 + - name: Validate package for Mac + if: matrix.TARGET == 'macos' + run: ./dist/${{ matrix.TARGET }}/${{ matrix.OUT_FILE_NAME }}/Contents/MacOS/${{ matrix.APP_BINARY_FILE_NAME }} + + - name: Tar app bundle for Mac + if: matrix.TARGET == 'macos' + run: | + rm -f dist/${{ matrix.TARGET }}/${{ matrix.APP_BINARY_FILE_NAME }} + cd dist/${{ matrix.TARGET }} + tar -cvf ${{ matrix.BUNDLE_NAME }}.tar ${{ matrix.OUT_FILE_NAME }} + + - name: Upload artifact + if: matrix.TARGET != 'macos' + uses: actions/upload-artifact@v4 with: name: ${{ matrix.OUT_FILE_NAME }} path: ./dist/${{ matrix.TARGET }}/${{ matrix.OUT_FILE_NAME }} + + - name: Upload artifact for Mac + if: matrix.TARGET == 'macos' + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.BUNDLE_NAME }} + path: ./dist/${{ matrix.TARGET }}/${{ matrix.BUNDLE_NAME }}.tar diff --git a/.gitignore b/.gitignore index adb4842c..eae72589 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ # general dev/ide files *.log *.vscode -*.csv .idea *.DS_Store @@ -24,19 +23,20 @@ workon/ .coverage pytest.xml +*.txt # content # todo probably want to add a workbook and ds in /res for getting started easily *.pdf *.png -*.twbx -*.hyper -*.twb -*.twbr -html *.html -*.twbr +# *.twbx +# *.hyper +# *.twb +# *.twbr +# *.tdsx **/credentials.py -*.txt +# exceptions +!tests/assets/ # doit .doit.* @@ -48,7 +48,3 @@ site-packages tabcmd-dev workon test.junit.xml - -# exceptions -!tests/assets/detailed_users.csv -est diff --git a/World Indicators.tdsx b/World Indicators.tdsx deleted file mode 100644 index 7c8e53db..00000000 Binary files a/World Indicators.tdsx and /dev/null differ diff --git a/WorldIndicators.tdsx b/WorldIndicators.tdsx deleted file mode 100644 index 2df960cc..00000000 Binary files a/WorldIndicators.tdsx and /dev/null differ diff --git a/pyproject.toml b/pyproject.toml index f5efb03c..00a6ed92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ packages = ["tabcmd"] tabcmd = ["tabcmd.locales/**/*.mo"] [tool.black] line-length = 120 +required-version = 22 target-version = ['py38', 'py39', 'py310', 'py311'] extend-exclude = '^/bin/*' [tool.mypy] @@ -40,7 +41,6 @@ classifiers = [ "Programming Language :: Python :: 3.11" ] dependencies = [ - 'argparse', "appdirs", "doit", "ftfy", @@ -52,8 +52,8 @@ dependencies = [ "types-mock", "types-requests", "types-setuptools", - "tableauserverclient==0.25", - "urllib3>=1.24.3,<2.0", + "tableauserverclient==0.31", + "urllib3", ] [project.optional-dependencies] test = [ diff --git a/tabcmd-mac.spec b/tabcmd-mac.spec index 8d50c283..dc8acdf2 100644 --- a/tabcmd-mac.spec +++ b/tabcmd-mac.spec @@ -32,7 +32,7 @@ exe = EXE( a.zipfiles, a.datas, [], - name='tabcmd.app', + name='tabcmd', debug=False, bootloader_ignore_signals=False, strip=False, @@ -40,12 +40,12 @@ exe = EXE( runtime_tmpdir=None, console=True, codesign_identity=None, - version='program_metadata.txt', + version='program_metadata.txt' ) app = BUNDLE( exe, - name = 'tabcmd-app', + name = 'tabcmd.app', icon='res/tabcmd.icns', bundle_identifier = None, ) diff --git a/tabcmd/commands/datasources_and_workbooks/get_url_command.py b/tabcmd/commands/datasources_and_workbooks/get_url_command.py index 38518bd8..8eae77bc 100644 --- a/tabcmd/commands/datasources_and_workbooks/get_url_command.py +++ b/tabcmd/commands/datasources_and_workbooks/get_url_command.py @@ -1,4 +1,5 @@ import inspect +import os import tableauserverclient as TSC from tableauserverclient import ServerResponseError @@ -80,7 +81,7 @@ def explain_expected_url(logger, url: str, command: str): def get_file_type_from_filename(logger, url, file_name): logger.debug("Choosing between {}, {}".format(file_name, url)) file_name = file_name or url - logger.debug(_("get.options.file") + ": {}".format(file_name)) + logger.debug(_("get.options.file") + ": {}".format(file_name)) # Name to save the file as type_of_file = GetUrl.get_file_extension(file_name) if not type_of_file and file_name is not None: @@ -98,25 +99,24 @@ def get_file_type_from_filename(logger, url, file_name): Errors.exit_with_error(logger, _("tabcmd.get.extension.not_found").format(file_name)) @staticmethod - def get_file_extension(filename): - parts = filename.split(".") - if len(parts) < 2: - return None - extension = parts[1] + def get_file_extension(path): + path_segments = os.path.split(path) + filename = path_segments[-1] + filename_segments = filename.split(".") + extension = filename_segments[-1] extension = GetUrl.strip_query_params(extension) return extension @staticmethod def strip_query_params(filename): - if filename.find("?") > 0: - filename = filename.split("?")[0] - return filename + if "?" in filename: + return filename.split("?")[0] + else: + return filename @staticmethod def get_name_without_possible_extension(filename): - if filename.find(".") > 0: - filename = filename.split(".")[0] - return filename + return filename.split(".")[0] @staticmethod def get_resource_name(url: str, logger): # workbooks/wb-name" -> "wb-name", datasource/ds-name -> ds-name @@ -181,7 +181,7 @@ def generate_pdf(logger, server, args, view_url): filename = GetUrl.filename_from_args(args.filename, view_item.name, "pdf") DatasourcesAndWorkbooks.save_to_file(logger, view_item.pdf, filename) except Exception as e: - Errors.exit_with_error(logger, e) + Errors.exit_with_error(logger, exception=e) @staticmethod def generate_png(logger, server, args, view_url): @@ -195,7 +195,7 @@ def generate_png(logger, server, args, view_url): filename = GetUrl.filename_from_args(args.filename, view_item.name, "png") DatasourcesAndWorkbooks.save_to_file(logger, view_item.image, filename) except Exception as e: - Errors.exit_with_error(logger, e) + Errors.exit_with_error(logger, exception=e) @staticmethod def generate_csv(logger, server, args, view_url): @@ -226,7 +226,7 @@ def generate_twb(logger, server, args, file_extension, url): server.workbooks.download(target_workbook.id, filepath=file_name_with_path, no_extract=False) logger.info(_("export.success").format(target_workbook.name, file_name_with_ext)) except Exception as e: - Errors.exit_with_error(logger, e) + Errors.exit_with_error(logger, exception=e) @staticmethod def generate_tds(logger, server, args, file_extension): @@ -243,4 +243,4 @@ def generate_tds(logger, server, args, file_extension): server.datasources.download(target_datasource.id, filepath=file_name_with_path, no_extract=False) logger.info(_("export.success").format(target_datasource.name, file_name_with_ext)) except Exception as e: - Errors.exit_with_error(logger, e) + Errors.exit_with_error(logger, exception=e) diff --git a/tabcmd/commands/datasources_and_workbooks/publish_command.py b/tabcmd/commands/datasources_and_workbooks/publish_command.py index 64189e9e..a4ff8c26 100644 --- a/tabcmd/commands/datasources_and_workbooks/publish_command.py +++ b/tabcmd/commands/datasources_and_workbooks/publish_command.py @@ -40,19 +40,17 @@ def run_command(args): session = Session() server = session.create_session(args, logger) - if args.project_name: - try: - dest_project = Server.get_project_by_name_and_parent_path( - logger, server, args.project_name, args.parent_project_path - ) - project_id = dest_project.id - except Exception as exc: - logger.error(exc.__str__()) - Errors.exit_with_error(logger, _("publish.errors.server_resource_not_found"), exc) - else: - project_id = "" - args.project_name = "default" + if not args.project_name: + args.project_name = "Default" args.parent_project_path = "" + try: + dest_project = Server.get_project_by_name_and_parent_path( + logger, server, args.project_name, args.parent_project_path + ) + project_id = dest_project.id + except Exception as exc: + logger.error(exc.__str__()) + Errors.exit_with_error(logger, _("publish.errors.server_resource_not_found"), exc) publish_mode = PublishCommand.get_publish_mode(args, logger) @@ -82,7 +80,7 @@ def run_command(args): # args.thumbnail_group, connection_credentials=creds, as_job=False, - skip_connection_check=False, + skip_connection_check=args.skip_connection_check, ) except Exception as e: Errors.exit_with_error(logger, exception=e) diff --git a/tabcmd/commands/site/list_command.py b/tabcmd/commands/site/list_command.py index 76c67cbb..64e4fa48 100644 --- a/tabcmd/commands/site/list_command.py +++ b/tabcmd/commands/site/list_command.py @@ -18,7 +18,6 @@ class ListCommand(Server): "tabcmd_content_none": "No content found.", } - name: str = "list" description: str = "List content items of a specified type" diff --git a/tabcmd/execution/global_options.py b/tabcmd/execution/global_options.py index 695820db..6c972616 100644 --- a/tabcmd/execution/global_options.py +++ b/tabcmd/execution/global_options.py @@ -312,6 +312,11 @@ def set_publish_args(parser): help="Encrypt extracts in the workbook, datasource, or extract being published to the server. " "[N/a on Tableau Cloud: extract encryption is controlled by Site Admin]", ) + parser.add_argument( + "--skip-connection-check", + action="store_true", + help="Skip connection check: do not validate the workbook/datasource connection during publishing", + ) # These two only apply for a workbook, not a datasource thumbnails = parser.add_mutually_exclusive_group() diff --git a/tabcmd/execution/localize.py b/tabcmd/execution/localize.py index f38995cc..0d6a4b04 100644 --- a/tabcmd/execution/localize.py +++ b/tabcmd/execution/localize.py @@ -82,7 +82,13 @@ def _load_language(current_locale, domain, logger): def _get_default_locale(): + # c:\dev\tabcmd\tabcmd\execution\localize.py:85: DeprecationWarning 'locale.getdefaultlocale' is deprecated + # see test_localize for details + import logging + + logging.captureWarnings(True) current_locale, encoding = locale.getdefaultlocale() + logging.captureWarnings(False) current_locale = _validate_lang(current_locale) return current_locale diff --git a/tabcmd/execution/tabcmd_controller.py b/tabcmd/execution/tabcmd_controller.py index 1a4544d2..6e06dfe1 100644 --- a/tabcmd/execution/tabcmd_controller.py +++ b/tabcmd/execution/tabcmd_controller.py @@ -37,14 +37,16 @@ def run(parser, user_input=None): logger.debug(namespace) if namespace.language: set_client_locale(namespace.language, logger) - try: + func = namespace.func # if a subcommand was identified, call the function assigned to it # this is the functional equivalent of the call by reflection in the previous structure # https://stackoverflow.com/questions/49038616/argparse-subparsers-with-functions namespace.func.run_command(namespace) + except AttributeError: + parser.error("No command identified or too few arguments") except Exception as e: - # todo: use log_stack here for better presentation + # todo: use log_stack here for better presentation? logger.exception(e) # if no command was given, argparse will just not create the attribute sys.exit(2) diff --git a/tests/assets/EmbeddedCredentials.twb b/tests/assets/EmbeddedCredentials.twb new file mode 100644 index 00000000..196f0924 --- /dev/null +++ b/tests/assets/EmbeddedCredentials.twb @@ -0,0 +1,977 @@ + + + + + + + + + + + + + ID + 3 + [ID] + [DBZ] + ID + 1 + integer + Sum + 10 + false + + "SQL_INTEGER" + "SQL_C_SLONG" + + + + Vendor + 130 + [Vendor] + [DBZ] + Vendor + 2 + string + Count + 50 + true + true + + + "SQL_WVARCHAR" + "SQL_C_WCHAR" + "true" + + + + Database + 130 + [Database] + [DBZ] + Database + 3 + string + Count + 50 + true + true + + + "SQL_WVARCHAR" + "SQL_C_WCHAR" + "true" + + + + TDCName + 130 + [TDCName] + [DBZ] + TDCName + 4 + string + Count + 50 + true + true + + + "SQL_WVARCHAR" + "SQL_C_WCHAR" + "true" + + + + Release + 130 + [Release] + [DBZ] + Release + 5 + string + Count + 50 + true + true + + + "SQL_WVARCHAR" + "SQL_C_WCHAR" + "true" + + + + Release Notes + 130 + [Release Notes] + [DBZ] + Release Notes + 6 + string + Count + 255 + true + true + + + "SQL_WVARCHAR" + "SQL_C_WCHAR" + "true" + + + + Status + 130 + [Status] + [DBZ] + Status + 7 + string + Count + 50 + true + true + + + "SQL_WVARCHAR" + "SQL_C_WCHAR" + "true" + + + + Machine Type + 130 + [Machine Type] + [DBZ] + Machine Type + 8 + string + Count + 50 + true + true + + + "SQL_WVARCHAR" + "SQL_C_WCHAR" + "true" + + + + CName + 130 + [CName] + [DBZ] + CName + 9 + string + Count + 255 + true + true + + + "SQL_WVARCHAR" + "SQL_C_WCHAR" + "true" + + + + database/schema + 130 + [database/schema] + [DBZ] + database/schema + 10 + string + Count + 50 + true + true + + + "SQL_WVARCHAR" + "SQL_C_WCHAR" + "true" + + + + username + 130 + [username] + [DBZ] + username + 11 + string + Count + 50 + true + true + + + "SQL_WVARCHAR" + "SQL_C_WCHAR" + "true" + + + + password + 130 + [password] + [DBZ] + password + 12 + string + Count + 50 + true + true + + + "SQL_WVARCHAR" + "SQL_C_WCHAR" + "true" + + + + Port/Service + 130 + [Port/Service] + [DBZ] + Port/Service + 13 + string + Count + 50 + true + true + + + "SQL_WVARCHAR" + "SQL_C_WCHAR" + "true" + + + + AD auth + 11 + [AD auth] + [DBZ] + AD auth + 14 + boolean + Count + true + + "SQL_BIT" + "SQL_C_BIT" + + + + LDAP Auth + 11 + [LDAP Auth] + [DBZ] + LDAP Auth + 15 + boolean + Count + true + + "SQL_BIT" + "SQL_C_BIT" + + + + DBZ Host + 130 + [DBZ Host] + [DBZ] + DBZ Host + 16 + string + Count + 25 + true + true + + + "SQL_WVARCHAR" + "SQL_C_WCHAR" + "true" + + + + DBZ Node + 130 + [DBZ Node] + [DBZ] + DBZ Node + 17 + string + Count + 255 + true + true + + + "SQL_WVARCHAR" + "SQL_C_WCHAR" + "true" + + + + DBZ Internal IP + 130 + [DBZ Internal IP] + [DBZ] + DBZ Internal IP + 18 + string + Count + 15 + true + true + + + "SQL_WCHAR" + "SQL_C_WCHAR" + + + + GOLD IP + 130 + [GOLD IP] + [DBZ] + GOLD IP + 19 + string + Count + 15 + true + true + + + "SQL_WCHAR" + "SQL_C_WCHAR" + + + + LIVE IP + 130 + [LIVE IP] + [DBZ] + LIVE IP + 20 + string + Count + 15 + true + true + + + "SQL_WCHAR" + "SQL_C_WCHAR" + + + + TEST IP + 130 + [TEST IP] + [DBZ] + TEST IP + 21 + string + Count + 15 + true + true + + + "SQL_WCHAR" + "SQL_C_WCHAR" + + + + dvah IP + 130 + [dvah IP] + [DBZ] + dvah IP + 22 + string + Count + 15 + true + true + + + "SQL_WCHAR" + "SQL_C_WCHAR" + + + + tsah IP + 130 + [tsah IP] + [DBZ] + tsah IP + 23 + string + Count + 15 + true + true + + + "SQL_WCHAR" + "SQL_C_WCHAR" + + + + IP Address Locked + 130 + [IP Address Locked] + [DBZ] + IP Address Locked + 24 + string + Count + 50 + true + true + + + "SQL_WVARCHAR" + "SQL_C_WCHAR" + "true" + + + + Node Name Locked + 130 + [Node Name Locked] + [DBZ] + Node Name Locked + 25 + string + Count + 50 + true + true + + + "SQL_WVARCHAR" + "SQL_C_WCHAR" + "true" + + + + MAC Address Locked + 130 + [MAC Address Locked] + [DBZ] + MAC Address Locked + 26 + string + Count + 50 + true + true + + + "SQL_WVARCHAR" + "SQL_C_WCHAR" + "true" + + + + FirstBuiltDate + 7 + [FirstBuiltDate] + [DBZ] + FirstBuiltDate + 27 + date + Year + true + + "SQL_TYPE_DATE" + "SQL_C_TYPE_DATE" + true + + + + FirstBlessDate + 7 + [FirstBlessDate] + [DBZ] + FirstBlessDate + 28 + date + Year + true + + "SQL_TYPE_DATE" + "SQL_C_TYPE_DATE" + true + + + + ExternalDocURL + 130 + [ExternalDocURL] + [DBZ] + ExternalDocURL + 29 + string + Count + 250 + true + true + + + "SQL_WVARCHAR" + "SQL_C_WCHAR" + "true" + + + + InternalDocURL + 130 + [InternalDocURL] + [DBZ] + InternalDocURL + 30 + string + Count + 250 + true + true + + + "SQL_WVARCHAR" + "SQL_C_WCHAR" + "true" + + + + DriverInfo + 129 + [DriverInfo] + [DBZ] + DriverInfo + 31 + string + Count + 250 + true + true + + + "SQL_VARCHAR" + "SQL_C_CHAR" + "true" + + + + Migration Group + 130 + [Migration Group] + [DBZ] + Migration Group + 32 + string + Count + 50 + true + true + + + "SQL_WVARCHAR" + "SQL_C_WCHAR" + "true" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ([sqlserver.42493.565638564818].[none:LIVE IP:nk] / [sqlserver.42493.565638564818].[none:Status:nk]) + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + [sqlserver.42493.565638564818].[none:Status:nk] + + + + + + + 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 + + +
diff --git a/tests/assets/SampleDS.tds b/tests/assets/SampleDS.tds index 40b850ac..502e6a22 100644 --- a/tests/assets/SampleDS.tds +++ b/tests/assets/SampleDS.tds @@ -1,6 +1,6 @@ - + diff --git a/tests/assets/WorkbookWithExtract.twbx b/tests/assets/WorkbookWithExtract.twbx new file mode 100644 index 00000000..b6025b36 Binary files /dev/null and b/tests/assets/WorkbookWithExtract.twbx differ diff --git a/tests/assets/WorkbookWithoutExtract.twbx b/tests/assets/WorkbookWithoutExtract.twbx new file mode 100644 index 00000000..49fc684a Binary files /dev/null and b/tests/assets/WorkbookWithoutExtract.twbx differ diff --git a/tests/assets/simple-data.twbx b/tests/assets/simple-data.twbx new file mode 100644 index 00000000..bf8ad6df Binary files /dev/null and b/tests/assets/simple-data.twbx differ diff --git a/tests/assets/usernames.csv b/tests/assets/usernames.csv new file mode 100644 index 00000000..b300c1c1 --- /dev/null +++ b/tests/assets/usernames.csv @@ -0,0 +1 @@ +anyoung@salesforce.com \ No newline at end of file diff --git a/tests/commands/test_execution.py b/tests/commands/test_execution.py index 364ce456..b31e8056 100644 --- a/tests/commands/test_execution.py +++ b/tests/commands/test_execution.py @@ -1,7 +1,6 @@ import argparse -import sys import unittest -import mock +import unittest.mock as mock from tabcmd.execution.logger_config import * from tabcmd.execution.tabcmd_controller import TabcmdController diff --git a/tests/commands/test_geturl_utils.py b/tests/commands/test_geturl_utils.py index 32497be6..ec63e4df 100644 --- a/tests/commands/test_geturl_utils.py +++ b/tests/commands/test_geturl_utils.py @@ -24,7 +24,23 @@ fake_item.id = "fake-id" -class GeturlTests(unittest.TestCase): +class FileHandling(unittest.TestCase): + + # get_file_type_from_filename(logger, url, filename) + # get_file_extension(filepath) + # evaluate_content_type(logger, url) + # strip_query_params(filename) + # get_name_without_possible_extension(fileanme) + # get_resource_name(url, logger) + # get_view_url(url, logger) + # filename_from_args(file_arg, item_name, filetype) + + 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) + assert filetype == "twbx", filetype + def test_evaluate_file_name_pdf(self): filename = "filename.pdf" url = None @@ -59,6 +75,9 @@ def test_get_view_without_extension_that_doesnt_have_one(self): filename = "viewname" assert GetUrl.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" @@ -69,9 +88,12 @@ def test_view_name_with_url_params(self): assert GetUrl.get_view_url("views/wb-name/view-name?:refresh=y", None) == "wb-name/sheets/view-name" """ - GetUrl.get_view_without_extension(view_name) GetUrl.get_view(url) + GetUrl.get_view_without_extension(view_name) GetUrl.get_workbook(url) + """ + + """ GetUrl.generate_twb(logger, server, args) GetUrl.generate_pdf(logger, server, args) GetUrl.generate_png(logger, server, args) diff --git a/tests/commands/test_localize.py b/tests/commands/test_localize.py index 90bb4851..2ae38ba2 100644 --- a/tests/commands/test_localize.py +++ b/tests/commands/test_localize.py @@ -1,6 +1,8 @@ import gettext +import locale +import sys import unittest -from tabcmd.execution.localize import set_client_locale +from tabcmd.execution.localize import set_client_locale, _get_default_locale class LocaleTests(unittest.TestCase): @@ -44,3 +46,19 @@ def test_en_smoke_line_processed(self): assert translations is not None assert translations("importcsvsummary.line.processed") == "Lines processed: {0}" + + # https://docs.python.org/3/library/locale.html + # c:\dev\tabcmd\tabcmd\execution\localize.py:85: DeprecationWarning: + # 'locale.getdefaultlocale' is deprecated and slated for removal in Python 3.15. Use setlocale(), getencoding() and getlocale() instead + def test_get_default_locale(self): + # Our method in localize.py that needs to change. An eventual unit test should call this method. + # loc = _get_default_locale() # doesn't return anything: need to mock _validate_lang + + # This bug on pytest explains why the proposed replacements aren't directly equivalent. + # Some people online have solved this with manual string mangling. I like the pytest decision + # to wait until we hit 3.15 and hope someone has implemented a better option by then. + # current call that is deprecated -->loc = locale.getdefaultlocale() # returns ('en_US', 'cp1252') + # sys.getdefaultencoding() # returns utf-8 + # locale.getlocale() # returns ('English_United States', '1252') + new_locale = locale.setlocale(locale.LC_CTYPE, None) # returns English_United States.125 + # assert loc == new_locale diff --git a/tests/commands/test_publish_command.py b/tests/commands/test_publish_command.py new file mode 100644 index 00000000..0c9049a7 --- /dev/null +++ b/tests/commands/test_publish_command.py @@ -0,0 +1,92 @@ +import argparse +import unittest +from unittest.mock import * +import tableauserverclient as TSC + +from tabcmd.commands.auth import login_command +from tabcmd.commands.datasources_and_workbooks import delete_command, export_command, get_url_command, publish_command + + +from typing import List, NamedTuple, TextIO, Union +import io + +mock_args = argparse.Namespace() + +fake_item = MagicMock() +fake_item.name = "fake-name" +fake_item.id = "fake-id" +fake_item.pdf = b"/pdf-representation-of-view" +fake_item.extract_encryption_mode = "Disabled" + +fake_job = MagicMock() +fake_job.id = "fake-job-id" + +creator = MagicMock() +getter = MagicMock() +getter.get = MagicMock("get", return_value=([fake_item], 1)) +getter.publish = MagicMock("publish", return_value=fake_item) + + +@patch("tableauserverclient.Server") +@patch("tabcmd.commands.auth.session.Session.create_session") +class RunCommandsTest(unittest.TestCase): + @staticmethod + def _set_up_session(mock_session, mock_server): + mock_session.return_value = mock_server + assert mock_session is not None + mock_session.assert_not_called() + global mock_args + mock_args = argparse.Namespace(logging_level="DEBUG") + # set values for things that should always have a default + # should refactor so this can be automated + mock_args.continue_if_exists = False + mock_args.project_name = None + mock_args.parent_project_path = None + mock_args.parent_path = None + mock_args.timeout = None + mock_args.username = None + + def test_publish(self, mock_session, mock_server): + RunCommandsTest._set_up_session(mock_session, mock_server) + mock_args.overwrite = False + mock_args.filename = "existing_file.twbx" + mock_args.project_name = "project-name" + mock_args.parent_project_path = "projects" + mock_args.name = "" + mock_args.tabbed = True + mock_args.db_username = None + mock_args.oauth_username = None + mock_args.append = False + mock_args.replace = False + mock_args.thumbnail_username = None + mock_args.thumbnail_group = None + mock_args.skip_connection_check = False + mock_server.projects = getter + publish_command.PublishCommand.run_command(mock_args) + mock_session.assert_called() + + def test_publish_with_creds(self, mock_session, mock_server): + RunCommandsTest._set_up_session(mock_session, mock_server) + mock_args.overwrite = False + mock_args.append = True + mock_args.replace = False + + mock_args.filename = "existing_file.twbx" + mock_args.project_name = "project-name" + mock_args.parent_project_path = "projects" + mock_args.name = "" + mock_args.tabbed = True + + mock_args.db_username = "username" + mock_args.db_password = "oauth_u" + mock_args.save_db_password = True + mock_args.oauth_username = None + mock_args.embed = False + + mock_args.thumbnail_username = None + mock_args.thumbnail_group = None + mock_args.skip_connection_check = False + + mock_server.projects = getter + publish_command.PublishCommand.run_command(mock_args) + mock_session.assert_called() diff --git a/tests/commands/test_run_commands.py b/tests/commands/test_run_commands.py index dbb9bf55..c29341d0 100644 --- a/tests/commands/test_run_commands.py +++ b/tests/commands/test_run_commands.py @@ -4,13 +4,7 @@ import tableauserverclient as TSC from tabcmd.commands.auth import login_command, logout_command -from tabcmd.commands.datasources_and_workbooks import ( - delete_command, - export_command, - get_url_command, - publish_command, - runschedule_command, -) +from tabcmd.commands.datasources_and_workbooks import delete_command, export_command, get_url_command, publish_command from tabcmd.commands.extracts import ( create_extracts_command, delete_extracts_command, @@ -35,7 +29,7 @@ remove_users_command, delete_site_users_command, ) -from typing import List, NamedTuple, TextIO, Union +from typing import NamedTuple, TextIO, Union import io mock_args = argparse.Namespace() @@ -153,6 +147,7 @@ def test_publish(self, mock_session, mock_server): mock_args.replace = False mock_args.thumbnail_username = None mock_args.thumbnail_group = None + mock_args.skip_connection_check = False mock_server.projects = getter publish_command.PublishCommand.run_command(mock_args) mock_session.assert_called() diff --git a/tests/commands/test_session.py b/tests/commands/test_session.py index 44b20a8a..cb60260d 100644 --- a/tests/commands/test_session.py +++ b/tests/commands/test_session.py @@ -74,6 +74,7 @@ def _set_mocks_for_json_file_exists(mock_path, mock_json_lib, does_it_exist=True mock_json_lib.load.return_value = None return path + def _set_mock_file_content(mock_load, expected_content): mock_load.return_value = expected_content return mock_load @@ -120,6 +121,7 @@ def test_json_invalid(self, mock_open, mock_path, mock_json): test_session = Session() assert test_session.username is None + @mock.patch("getpass.getpass") class BuildCredentialsTests(unittest.TestCase): @classmethod diff --git a/tests/e2e/online_tests.py b/tests/e2e/online_tests.py index d0b402df..6b502280 100644 --- a/tests/e2e/online_tests.py +++ b/tests/e2e/online_tests.py @@ -38,7 +38,7 @@ site_admin = True project_admin = True extract_encryption_enabled = False -use_tabcmd_classic = False # toggle between testing using tabcmd 2 or tabcmd classic +use_tabcmd_classic = False # toggle between testing using tabcmd 2 or tabcmd classic def _test_command(test_args: list[str]): @@ -49,7 +49,11 @@ def _test_command(test_args: list[str]): # call the executable directly: lets us drop in classic tabcmd if use_tabcmd_classic: - calling_args = ["C:\\Program Files\\Tableau\\Tableau Server\\2023.3\\extras\\Command Line Utility\\tabcmd.exe"] + test_args + ["--no-certcheck"] + calling_args = ( + ["C:\\Program Files\\Tableau\\Tableau Server\\2023.3\\extras\\Command Line Utility\\tabcmd.exe"] + + test_args + + ["--no-certcheck"] + ) if database_password not in calling_args: print(calling_args) return subprocess.check_call(calling_args) @@ -89,7 +93,7 @@ def _publish_samples(self, project_name): arguments = [command, "--name", project_name] _test_command(arguments) - def _publish_args(self, file, name, tabbed=None): + def _publish_args(self, file, name): command = "publish" arguments = [command, file, "--name", name, "--overwrite"] return arguments @@ -197,11 +201,11 @@ def _list(self, item_type: str): TDSX_WITH_EXTRACT_NAME = "WorldIndicators" TDSX_FILE_WITH_EXTRACT = "World Indicators.tdsx" # fill in - TDS_FILE_LIVE_NAME = "" - TDS_FILE_LIVE = "" - # only works on linux servers or something - TWB_WITH_EMBEDDED_CONNECTION = "embedded_connection_waremart.twb" - EMBEDDED_TWB_NAME = "waremart" + TDS_FILE_LIVE_NAME = "SampleDS" + TDS_FILE_LIVE = "SampleDS.tds" + + TWB_WITH_EMBEDDED_CONNECTION = "EmbeddedCredentials.twb" + EMBEDDED_TWB_NAME = "EmbeddedCredentials" @pytest.mark.order(1) def test_login(self): @@ -312,7 +316,7 @@ def test_list_workbooks(self): self._list("workbooks") @pytest.mark.order(8) - def test_list_workbooks(self): + def test_list_datasources(self): if use_tabcmd_classic: pytest.skip("not for tabcmd classic") self._list("datasources") @@ -329,7 +333,10 @@ def test_delete_projects(self): def test_wb_publish(self): file = os.path.join("tests", "assets", OnlineCommandTest.TWBX_FILE_WITH_EXTRACT) arguments = self._publish_args(file, OnlineCommandTest.TWBX_WITH_EXTRACT_NAME) - _test_command(arguments) + val = _test_command(arguments) + if val != 0: + print("publishing failed: cancel test run") + exit(val) @pytest.mark.order(11) def test_wb_get(self): @@ -360,6 +367,8 @@ def test_wb_publish_embedded(self): file = os.path.join("tests", "assets", OnlineCommandTest.TWB_WITH_EMBEDDED_CONNECTION) arguments = self._publish_args(file, OnlineCommandTest.EMBEDDED_TWB_NAME) arguments = self._publish_creds_args(arguments, database_user, database_password, True) + arguments.append("--tabbed") + arguments.append("--skip-connection-check") _test_command(arguments) @pytest.mark.order(12) @@ -459,10 +468,10 @@ def test_list_sites(self): if not server_admin: pytest.skip("Must be server administrator to list sites") + result = True command = "listsites" try: _test_command([command]) except Exception as E: - print("yay") - result = True + result = False assert result diff --git a/tests/e2e/setup_e2e.py b/tests/e2e/setup_e2e.py index 29213fe7..7253fd6e 100644 --- a/tests/e2e/setup_e2e.py +++ b/tests/e2e/setup_e2e.py @@ -4,7 +4,7 @@ try: from tests.e2e import credentials # type: ignore except ImportError: - credentials = None # type: ignore + credentials = {} # type: ignore our_program = "tabcmd.exe" launch_path = os.path.join("dist", "tabcmd") @@ -13,7 +13,7 @@ def login(extra="--language", value="en"): if not credentials: - return + return # when run on github # --server, --site, --username, --password args = [ "python",