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",