WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit 71f7273

Browse files
push code for 2.11 (#229)
* make usernames case sensitive in import * Set user agent (#219) * add logging to check signin failure * add flows and --detail option to listing output * encoding filter values to handle spaces and special chars in filters, added new --filter option to pass in un-encoded value for simpler input * add --url, --include-all/--embedded-datasources for create/refresh/delete extract commands * replace polling code with library call --------- Co-authored-by: Bhuvnesh Singh <[email protected]>
1 parent 5a5d2fd commit 71f7273

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+466
-320
lines changed

WorldIndicators.tdsx

157 KB
Binary file not shown.

pyproject.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,14 @@ dependencies = [
4444
"appdirs",
4545
"doit",
4646
"ftfy",
47-
"polling2",
4847
"pyinstaller_versionfile",
49-
"requests>=2.11,<3.0",
48+
"requests>=2.25,<3.0",
5049
"setuptools_scm",
5150
"types-appdirs",
5251
"types-mock",
5352
"types-requests",
5453
"types-setuptools",
55-
"tableauserverclient>=0.19",
54+
"tableauserverclient>=0.23",
5655
"urllib3>=1.24.3,<2.0",
5756
]
5857
[project.optional-dependencies]

tabcmd/commands/auth/login_command.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,4 @@ def run_command(args):
2222
logger = log(__class__.__name__, args.logging_level)
2323
logger.debug(_("tabcmd.launching"))
2424
session = Session()
25-
session.create_session(args)
25+
session.create_session(args, logger)

tabcmd/commands/auth/session.py

Lines changed: 65 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ def __init__(self):
4848
self.timeout = None
4949

5050
self.logging_level = "info"
51-
self._read_from_json()
5251
self.logger = log(__name__, self.logging_level) # instantiate here mostly for tests
52+
self._read_from_json()
5353
self.tableau_server = None # this one is an object that doesn't get persisted in the file
5454

5555
# called before we connect to the server
@@ -206,7 +206,7 @@ def _create_new_connection(self) -> TSC.Server:
206206
return self.tableau_server
207207

208208
def _read_existing_state(self):
209-
if self._check_json():
209+
if self._json_exists():
210210
self._read_from_json()
211211

212212
def _print_server_info(self):
@@ -265,13 +265,13 @@ def _get_saved_credentials(self):
265265
return credentials
266266

267267
# external entry point:
268-
def create_session(self, args):
268+
def create_session(self, args, logger):
269269
signed_in_object = None
270270
# pull out cached info from json, then overwrite with new args if available
271271
self._read_existing_state()
272272
self._update_session_data(args)
273273
self.logging_level = args.logging_level or self.logging_level
274-
self.logger = self.logger or log(__class__.__name__, self.logging_level)
274+
self.logger = logger or log(__class__.__name__, self.logging_level)
275275

276276
credentials = None
277277
if args.password:
@@ -344,51 +344,70 @@ def _clear_data(self):
344344
self.timeout = None
345345

346346
# json file functions ----------------------------------------------------
347+
# These should be moved into a separate class
347348
def _get_file_path(self):
348349
home_path = os.path.expanduser("~")
349350
file_path = os.path.join(home_path, "tableau_auth.json")
350351
return file_path
351352

352353
def _read_from_json(self):
353-
if not self._check_json():
354+
if not self._json_exists():
354355
return
355356
file_path = self._get_file_path()
356-
data = {}
357-
with open(str(file_path), "r") as file_contents:
358-
data = json.load(file_contents)
357+
content = None
359358
try:
360-
for auth in data["tableau_auth"]:
361-
self.auth_token = auth["auth_token"]
362-
self.server_url = auth["server"]
363-
self.site_name = auth["site_name"]
364-
self.site_id = auth["site_id"]
365-
self.username = auth["username"]
366-
self.user_id = auth["user_id"]
367-
self.token_name = auth["personal_access_token_name"]
368-
self.token_value = auth["personal_access_token"]
369-
self.last_login_using = auth["last_login_using"]
370-
self.password_file = auth["password_file"]
371-
self.no_prompt = auth["no_prompt"]
372-
self.no_certcheck = auth["no_certcheck"]
373-
self.certificate = auth["certificate"]
374-
self.no_proxy = auth["no_proxy"]
375-
self.proxy = auth["proxy"]
376-
self.timeout = auth["timeout"]
377-
except KeyError as e:
378-
self.logger.debug(_("sessionoptions.errors.bad_password_file"), e)
379-
self._remove_json()
380-
except Exception as any_error:
381-
self.logger.info(_("session.new_session"))
382-
self._remove_json()
359+
with open(str(file_path), "r") as file_contents:
360+
data = json.load(file_contents)
361+
content = data["tableau_auth"]
362+
except json.JSONDecodeError as e:
363+
self._wipe_bad_json(e, "Error reading data from session file")
364+
except IOError as e:
365+
self._wipe_bad_json(e, "Error reading session file")
366+
except AttributeError as e:
367+
self._wipe_bad_json(e, "Error parsing session details from file")
368+
except Exception as e:
369+
self._wipe_bad_json(e, "Unexpected error reading session details from file")
370+
371+
try:
372+
auth = content[0]
373+
self.auth_token = auth["auth_token"]
374+
self.server_url = auth["server"]
375+
self.site_name = auth["site_name"]
376+
self.site_id = auth["site_id"]
377+
self.username = auth["username"]
378+
self.user_id = auth["user_id"]
379+
self.token_name = auth["personal_access_token_name"]
380+
self.token_value = auth["personal_access_token"]
381+
self.last_login_using = auth["last_login_using"]
382+
self.password_file = auth["password_file"]
383+
self.no_prompt = auth["no_prompt"]
384+
self.no_certcheck = auth["no_certcheck"]
385+
self.certificate = auth["certificate"]
386+
self.no_proxy = auth["no_proxy"]
387+
self.proxy = auth["proxy"]
388+
self.timeout = auth["timeout"]
389+
except AttributeError as e:
390+
self._wipe_bad_json(e, "Unrecognized attribute in session file")
391+
except Exception as e:
392+
self._wipe_bad_json(e, "Failed to load session file")
383393

384-
def _check_json(self):
394+
def _wipe_bad_json(self, e, message):
395+
self.logger.debug(message + ": " + e.__str__())
396+
self.logger.info(_("session.new_session"))
397+
self._remove_json()
398+
399+
def _json_exists(self):
400+
# todo: make this location configurable
385401
home_path = os.path.expanduser("~")
386402
file_path = os.path.join(home_path, "tableau_auth.json")
387403
return os.path.exists(file_path)
388404

389405
def _save_session_to_json(self):
390-
data = self._serialize_for_save()
391-
self._save_file(data)
406+
try:
407+
data = self._serialize_for_save()
408+
self._save_file(data)
409+
except Exception as e:
410+
self._wipe_bad_json(e, "Failed to save session file")
392411

393412
def _save_file(self, data):
394413
file_path = self._get_file_path()
@@ -420,7 +439,15 @@ def _serialize_for_save(self):
420439
return data
421440

422441
def _remove_json(self):
423-
file_path = self._get_file_path()
424-
self._save_file({})
425-
if os.path.exists(file_path):
426-
os.remove(file_path)
442+
file_path = ""
443+
try:
444+
if not self._json_exists():
445+
return
446+
file_path = self._get_file_path()
447+
self._save_file({})
448+
if os.path.exists(file_path):
449+
os.remove(file_path)
450+
except Exception as e:
451+
message = "Error clearing session data from {}: check and remove manually".format(file_path)
452+
self.logger.error(message)
453+
self.logger.error(e)

tabcmd/commands/constants.py

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import inspect
22
import sys
33

4-
from tableauserverclient import ServerResponseError
5-
64
from tabcmd.execution.localize import _
75

86

@@ -25,20 +23,19 @@ def is_expired_session(error):
2523
@staticmethod
2624
def is_resource_conflict(error):
2725
if hasattr(error, "code"):
28-
return error.code == Constants.source_already_exists
26+
return error.code.startswith(Constants.resource_conflict_general)
2927

3028
@staticmethod
3129
def is_login_error(error):
3230
if hasattr(error, "code"):
3331
return error.code == Constants.login_error
3432

35-
@staticmethod
36-
def is_server_response_error(error):
37-
return isinstance(error, ServerResponseError)
38-
3933
# https://gist.github.com/FredLoney/5454553
4034
@staticmethod
4135
def log_stack(logger):
36+
if not logger:
37+
print("logger not available: cannot show stack")
38+
return
4239
try:
4340
"""The log header message formatter."""
4441
HEADER_FMT = "Printing Call Stack at %s::%s"
@@ -49,11 +46,10 @@ def log_stack(logger):
4946
file, line, func = here[1:4]
5047
start = 0
5148
n_lines = 5
52-
logger.trace(HEADER_FMT % (file, func))
53-
54-
for frame in stack[start + 2 : n_lines]:
49+
logger.debug(HEADER_FMT % (file, func))
50+
for frame in stack[start + 1 : n_lines]:
5551
file, line, func = frame[1:4]
56-
logger.trace(STACK_FMT % (file, line, func))
52+
logger.debug(STACK_FMT % (file, line, func))
5753
except Exception as e:
5854
logger.info("Error printing stack trace:", e)
5955

@@ -66,9 +62,9 @@ def exit_with_error(logger, message=None, exception=None):
6662
if exception:
6763
if message:
6864
logger.debug("Error message: " + message)
69-
Errors.check_common_error_codes_and_explain(logger, exception)
65+
Errors.check_common_error_codes_and_explain(logger, exception)
7066
except Exception as exc:
71-
print(sys.stderr, "Error during log call from exception - {}".format(exc))
67+
print(sys.stderr, "Error during log call from exception - {} {}".format(exc.__class__, message))
7268
try:
7369
logger.info("Exiting...")
7470
except Exception:
@@ -77,16 +73,15 @@ def exit_with_error(logger, message=None, exception=None):
7773

7874
@staticmethod
7975
def check_common_error_codes_and_explain(logger, exception):
80-
if Errors.is_server_response_error(exception):
81-
logger.error(_("publish.errors.unexpected_server_response").format(exception))
82-
if Errors.is_expired_session(exception):
83-
logger.error(_("session.errors.session_expired"))
84-
# TODO: add session as an argument to this method
85-
# and add the full command line as a field in Session?
86-
# "session.session_expired_login"))
87-
# session.renew_session
88-
return
89-
if exception.code == Constants.source_not_found:
90-
logger.error(_("publish.errors.server_resource_not_found"), exception)
76+
# most errors contain as much info in the message as we can get from the code
77+
# identify any that we can add useful detail for and include them here
78+
if Errors.is_expired_session(exception):
79+
# catch this one so we can attempt to refresh the session before telling them it failed
80+
logger.error(_("session.errors.session_expired"))
81+
# TODO: add session as an argument to this method
82+
# and add the full command line as a field in Session?
83+
# "session.session_expired_login"))
84+
# session.renew_session()
85+
return
9186
else:
9287
logger.error(exception)

tabcmd/commands/datasources_and_workbooks/datasources_and_workbooks_command.py

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import urllib
2+
13
import tableauserverclient as TSC
24

35
from tabcmd.commands.constants import Errors
@@ -23,7 +25,7 @@ def get_view_by_content_url(logger, server, view_content_url) -> TSC.ViewItem:
2325
try:
2426
req_option = TSC.RequestOptions()
2527
req_option.filter.add(TSC.Filter("contentUrl", TSC.RequestOptions.Operator.Equals, view_content_url))
26-
logger.trace(req_option.get_query_params())
28+
logger.debug(req_option.get_query_params())
2729
matching_views, paging = server.views.get(req_option)
2830
except Exception as e:
2931
Errors.exit_with_error(logger, e)
@@ -37,7 +39,7 @@ def get_wb_by_content_url(logger, server, workbook_content_url) -> TSC.WorkbookI
3739
try:
3840
req_option = TSC.RequestOptions()
3941
req_option.filter.add(TSC.Filter("contentUrl", TSC.RequestOptions.Operator.Equals, workbook_content_url))
40-
logger.trace(req_option.get_query_params())
42+
logger.debug(req_option.get_query_params())
4143
matching_workbooks, paging = server.workbooks.get(req_option)
4244
except Exception as e:
4345
Errors.exit_with_error(logger, e)
@@ -51,7 +53,7 @@ def get_ds_by_content_url(logger, server, datasource_content_url) -> TSC.Datasou
5153
try:
5254
req_option = TSC.RequestOptions()
5355
req_option.filter.add(TSC.Filter("contentUrl", TSC.RequestOptions.Operator.Equals, datasource_content_url))
54-
logger.trace(req_option.get_query_params())
56+
logger.debug(req_option.get_query_params())
5557
matching_datasources, paging = server.datasources.get(req_option)
5658
except Exception as e:
5759
Errors.exit_with_error(logger, e)
@@ -60,39 +62,52 @@ def get_ds_by_content_url(logger, server, datasource_content_url) -> TSC.Datasou
6062
return matching_datasources[0]
6163

6264
@staticmethod
63-
def apply_values_from_url_params(request_options: TSC.PDFRequestOptions, url, logger) -> None:
64-
# should be able to replace this with request_options._append_view_filters(params)
65+
def apply_values_from_url_params(logger, request_options: TSC.PDFRequestOptions, url) -> None:
6566
logger.debug(url)
6667
try:
6768
if "?" in url:
6869
query = url.split("?")[1]
69-
logger.trace("Query parameters: {}".format(query))
70+
logger.debug("Query parameters: {}".format(query))
7071
else:
7172
logger.debug("No query parameters present in url")
7273
return
7374

7475
params = query.split("&")
75-
logger.trace(params)
76+
logger.debug(params)
7677
for value in params:
7778
if value.startswith(":"):
78-
DatasourcesAndWorkbooks.apply_option_value(request_options, value, logger)
79+
DatasourcesAndWorkbooks.apply_options_in_url(logger, request_options, value)
7980
else: # it must be a filter
80-
DatasourcesAndWorkbooks.apply_filter_value(request_options, value, logger)
81+
DatasourcesAndWorkbooks.apply_encoded_filter_value(logger, request_options, value)
8182

8283
except Exception as e:
8384
logger.warn("Error building filter params", e)
8485
# ExportCommand.log_stack(logger) # type: ignore
8586

87+
# this is called from within from_url_params, for each view_filter value
88+
@staticmethod
89+
def apply_encoded_filter_value(logger, request_options, value):
90+
# the REST API doesn't appear to have the option to disambiguate with "Parameters.<fieldname>"
91+
value = value.replace("Parameters.", "")
92+
# the filter values received from the url are already url encoded. tsc will encode them again.
93+
# so we run url.decode, which will be a no-op if they are not encoded.
94+
decoded_value = urllib.parse.unquote(value)
95+
logger.debug("url had `{0}`, saved as `{1}`".format(value, decoded_value))
96+
DatasourcesAndWorkbooks.apply_filter_value(logger, request_options, decoded_value)
97+
98+
# this is called for each filter value,
99+
# from apply_options, which expects an un-encoded input,
100+
# or from apply_url_params via apply_encoded_filter_value which decodes the input
86101
@staticmethod
87-
def apply_filter_value(request_options: TSC.PDFRequestOptions, value: str, logger) -> None:
88-
# todo: do we need to strip Parameters.x -> x?
89-
logger.trace("handling filter param {}".format(value))
102+
def apply_filter_value(logger, request_options: TSC.PDFRequestOptions, value: str) -> None:
103+
logger.debug("handling filter param {}".format(value))
90104
data_filter = value.split("=")
91105
request_options.vf(data_filter[0], data_filter[1])
92106

107+
# this is called from within from_url_params, for each param value
93108
@staticmethod
94-
def apply_option_value(request_options: TSC.PDFRequestOptions, value: str, logger) -> None:
95-
logger.trace("handling url option {}".format(value))
109+
def apply_options_in_url(logger, request_options: TSC.PDFRequestOptions, value: str) -> None:
110+
logger.debug("handling url option {}".format(value))
96111
setting = value.split("=")
97112
if ":iid" == setting[0]:
98113
logger.debug(":iid value ignored in url")
@@ -111,19 +126,18 @@ def is_truthy(value: str):
111126
return value.lower() in ["yes", "y", "1", "true"]
112127

113128
@staticmethod
114-
def apply_png_options(request_options: TSC.ImageRequestOptions, args, logger):
129+
def apply_png_options(logger, request_options: TSC.ImageRequestOptions, args):
115130
if args.height or args.width:
116-
# only applicable for png
117131
logger.warn("Height/width arguments not yet implemented in export")
118132
# Always request high-res images
119133
request_options.image_resolution = "high"
120134

121135
@staticmethod
122-
def apply_pdf_options(request_options: TSC.PDFRequestOptions, args, logger):
123-
request_options.page_type = args.pagesize
136+
def apply_pdf_options(logger, request_options: TSC.PDFRequestOptions, args):
124137
if args.pagelayout:
125-
logger.debug("Setting page layout to: {}".format(args.pagelayout))
126138
request_options.orientation = args.pagelayout
139+
if args.pagesize:
140+
request_options.page_type = args.pagesize
127141

128142
@staticmethod
129143
def save_to_data_file(logger, output, filename):

tabcmd/commands/datasources_and_workbooks/delete_command.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def run_command(args):
3232
logger = log(__class__.__name__, args.logging_level)
3333
logger.debug(_("tabcmd.launching"))
3434
session = Session()
35-
server = session.create_session(args)
35+
server = session.create_session(args, logger)
3636
content_type: str = ""
3737
if args.workbook:
3838
content_type = "workbook"

0 commit comments

Comments
 (0)