Skip to content

Commit f7ce761

Browse files
authored
Development (#216)
fix handling of -f argument for exporting add --auth and --role args add --timeout arg
1 parent 91d89f1 commit f7ce761

File tree

20 files changed

+278
-126
lines changed

20 files changed

+278
-126
lines changed

.github/workflows/package.yml

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,23 +26,22 @@ jobs:
2626
TARGET: windows
2727
CMD_BUILD: >
2828
pyinstaller tabcmd-windows.spec --clean --noconfirm --distpath ./dist/windows
29-
OUT_FILE_NAME: tabcmd-windows.exe
29+
OUT_FILE_NAME: tabcmd.exe
3030
ASSET_MIME: application/vnd.microsoft.portable-executable
3131
- os: macos-latest
3232
TARGET: macos
3333
CMD_BUILD: >
34-
pyinstaller tabcmd-mac.spec --clean --noconfirm --distpath ./dist/macos && ls && ls dist
35-
# zip -r9 mac tabcmd-mac*
36-
OUT_FILE_NAME: tabcmd-mac.app # tabcmd.zip
34+
pyinstaller tabcmd-mac.spec --clean --noconfirm --distpath ./dist/macos
35+
OUT_FILE_NAME: tabcmd.app
3736
ASSET_MIME: application/zip
3837
- os: ubuntu-latest
3938
TARGET: ubuntu
4039
# https://stackoverflow.com/questions/31259856
4140
# /how-to-create-an-executable-file-for-linux-machine-using-pyinstaller
4241
CMD_BUILD: >
43-
pyinstaller --clean -y --distpath ./dist/linux tabcmd-linux.spec &&
44-
chown -R --reference=. ./dist/linux
45-
OUT_FILE_NAME: tab-for-linux
42+
pyinstaller --clean -y --distpath ./dist/ubuntu tabcmd-linux.spec &&
43+
chown -R --reference=. ./dist/ubuntu
44+
OUT_FILE_NAME: tabcmd
4645

4746
steps:
4847
- uses: actions/checkout@v3
@@ -64,12 +63,9 @@ jobs:
6463
- name: Package with pyinstaller for ${{matrix.TARGET}}
6564
run: ${{matrix.CMD_BUILD}}
6665

67-
- name: Upload assets to release
68-
uses: WebFreak001/upload-asset@v1.0.0
69-
env:
70-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # automatically provided by github actions
71-
OS: windows # a variable we use in the name pattern?
66+
- name: Validate package for ${{matrix.TARGET}}
67+
run: ./dist/${{ matrix.TARGET }}/${{matrix.OUT_FILE_NAME}}
68+
69+
- uses: actions/upload-artifact@v3
7270
with:
73-
file: ./dist/${{ matrix.TARGET }}/${{ matrix.OUT_FILE_NAME}}
74-
mime: ${{ matrix.ASSET_MIME}} # required by GitHub API
75-
name: ${{ matrix.OUT_FILE_NAME}} # name pattern to upload the file as
71+
path: ./dist/${{ matrix.TARGET }}/${{ matrix.OUT_FILE_NAME }}/

tabcmd-linux.spec

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ print(datas)
88
block_cipher = None
99

1010
a = Analysis(
11-
['tabcmd\\tabcmd.py'],
11+
['tabcmd.py'],
1212
pathex=[],
1313
binaries=[],
1414
datas=datas,
15-
hiddenimports=['tableauserverclient', 'requests.packages.urllib3', 'pkg_resources'],
15+
hiddenimports=['tableauserverclient', 'requests', 'urllib3', 'pkg_resources'],
1616
hookspath=[],
1717
hooksconfig={},
1818
runtime_hooks=[],
@@ -31,7 +31,7 @@ exe = EXE(
3131
a.zipfiles,
3232
a.datas,
3333
[],
34-
name='tabcmd-windows',
34+
name='tabcmd',
3535
debug=False,
3636
bootloader_ignore_signals=False,
3737
strip=False,

tabcmd-mac.spec

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ a = Analysis(
1313
pathex=[],
1414
binaries=[],
1515
datas=datas,
16-
hiddenimports=['tableauserverclient', 'requests.packages.urllib3', 'pkg_resources'],
16+
hiddenimports=['tableauserverclient', 'requests', 'urllib3', 'pkg_resources'],
1717
hookspath=[],
1818
hooksconfig={},
1919
runtime_hooks=[],
@@ -32,19 +32,20 @@ exe = EXE(
3232
a.zipfiles,
3333
a.datas,
3434
[],
35-
name='tabcmd-mac',
35+
name='tabcmd.app',
3636
debug=False,
3737
bootloader_ignore_signals=False,
3838
strip=False,
3939
upx=True,
40-
upx_exclude=[],
4140
runtime_tmpdir=None,
4241
console=True,
43-
disable_windowed_traceback=False,
44-
argv_emulation=False,
45-
target_arch=None,
4642
codesign_identity=None,
47-
entitlements_file=None,
4843
version='program_metadata.txt',
44+
)
45+
46+
app = BUNDLE(
47+
exe,
48+
name = 'tabcmd-app',
4949
icon='res/tabcmd.icns',
50+
bundle_identifier = None,
5051
)

tabcmd-windows.spec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ exe = EXE(
3131
a.zipfiles,
3232
a.datas,
3333
[],
34-
name='tabcmd-windows',
34+
name='tabcmd',
3535
debug=False,
3636
bootloader_ignore_signals=False,
3737
strip=False,

tabcmd/__main__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
try:
44
from tabcmd.tabcmd import main
5-
except ImportError:
5+
except ImportError as e:
6+
print(sys.stderr, e)
67
print(sys.stderr, "Tabcmd needs to be run as a module, it cannot be run as a script")
78
print(sys.stderr, "Try running python -m tabcmd")
89
sys.exit(1)

tabcmd/commands/auth/session.py

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import requests
66
import tableauserverclient as TSC
7+
import urllib3
78
from urllib3.exceptions import InsecureRequestWarning
89

910
from tabcmd.commands.constants import Errors
@@ -74,7 +75,26 @@ def _update_session_data(self, args):
7475
self.no_certcheck = args.no_certcheck or self.no_certcheck
7576
self.no_proxy = args.no_proxy or self.no_proxy
7677
self.proxy = args.proxy or self.proxy
77-
self.timeout = args.timeout or self.timeout
78+
self.timeout = self.timeout_as_integer(self.logger, args.timeout, self.timeout)
79+
80+
@staticmethod
81+
def timeout_as_integer(logger, option_1, option_2):
82+
result = None
83+
if option_1:
84+
try:
85+
result = int(option_1)
86+
except Exception as anyE:
87+
result = 0
88+
if option_2 and (not result or result <= 0):
89+
try:
90+
result = int(option_2)
91+
except Exception as anyE:
92+
result = 0
93+
if not option_1 and not option_2:
94+
logger.debug(_("setsetting.status").format("timeout", "None"))
95+
elif not result or result <= 0:
96+
logger.warning(_("sessionoptions.errors.bad_timeout").format("--timeout", result))
97+
return result or 0
7898

7999
@staticmethod
80100
def _read_password_from_file(filename):
@@ -108,7 +128,7 @@ def _create_new_credential(self, password, credential_type):
108128
credentials = self._create_new_token_credential()
109129
return credentials
110130
else:
111-
Errors.exit_with_error(self.logger, "Couldn't find credentials")
131+
Errors.exit_with_error(self.logger, _("session.errors.missing_arguments").format(""))
112132

113133
def _create_new_token_credential(self):
114134
if self.token_value:
@@ -127,31 +147,60 @@ def _create_new_token_credential(self):
127147
else:
128148
Errors.exit_with_error(self.logger, _("session.errors.missing_arguments").format("token name"))
129149

130-
def _set_connection_options(self):
150+
def _set_connection_options(self) -> TSC.Server:
151+
self.logger.debug("Setting up request options")
131152
# args still to be handled here:
132153
# proxy, --no-proxy,
133154
# cert
134-
# timeout
135155
http_options = {}
136156
if self.no_certcheck:
137-
http_options = {"verify": False}
138-
requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
157+
http_options["verify"] = False
158+
urllib3.disable_warnings(category=InsecureRequestWarning)
159+
if self.proxy:
160+
# do we catch this error? "sessionoptions.errors.bad_proxy_format"
161+
self.logger.debug("Setting proxy: ", self.proxy)
162+
if self.timeout:
163+
http_options["timeout"] = self.timeout
139164
try:
140-
tableau_server = TSC.Server(self.server_url, use_server_version=True, http_options=http_options)
165+
self.logger.debug(http_options)
166+
tableau_server = TSC.Server(self.server_url, http_options=http_options)
167+
141168
except Exception as e:
169+
self.logger.debug(
170+
"Connection args: server {}, site {}, proxy {}, cert {}".format(
171+
self.server_url, self.site_name, self.proxy, self.certificate
172+
)
173+
)
142174
Errors.exit_with_error(self.logger, "Failed to connect to server", e)
143175

176+
self.logger.debug("Finished setting up connection")
144177
return tableau_server
145178

146-
def _create_new_connection(self):
179+
def _verify_server_connection_unauthed(self):
180+
try:
181+
self.tableau_server.use_server_version()
182+
except requests.exceptions.ReadTimeout as timeout_error:
183+
Errors.exit_with_error(
184+
self.logger,
185+
message="Timed out after {} seconds attempting to connect to server".format(self.timeout),
186+
exception=timeout_error,
187+
)
188+
except requests.exceptions.RequestException as requests_error:
189+
Errors.exit_with_error(
190+
self.logger, message="Error attempting to connect to the server", exception=requests_error
191+
)
192+
except Exception as e:
193+
Errors.exit_with_error(self.logger, exception=e)
194+
195+
def _create_new_connection(self) -> TSC.Server:
147196
self.logger.info(_("session.new_session"))
148-
self.tableau_server = self._set_connection_options()
149197
self._print_server_info()
150198
self.logger.info(_("session.connecting"))
151199
try:
152-
self.tableau_server.use_server_version() # this will attempt to contact the server
200+
self.tableau_server = self._set_connection_options()
153201
except Exception as e:
154202
Errors.exit_with_error(self.logger, "Failed to connect to server", e)
203+
return self.tableau_server
155204

156205
def _read_existing_state(self):
157206
if self._check_json():
@@ -168,7 +217,7 @@ def _print_server_info(self):
168217

169218
def _validate_existing_signin(self):
170219
self.logger.info(_("session.continuing_session"))
171-
self.tableau_server = self._set_connection_options()
220+
# when do these two messages show up? self.logger.info(_("session.auto_site_login"))
172221
try:
173222
if self.tableau_server and self.tableau_server.is_signed_in():
174223
response = self.tableau_server.users.get_by_id(self.user_id)
@@ -181,7 +230,8 @@ def _validate_existing_signin(self):
181230
self.logger.info(_("errors.internal_error.request.message"), e)
182231
return None
183232

184-
def _sign_in(self, tableau_auth):
233+
# server connection created, not yet logged in
234+
def _sign_in(self, tableau_auth) -> TSC.Server:
185235
self.logger.debug(_("session.login") + self.server_url)
186236
self.logger.debug(_("listsites.output").format("", self.username or self.token_name, self.site_name))
187237
try:
@@ -245,7 +295,8 @@ def create_session(self, args):
245295

246296
if credentials and not signed_in_object:
247297
# logging in, not using an existing session
248-
self._create_new_connection()
298+
self.tableau_server = self._create_new_connection()
299+
self._verify_server_connection_unauthed()
249300
signed_in_object = self._sign_in(credentials)
250301

251302
if not signed_in_object:

tabcmd/commands/constants.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def is_expired_session(error):
2525
@staticmethod
2626
def is_resource_conflict(error):
2727
if hasattr(error, "code"):
28-
return error.code.startswith(Constants.resource_conflict_general)
28+
return error.code == Constants.source_already_exists
2929

3030
@staticmethod
3131
def is_login_error(error):
@@ -86,7 +86,7 @@ def check_common_error_codes_and_explain(logger, exception):
8686
# "session.session_expired_login"))
8787
# session.renew_session
8888
return
89-
if exception.code.startswith(Constants.source_not_found):
89+
if exception.code == Constants.source_not_found:
9090
logger.error(_("publish.errors.server_resource_not_found"), exception)
9191
else:
9292
logger.error(exception)

tabcmd/commands/datasources_and_workbooks/export_command.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ def define_args(export_parser):
3232
help="page orientation (landscape or portrait) of the exported PDF",
3333
)
3434
group.add_argument(
35-
3635
"--pagesize",
3736
choices=[
3837
pagesize.A3,

tabcmd/commands/datasources_and_workbooks/get_url_command.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ def run_command(args):
4646
Errors.exit_with_error(logger, _("export.errors.white_space_workbook_view"))
4747

4848
url = args.url.lstrip("/") # strip opening / if present
49+
content_type = GetUrl.evaluate_content_type(logger, url)
50+
file_type = GetUrl.get_file_type_from_filename(logger, url, args.filename)
51+
52+
GetUrl.get_content_as_file(file_type, content_type, logger, args, server, url)
4953

5054
## this first set of methods is all parsing the url and file input from the user
5155

@@ -60,7 +64,6 @@ def evaluate_content_type(logger, url):
6064
return content_type
6165
Errors.exit_with_error(logger, message=_("get.errors.invalid_content_type").format(url))
6266

63-
6467
@staticmethod
6568
def explain_expected_url(logger, url: str, command: str):
6669
view_example = "/views/<workbookname>/<viewname>[.ext]"
@@ -74,13 +77,19 @@ def explain_expected_url(logger, url: str, command: str):
7477
Errors.exit_with_error(logger, message)
7578

7679
@staticmethod
77-
def get_file_type_from_filename(logger, file_name, url):
80+
def get_file_type_from_filename(logger, url, file_name):
81+
logger.debug("Choosing between {}, {}".format(file_name, url))
7882
file_name = file_name or url
7983
logger.debug(_("get.options.file") + ": {}".format(file_name))
8084
type_of_file = GetUrl.get_file_extension(file_name)
8185

82-
if not type_of_file:
83-
Errors.exit_with_error(logger, _("tabcmd.get.extension.not_found").format(file_name))
86+
if not type_of_file and file_name is not None:
87+
# check the url
88+
backup = GetUrl.get_file_extension(url)
89+
if backup is not None:
90+
type_of_file = backup
91+
else:
92+
Errors.exit_with_error(logger, _("tabcmd.get.extension.not_found").format(file_name))
8493

8594
logger.debug("filetype: {}".format(type_of_file))
8695
if type_of_file in ["pdf", "csv", "png", "twb", "twbx", "tdsx"]:
@@ -134,14 +143,17 @@ def get_view_url(url, logger): # "views/wb-name/view-name" -> wb-name/sheets/vi
134143
@staticmethod
135144
def filename_from_args(file_argument, item_name, filetype):
136145
if file_argument is None:
137-
file_argument = "{}.{}".format(item_name, filetype)
146+
file_argument = item_name
147+
if not file_argument.endswith(filetype):
148+
file_argument = "{}.{}".format(file_argument, filetype)
138149
return file_argument
139150

140151
## methods below here have done all the parsing and just have to do the download and saving
141152
## these should be able to be shared with export
142153

143154
@staticmethod
144155
def get_content_as_file(file_type, content_type, logger, args, server, url):
156+
logger.debug("fetching {} as {}".format(content_type, file_type))
145157
if content_type == "workbook":
146158
return GetUrl.generate_twb(logger, server, args, file_type, url)
147159
elif content_type == "datasource":
@@ -207,9 +219,12 @@ def generate_twb(logger, server, args, file_extension, url):
207219
target_workbook = GetUrl.get_wb_by_content_url(logger, server, workbook_name)
208220
logger.debug(_("content_type.workbook") + ": {}".format(workbook_name))
209221
file_name_with_path = GetUrl.filename_from_args(args.filename, workbook_name, file_extension)
210-
logger.debug("Saving as {}".format(file_name_with_path))
211-
server.workbooks.download(target_workbook.id, filepath=None, no_extract=False)
212-
logger.info(_("export.success").format(target_workbook.name, file_name_with_path))
222+
# the download method will add an extension. How do I tell which one?
223+
file_name_with_path = GetUrl.get_name_without_possible_extension(file_name_with_path)
224+
file_name_with_ext = "{}.{}".format(file_name_with_path, file_extension)
225+
logger.debug("Saving as {}".format(file_name_with_ext))
226+
server.workbooks.download(target_workbook.id, filepath=file_name_with_path, no_extract=False)
227+
logger.info(_("export.success").format(target_workbook.name, file_name_with_ext))
213228
except Exception as e:
214229
Errors.exit_with_error(logger, e)
215230

@@ -221,8 +236,11 @@ def generate_tds(logger, server, args, file_extension):
221236
target_datasource = GetUrl.get_ds_by_content_url(logger, server, datasource_name)
222237
logger.debug(_("content_type.datasource") + ": {}".format(datasource_name))
223238
file_name_with_path = GetUrl.filename_from_args(args.filename, datasource_name, file_extension)
224-
logger.debug("Saving as {}".format(file_name_with_path))
225-
server.datasources.download(target_datasource.id, filepath=None, no_extract=False)
226-
logger.info(_("export.success").format(target_datasource.name, file_name_with_path))
239+
# the download method will add an extension
240+
file_name_with_path = GetUrl.get_name_without_possible_extension(file_name_with_path)
241+
file_name_with_ext = "{}.{}".format(file_name_with_path, file_extension)
242+
logger.debug("Saving as {}".format(file_name_with_ext))
243+
server.datasources.download(target_datasource.id, filepath=file_name_with_path, no_extract=False)
244+
logger.info(_("export.success").format(target_datasource.name, file_name_with_ext))
227245
except Exception as e:
228246
Errors.exit_with_error(logger, e)

0 commit comments

Comments
 (0)