diff --git a/.gitignore b/.gitignore index d8caf99a9..f0226c065 100644 --- a/.gitignore +++ b/.gitignore @@ -84,6 +84,7 @@ celerybeat-schedule # dotenv .env +env.py # virtualenv venv/ diff --git a/samples/add_default_permission.py b/samples/add_default_permission.py index 8a87c1fd6..5a450e8ab 100644 --- a/samples/add_default_permission.py +++ b/samples/add_default_permission.py @@ -18,14 +18,10 @@ def main(): parser = argparse.ArgumentParser(description="Add workbook default permissions for a given project.") # Common options; please keep those in sync across all samples - parser.add_argument("--server", "-s", required=True, help="server address") + parser.add_argument("--server", "-s", help="server address") parser.add_argument("--site", "-S", help="site name") - parser.add_argument( - "--token-name", "-p", required=True, help="name of the personal access token used to sign into the server" - ) - parser.add_argument( - "--token-value", "-v", required=True, help="value of the personal access token used to sign into the server" - ) + parser.add_argument("--token-name", "-p", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server") parser.add_argument( "--logging-level", "-l", diff --git a/samples/create_group.py b/samples/create_group.py index 2229f7f26..f4c6a9ca9 100644 --- a/samples/create_group.py +++ b/samples/create_group.py @@ -20,14 +20,10 @@ def main(): parser = argparse.ArgumentParser(description="Creates a sample user group.") # Common options; please keep those in sync across all samples - parser.add_argument("--server", "-s", required=True, help="server address") + parser.add_argument("--server", "-s", help="server address") parser.add_argument("--site", "-S", help="site name") - parser.add_argument( - "--token-name", "-p", required=True, help="name of the personal access token used to sign into the server" - ) - parser.add_argument( - "--token-value", "-v", required=True, help="value of the personal access token used to sign into the server" - ) + parser.add_argument("--token-name", "-p", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server") parser.add_argument( "--logging-level", "-l", diff --git a/samples/create_project.py b/samples/create_project.py index 8b2ec3354..611dbe366 100644 --- a/samples/create_project.py +++ b/samples/create_project.py @@ -28,14 +28,10 @@ def create_project(server, project_item, samples=False): def main(): parser = argparse.ArgumentParser(description="Create new projects.") # Common options; please keep those in sync across all samples - parser.add_argument("--server", "-s", required=True, help="server address") + parser.add_argument("--server", "-s", help="server address") parser.add_argument("--site", "-S", help="site name") - parser.add_argument( - "--token-name", "-p", required=True, help="name of the personal access token used to sign into the server" - ) - parser.add_argument( - "--token-value", "-v", required=True, help="value of the personal access token used to sign into the server" - ) + parser.add_argument("--token-name", "-p", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server") parser.add_argument( "--logging-level", "-l", diff --git a/samples/create_schedules.py b/samples/create_schedules.py index f193352de..dee088571 100644 --- a/samples/create_schedules.py +++ b/samples/create_schedules.py @@ -17,14 +17,10 @@ def main(): parser = argparse.ArgumentParser(description="Creates sample schedules for each type of frequency.") # Common options; please keep those in sync across all samples - parser.add_argument("--server", "-s", required=True, help="server address") + parser.add_argument("--server", "-s", help="server address") parser.add_argument("--site", "-S", help="site name") - parser.add_argument( - "--token-name", "-p", required=True, help="name of the personal access token used to sign into the server" - ) - parser.add_argument( - "--token-value", "-v", required=True, help="value of the personal access token used to sign into the server" - ) + parser.add_argument("--token-name", "-p", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server") parser.add_argument( "--logging-level", "-l", diff --git a/samples/explore_datasource.py b/samples/explore_datasource.py index aafbe167c..fb45cb45e 100644 --- a/samples/explore_datasource.py +++ b/samples/explore_datasource.py @@ -18,14 +18,10 @@ def main(): parser = argparse.ArgumentParser(description="Explore datasource functions supported by the Server API.") # Common options; please keep those in sync across all samples - parser.add_argument("--server", "-s", required=True, help="server address") + parser.add_argument("--server", "-s", help="server address") parser.add_argument("--site", "-S", help="site name") - parser.add_argument( - "--token-name", "-p", required=True, help="name of the personal access token used to sign into the server" - ) - parser.add_argument( - "--token-value", "-v", required=True, help="value of the personal access token used to sign into the server" - ) + parser.add_argument("--token-name", "-p", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server") parser.add_argument( "--logging-level", "-l", diff --git a/samples/explore_site.py b/samples/explore_site.py index a181abfec..a2274f1a7 100644 --- a/samples/explore_site.py +++ b/samples/explore_site.py @@ -14,14 +14,10 @@ def main(): parser = argparse.ArgumentParser(description="Explore site updates by the Server API.") # Common options; please keep those in sync across all samples - parser.add_argument("--server", "-s", required=True, help="server address") + parser.add_argument("--server", "-s", help="server address") parser.add_argument("--site", "-S", help="site name") - parser.add_argument( - "--token-name", "-p", required=True, help="name of the personal access token used to sign into the server" - ) - parser.add_argument( - "--token-value", "-v", required=True, help="value of the personal access token used to sign into the server" - ) + parser.add_argument("--token-name", "-p", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server") parser.add_argument( "--logging-level", "-l", diff --git a/samples/explore_webhooks.py b/samples/explore_webhooks.py index 47e59ac06..77802b1db 100644 --- a/samples/explore_webhooks.py +++ b/samples/explore_webhooks.py @@ -19,14 +19,10 @@ def main(): parser = argparse.ArgumentParser(description="Explore webhook functions supported by the Server API.") # Common options; please keep those in sync across all samples - parser.add_argument("--server", "-s", required=True, help="server address") + parser.add_argument("--server", "-s", help="server address") parser.add_argument("--site", "-S", help="site name") - parser.add_argument( - "--token-name", "-p", required=True, help="name of the personal access token used to sign into the server" - ) - parser.add_argument( - "--token-value", "-v", required=True, help="value of the personal access token used to sign into the server" - ) + parser.add_argument("--token-name", "-p", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server") parser.add_argument( "--logging-level", "-l", diff --git a/samples/explore_workbook.py b/samples/explore_workbook.py index f242ace70..c61b9b637 100644 --- a/samples/explore_workbook.py +++ b/samples/explore_workbook.py @@ -19,14 +19,10 @@ def main(): parser = argparse.ArgumentParser(description="Explore workbook functions supported by the Server API.") # Common options; please keep those in sync across all samples - parser.add_argument("--server", "-s", required=True, help="server address") + parser.add_argument("--server", "-s", help="server address") parser.add_argument("--site", "-S", help="site name") - parser.add_argument( - "--token-name", "-p", required=True, help="name of the personal access token used to sign into the server" - ) - parser.add_argument( - "--token-value", "-v", required=True, help="value of the personal access token used to sign into the server" - ) + parser.add_argument("--token-name", "-p", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server") parser.add_argument( "--logging-level", "-l", diff --git a/samples/export.py b/samples/export.py index 4c26770b9..f2783fa6e 100644 --- a/samples/export.py +++ b/samples/export.py @@ -14,14 +14,10 @@ def main(): parser = argparse.ArgumentParser(description="Export a view as an image, PDF, or CSV") # Common options; please keep those in sync across all samples - parser.add_argument("--server", "-s", required=True, help="server address") + parser.add_argument("--server", "-s", help="server address") parser.add_argument("--site", "-S", help="site name") - parser.add_argument( - "--token-name", "-p", required=True, help="name of the personal access token used to sign into the server" - ) - parser.add_argument( - "--token-value", "-v", required=True, help="value of the personal access token used to sign into the server" - ) + parser.add_argument("--token-name", "-p", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server") parser.add_argument( "--logging-level", "-l", diff --git a/samples/extracts.py b/samples/extracts.py index c77da89d0..9bd87a473 100644 --- a/samples/extracts.py +++ b/samples/extracts.py @@ -19,14 +19,10 @@ def main(): parser = argparse.ArgumentParser(description="Explore extract functions supported by the Server API.") # Common options; please keep those in sync across all samples - parser.add_argument("--server", "-s", required=True, help="server address") + parser.add_argument("--server", "-s", help="server address") parser.add_argument("--site", help="site name") - parser.add_argument( - "--token-name", "-tn", required=True, help="name of the personal access token used to sign into the server" - ) - parser.add_argument( - "--token-value", "-tv", required=True, help="value of the personal access token used to sign into the server" - ) + parser.add_argument("--token-name", "-tn", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-tv", help="value of the personal access token used to sign into the server") parser.add_argument( "--logging-level", "-l", diff --git a/samples/filter_sort_groups.py b/samples/filter_sort_groups.py index 984d8d344..042af32e2 100644 --- a/samples/filter_sort_groups.py +++ b/samples/filter_sort_groups.py @@ -26,14 +26,10 @@ def create_example_group(group_name="Example Group", server=None): def main(): parser = argparse.ArgumentParser(description="Filter and sort groups.") # Common options; please keep those in sync across all samples - parser.add_argument("--server", "-s", required=True, help="server address") + parser.add_argument("--server", "-s", help="server address") parser.add_argument("--site", "-S", help="site name") - parser.add_argument( - "--token-name", "-p", required=True, help="name of the personal access token used to sign into the server" - ) - parser.add_argument( - "--token-value", "-v", required=True, help="value of the personal access token used to sign into the server" - ) + parser.add_argument("--token-name", "-p", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server") parser.add_argument( "--logging-level", "-l", diff --git a/samples/filter_sort_projects.py b/samples/filter_sort_projects.py index 608f472ba..7aa62a5c1 100644 --- a/samples/filter_sort_projects.py +++ b/samples/filter_sort_projects.py @@ -29,14 +29,10 @@ def create_example_project( def main(): parser = argparse.ArgumentParser(description="Filter and sort projects.") # Common options; please keep those in sync across all samples - parser.add_argument("--server", "-s", required=True, help="server address") + parser.add_argument("--server", "-s", help="server address") parser.add_argument("--site", "-S", help="site name") - parser.add_argument( - "--token-name", "-p", required=True, help="name of the personal access token used to sign into the server" - ) - parser.add_argument( - "--token-value", "-v", required=True, help="value of the personal access token used to sign into the server" - ) + parser.add_argument("--token-name", "-p", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server") parser.add_argument( "--logging-level", "-l", diff --git a/samples/initialize_server.py b/samples/initialize_server.py index e7ed0139f..cb3d9e1d0 100644 --- a/samples/initialize_server.py +++ b/samples/initialize_server.py @@ -13,14 +13,10 @@ def main(): parser = argparse.ArgumentParser(description="Initialize a server with content.") # Common options; please keep those in sync across all samples - parser.add_argument("--server", "-s", required=True, help="server address") + parser.add_argument("--server", "-s", help="server address") parser.add_argument("--site", "-S", help="site name") - parser.add_argument( - "--token-name", "-p", required=True, help="name of the personal access token used to sign into the server" - ) - parser.add_argument( - "--token-value", "-v", required=True, help="value of the personal access token used to sign into the server" - ) + parser.add_argument("--token-name", "-p", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server") parser.add_argument( "--logging-level", "-l", @@ -29,8 +25,8 @@ def main(): help="desired logging level (set to error by default)", ) # Options specific to this sample - parser.add_argument("--datasources-folder", "-df", required=True, help="folder containing datasources") - parser.add_argument("--workbooks-folder", "-wf", required=True, help="folder containing workbooks") + parser.add_argument("--datasources-folder", "-df", help="folder containing datasources") + parser.add_argument("--workbooks-folder", "-wf", help="folder containing workbooks") parser.add_argument("--project", required=False, default="Default", help="project to use") args = parser.parse_args() diff --git a/samples/kill_all_jobs.py b/samples/kill_all_jobs.py index 1a833f938..bfebb49b8 100644 --- a/samples/kill_all_jobs.py +++ b/samples/kill_all_jobs.py @@ -13,14 +13,10 @@ def main(): parser = argparse.ArgumentParser(description="Cancel all of the running background jobs.") # Common options; please keep those in sync across all samples - parser.add_argument("--server", "-s", required=True, help="server address") + parser.add_argument("--server", "-s", help="server address") parser.add_argument("--site", "-S", help="site name") - parser.add_argument( - "--token-name", "-p", required=True, help="name of the personal access token used to sign into the server" - ) - parser.add_argument( - "--token-value", "-v", required=True, help="value of the personal access token used to sign into the server" - ) + parser.add_argument("--token-name", "-p", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server") parser.add_argument( "--logging-level", "-l", diff --git a/samples/list.py b/samples/list.py index b5cdb38a5..8d72fb620 100644 --- a/samples/list.py +++ b/samples/list.py @@ -15,14 +15,10 @@ def main(): parser = argparse.ArgumentParser(description="List out the names and LUIDs for different resource types.") # Common options; please keep those in sync across all samples - parser.add_argument("--server", "-s", required=True, help="server address") + parser.add_argument("--server", "-s", help="server address") parser.add_argument("--site", "-S", help="site name") - parser.add_argument( - "--token-name", "-n", required=True, help="name of the personal access token used to sign into the server" - ) - parser.add_argument( - "--token-value", "-v", required=True, help="value of the personal access token used to sign into the server" - ) + parser.add_argument("--token-name", "-n", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server") parser.add_argument( "--logging-level", "-l", diff --git a/samples/login.py b/samples/login.py index f3e9d77dc..6a3e9e8b3 100644 --- a/samples/login.py +++ b/samples/login.py @@ -9,6 +9,7 @@ import logging import tableauserverclient as TSC +import env # If a sample has additional arguments, then it should copy this code and insert them after the call to @@ -18,10 +19,15 @@ def set_up_and_log_in(): parser = argparse.ArgumentParser(description="Logs in to the server.") sample_define_common_options(parser) args = parser.parse_args() - - # Set logging level based on user input, or error by default. - logging_level = getattr(logging, args.logging_level.upper()) - logging.basicConfig(level=logging_level) + if not args.server: + args.server = env.server + if not args.site: + args.site = env.site + if not args.token_name: + args.token_name = env.token_name + if not args.token_value: + args.token_value = env.token_value + args.logging_level = "debug" server = sample_connect_to_server(args) print(server.server_info.get()) @@ -30,9 +36,9 @@ def set_up_and_log_in(): def sample_define_common_options(parser): # Common options; please keep these in sync across all samples by copying or calling this method directly - parser.add_argument("--server", "-s", required=True, help="server address") + parser.add_argument("--server", "-s", help="server address") parser.add_argument("--site", "-t", help="site name") - auth = parser.add_mutually_exclusive_group(required=True) + auth = parser.add_mutually_exclusive_group(required=False) auth.add_argument("--token-name", "-tn", help="name of the personal access token used to sign into the server") auth.add_argument("--username", "-u", help="username to sign into the server") @@ -73,6 +79,9 @@ def sample_connect_to_server(args): # Make sure we use an updated version of the rest apis, and pass in our cert handling choice server = TSC.Server(args.server, use_server_version=True, http_options={"verify": check_ssl_certificate}) server.auth.sign_in(tableau_auth) + server.version = "2.6" + new_site: TSC.SiteItem = TSC.SiteItem("cdnear", content_url=env.site) + server.auth.switch_site(new_site) print("Logged in successfully") return server diff --git a/samples/metadata_query.py b/samples/metadata_query.py index 26f8f94fa..7524453c2 100644 --- a/samples/metadata_query.py +++ b/samples/metadata_query.py @@ -14,14 +14,10 @@ def main(): parser = argparse.ArgumentParser(description="Use the metadata API to get information on a published data source.") # Common options; please keep those in sync across all samples - parser.add_argument("--server", "-s", required=True, help="server address") + parser.add_argument("--server", "-s", help="server address") parser.add_argument("--site", "-S", help="site name") - parser.add_argument( - "--token-name", "-n", required=True, help="name of the personal access token used to sign into the server" - ) - parser.add_argument( - "--token-value", "-v", required=True, help="value of the personal access token used to sign into the server" - ) + parser.add_argument("--token-name", "-n", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server") parser.add_argument( "--logging-level", "-l", diff --git a/samples/move_workbook_projects.py b/samples/move_workbook_projects.py index be49ec23b..392dc0ff8 100644 --- a/samples/move_workbook_projects.py +++ b/samples/move_workbook_projects.py @@ -17,14 +17,10 @@ def main(): parser = argparse.ArgumentParser(description="Move one workbook from the default project to another.") # Common options; please keep those in sync across all samples - parser.add_argument("--server", "-s", required=True, help="server address") + parser.add_argument("--server", "-s", help="server address") parser.add_argument("--site", "-S", help="site name") - parser.add_argument( - "--token-name", "-p", required=True, help="name of the personal access token used to sign into the server" - ) - parser.add_argument( - "--token-value", "-v", required=True, help="value of the personal access token used to sign into the server" - ) + parser.add_argument("--token-name", "-p", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server") parser.add_argument( "--logging-level", "-l", @@ -33,8 +29,8 @@ def main(): help="desired logging level (set to error by default)", ) # Options specific to this sample - parser.add_argument("--workbook-name", "-w", required=True, help="name of workbook to move") - parser.add_argument("--destination-project", "-d", required=True, help="name of project to move workbook into") + parser.add_argument("--workbook-name", "-w", help="name of workbook to move") + parser.add_argument("--destination-project", "-d", help="name of project to move workbook into") args = parser.parse_args() diff --git a/samples/move_workbook_sites.py b/samples/move_workbook_sites.py index 3feb62be2..47af1f2f9 100644 --- a/samples/move_workbook_sites.py +++ b/samples/move_workbook_sites.py @@ -22,14 +22,10 @@ def main(): "the default project of another site." ) # Common options; please keep those in sync across all samples - parser.add_argument("--server", "-s", required=True, help="server address") + parser.add_argument("--server", "-s", help="server address") parser.add_argument("--site", "-S", help="site name") - parser.add_argument( - "--token-name", "-p", required=True, help="name of the personal access token used to sign into the server" - ) - parser.add_argument( - "--token-value", "-v", required=True, help="value of the personal access token used to sign into the server" - ) + parser.add_argument("--token-name", "-p", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server") parser.add_argument( "--logging-level", "-l", @@ -38,8 +34,8 @@ def main(): help="desired logging level (set to error by default)", ) # Options specific to this sample - parser.add_argument("--workbook-name", "-w", required=True, help="name of workbook to move") - parser.add_argument("--destination-site", "-d", required=True, help="name of site to move workbook into") + parser.add_argument("--workbook-name", "-w", help="name of workbook to move") + parser.add_argument("--destination-site", "-d", help="name of site to move workbook into") args = parser.parse_args() diff --git a/samples/pagination_sample.py b/samples/pagination_sample.py index b55fef320..a7ae6dc89 100644 --- a/samples/pagination_sample.py +++ b/samples/pagination_sample.py @@ -20,14 +20,10 @@ def main(): parser = argparse.ArgumentParser(description="Demonstrate pagination on the list of workbooks on the server.") # Common options; please keep those in sync across all samples - parser.add_argument("--server", "-s", required=True, help="server address") + parser.add_argument("--server", "-s", help="server address") parser.add_argument("--site", "-S", help="site name") - parser.add_argument( - "--token-name", "-n", required=True, help="name of the personal access token used to sign into the server" - ) - parser.add_argument( - "--token-value", "-v", required=True, help="value of the personal access token used to sign into the server" - ) + parser.add_argument("--token-name", "-n", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server") parser.add_argument( "--logging-level", "-l", diff --git a/samples/publish_datasource.py b/samples/publish_datasource.py index 8d9e59ea2..5ac768674 100644 --- a/samples/publish_datasource.py +++ b/samples/publish_datasource.py @@ -23,18 +23,17 @@ import tableauserverclient as TSC +import env +import tableauserverclient.datetime_helpers + def main(): parser = argparse.ArgumentParser(description="Publish a datasource to server.") # Common options; please keep those in sync across all samples - parser.add_argument("--server", "-s", required=True, help="server address") + parser.add_argument("--server", "-s", help="server address") parser.add_argument("--site", "-S", help="site name") - parser.add_argument( - "--token-name", "-p", required=True, help="name of the personal access token used to sign into the server" - ) - parser.add_argument( - "--token-value", "-v", required=True, help="value of the personal access token used to sign into the server" - ) + parser.add_argument("--token-name", "-p", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server") parser.add_argument( "--logging-level", "-l", @@ -43,7 +42,7 @@ def main(): help="desired logging level (set to error by default)", ) # Options specific to this sample - parser.add_argument("--file", "-f", required=True, help="filepath to the datasource to publish") + parser.add_argument("--file", "-f", help="filepath to the datasource to publish") parser.add_argument("--project", help="Project within which to publish the datasource") parser.add_argument("--async", "-a", help="Publishing asynchronously", dest="async_", action="store_true") parser.add_argument("--conn-username", help="connection username") @@ -52,14 +51,27 @@ def main(): parser.add_argument("--conn-oauth", help="connection is configured to use oAuth", action="store_true") args = parser.parse_args() + if not args.server: + args.server = env.server + if not args.site: + args.site = env.site + if not args.token_name: + args.token_name = env.token_name + if not args.token_value: + args.token_value = env.token_value + args.logging = "debug" + args.file = "C:/dev/tab-samples/5M.tdsx" + args.async_ = True # Ensure that both the connection username and password are provided, or none at all if (args.conn_username and not args.conn_password) or (not args.conn_username and args.conn_password): parser.error("Both the connection username and password must be provided") # Set logging level based on user input, or error by default - logging_level = getattr(logging, args.logging_level.upper()) - logging.basicConfig(level=logging_level) + + _logger = logging.getLogger(__name__) + _logger.setLevel(logging.DEBUG) + _logger.addHandler(logging.StreamHandler()) # Sign in to server tableau_auth = TSC.PersonalAccessTokenAuth(args.token_name, args.token_value, site_id=args.site) @@ -94,6 +106,7 @@ def main(): # Publish datasource if args.async_: + print("Publish as a job") # Async publishing, returns a job_item new_job = server.datasources.publish( new_datasource, args.file, publish_mode, connection_credentials=new_conn_creds, as_job=True @@ -104,7 +117,12 @@ def main(): new_datasource = server.datasources.publish( new_datasource, args.file, publish_mode, connection_credentials=new_conn_creds ) - print("Datasource published. Datasource ID: {0}".format(new_datasource.id)) + print( + "{0}Datasource published. Datasource ID: {1}".format( + new_datasource.id, tableauserverclient.datetime_helpers.timestamp() + ) + ) + print("\t\tClosing connection") if __name__ == "__main__": diff --git a/samples/publish_workbook.py b/samples/publish_workbook.py index f0edc380c..8a9f45279 100644 --- a/samples/publish_workbook.py +++ b/samples/publish_workbook.py @@ -24,14 +24,10 @@ def main(): parser = argparse.ArgumentParser(description="Publish a workbook to server.") # Common options; please keep those in sync across all samples - parser.add_argument("--server", "-s", required=True, help="server address") + parser.add_argument("--server", "-s", help="server address") parser.add_argument("--site", "-S", help="site name") - parser.add_argument( - "--token-name", "-p", required=True, help="name of the personal access token used to sign into the server" - ) - parser.add_argument( - "--token-value", "-v", required=True, help="value of the personal access token used to sign into the server" - ) + parser.add_argument("--token-name", "-p", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server") parser.add_argument( "--logging-level", "-l", @@ -40,7 +36,7 @@ def main(): help="desired logging level (set to error by default)", ) # Options specific to this sample - parser.add_argument("--file", "-f", required=True, help="local filepath of the workbook to publish") + parser.add_argument("--file", "-f", help="local filepath of the workbook to publish") parser.add_argument("--as-job", "-a", help="Publishing asynchronously", action="store_true") parser.add_argument("--skip-connection-check", "-c", help="Skip live connection check", action="store_true") diff --git a/samples/query_permissions.py b/samples/query_permissions.py index 7106da934..4e509cd97 100644 --- a/samples/query_permissions.py +++ b/samples/query_permissions.py @@ -15,14 +15,10 @@ def main(): parser = argparse.ArgumentParser(description="Query permissions of a given resource.") # Common options; please keep those in sync across all samples - parser.add_argument("--server", "-s", required=True, help="server address") + parser.add_argument("--server", "-s", help="server address") parser.add_argument("--site", "-S", help="site name") - parser.add_argument( - "--token-name", "-p", required=True, help="name of the personal access token used to sign into the server" - ) - parser.add_argument( - "--token-value", "-v", required=True, help="value of the personal access token used to sign into the server" - ) + parser.add_argument("--token-name", "-p", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server") parser.add_argument( "--logging-level", "-l", diff --git a/samples/refresh.py b/samples/refresh.py index f90441224..d3e49ed24 100644 --- a/samples/refresh.py +++ b/samples/refresh.py @@ -13,14 +13,10 @@ def main(): parser = argparse.ArgumentParser(description="Trigger a refresh task on a workbook or datasource.") # Common options; please keep those in sync across all samples - parser.add_argument("--server", "-s", required=True, help="server address") + parser.add_argument("--server", "-s", help="server address") parser.add_argument("--site", "-S", help="site name") - parser.add_argument( - "--token-name", "-p", required=True, help="name of the personal access token used to sign into the server" - ) - parser.add_argument( - "--token-value", "-v", required=True, help="value of the personal access token used to sign into the server" - ) + parser.add_argument("--token-name", "-p", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server") parser.add_argument( "--logging-level", "-l", diff --git a/samples/refresh_tasks.py b/samples/refresh_tasks.py index 2bfc85621..03daedf16 100644 --- a/samples/refresh_tasks.py +++ b/samples/refresh_tasks.py @@ -30,14 +30,10 @@ def handle_info(server, args): def main(): parser = argparse.ArgumentParser(description="Get all of the refresh tasks available on a server") # Common options; please keep those in sync across all samples - parser.add_argument("--server", "-s", required=True, help="server address") + parser.add_argument("--server", "-s", help="server address") parser.add_argument("--site", "-S", help="site name") - parser.add_argument( - "--token-name", "-p", required=True, help="name of the personal access token used to sign into the server" - ) - parser.add_argument( - "--token-value", "-v", required=True, help="value of the personal access token used to sign into the server" - ) + parser.add_argument("--token-name", "-p", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server") parser.add_argument( "--logging-level", "-l", diff --git a/samples/set_refresh_schedule.py b/samples/set_refresh_schedule.py index 9b3dbc236..56fd12e62 100644 --- a/samples/set_refresh_schedule.py +++ b/samples/set_refresh_schedule.py @@ -15,14 +15,10 @@ def usage(args): parser = argparse.ArgumentParser(description="Set refresh schedule for a workbook or datasource.") # Common options; please keep those in sync across all samples - parser.add_argument("--server", "-s", required=True, help="server address") + parser.add_argument("--server", "-s", help="server address") parser.add_argument("--site", "-S", help="site name") - parser.add_argument( - "--token-name", "-p", required=True, help="name of the personal access token used to sign into the server" - ) - parser.add_argument( - "--token-value", "-v", required=True, help="value of the personal access token used to sign into the server" - ) + parser.add_argument("--token-name", "-p", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server") parser.add_argument( "--logging-level", "-l", diff --git a/samples/smoke_test.py b/samples/smoke_test.py index f2dad1048..b23eacdb8 100644 --- a/samples/smoke_test.py +++ b/samples/smoke_test.py @@ -1,8 +1,16 @@ # This sample verifies that tableau server client is installed # and you can run it. It also shows the version of the client. +import logging import tableauserverclient as TSC + +logger = logging.getLogger("Sample") +logger.setLevel(logging.DEBUG) +logger.addHandler(logging.StreamHandler()) + + server = TSC.Server("Fake-Server-Url", use_server_version=False) print("Client details:") -print(TSC.server.endpoint.Endpoint._make_common_headers("fake-token", "any-content")) +logger.info(server.server_address) +logger.debug(TSC.server.endpoint.Endpoint.set_user_agent({})) diff --git a/samples/update_connection.py b/samples/update_connection.py index e27b4477f..4af6592bc 100644 --- a/samples/update_connection.py +++ b/samples/update_connection.py @@ -13,14 +13,10 @@ def main(): parser = argparse.ArgumentParser(description="Update a connection on a datasource or workbook to embed credentials") # Common options; please keep those in sync across all samples - parser.add_argument("--server", "-s", required=True, help="server address") + parser.add_argument("--server", "-s", help="server address") parser.add_argument("--site", "-S", help="site name") - parser.add_argument( - "--token-name", "-p", required=True, help="name of the personal access token used to sign into the server" - ) - parser.add_argument( - "--token-value", "-v", required=True, help="value of the personal access token used to sign into the server" - ) + parser.add_argument("--token-name", "-p", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server") parser.add_argument( "--logging-level", "-l", diff --git a/samples/update_datasource_data.py b/samples/update_datasource_data.py index 41f42ee74..f6bc92022 100644 --- a/samples/update_datasource_data.py +++ b/samples/update_datasource_data.py @@ -25,14 +25,10 @@ def main(): description="Delete the `Europe` region from a published `World Indicators` datasource." ) # Common options; please keep those in sync across all samples - parser.add_argument("--server", "-s", required=True, help="server address") + parser.add_argument("--server", "-s", help="server address") parser.add_argument("--site", "-S", help="site name") - parser.add_argument( - "--token-name", "-p", required=True, help="name of the personal access token used to sign into the server" - ) - parser.add_argument( - "--token-value", "-v", required=True, help="value of the personal access token used to sign into the server" - ) + parser.add_argument("--token-name", "-p", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server") parser.add_argument( "--logging-level", "-l", diff --git a/tableauserverclient/config.py b/tableauserverclient/config.py new file mode 100644 index 000000000..67a77f479 --- /dev/null +++ b/tableauserverclient/config.py @@ -0,0 +1,13 @@ +# TODO: check for env variables, else set default values + +ALLOWED_FILE_EXTENSIONS = ["tds", "tdsx", "tde", "hyper", "parquet"] + +BYTES_PER_MB = 1024 * 1024 + +# For when a datasource is over 64MB, break it into 5MB(standard chunk size) chunks +CHUNK_SIZE_MB = 5 * 10 # 5MB felt too slow, upped it to 50 + +DELAY_SLEEP_SECONDS = 10 + +# The maximum size of a file that can be published in a single request is 64MB +FILESIZE_LIMIT_MB = 64 diff --git a/tableauserverclient/datetime_helpers.py b/tableauserverclient/datetime_helpers.py index 0d968428d..00f62faf8 100644 --- a/tableauserverclient/datetime_helpers.py +++ b/tableauserverclient/datetime_helpers.py @@ -5,6 +5,10 @@ HOUR = datetime.timedelta(hours=1) +def timestamp(): + return datetime.datetime.now().strftime("%H:%M:%S") + + # This class is a concrete implementation of the abstract base class tzinfo # docs: https://docs.python.org/2.3/lib/datetime-tzinfo.html class UTC(datetime.tzinfo): diff --git a/tableauserverclient/helpers/logging.py b/tableauserverclient/helpers/logging.py new file mode 100644 index 000000000..414d85786 --- /dev/null +++ b/tableauserverclient/helpers/logging.py @@ -0,0 +1,6 @@ +import logging + +# TODO change: this defaults to logging *everything* to stdout +logger = logging.getLogger("TSC") +logger.setLevel(logging.DEBUG) +logger.addHandler(logging.StreamHandler()) diff --git a/tableauserverclient/models/connection_item.py b/tableauserverclient/models/connection_item.py index 4ed06b831..29ffd2700 100644 --- a/tableauserverclient/models/connection_item.py +++ b/tableauserverclient/models/connection_item.py @@ -5,6 +5,7 @@ from .connection_credentials import ConnectionCredentials from .property_decorators import property_is_boolean +from tableauserverclient.helpers.logging import logger class ConnectionItem(object): @@ -46,7 +47,6 @@ def query_tagging(self) -> Optional[bool]: def query_tagging(self, value: Optional[bool]): # if connection type = hyper, Snowflake, or Teradata, we can't change this value: it is always true if self._connection_type in ["hyper", "snowflake", "teradata"]: - logger = logging.getLogger("tableauserverclient.models.connection_item") logger.debug( "Cannot update value: Query tagging is always enabled for {} connections".format(self._connection_type) ) diff --git a/tableauserverclient/models/favorites_item.py b/tableauserverclient/models/favorites_item.py index f075d1fc3..987623404 100644 --- a/tableauserverclient/models/favorites_item.py +++ b/tableauserverclient/models/favorites_item.py @@ -11,8 +11,8 @@ from .workbook_item import WorkbookItem from typing import Dict, List -logger = logging.getLogger("tableau.models.favorites_item") - +from tableauserverclient.helpers.logging import logger +from typing import Dict, List, Union FavoriteType = Dict[ str, diff --git a/tableauserverclient/models/fileupload_item.py b/tableauserverclient/models/fileupload_item.py index 7848b94cf..e9bdd25b2 100644 --- a/tableauserverclient/models/fileupload_item.py +++ b/tableauserverclient/models/fileupload_item.py @@ -11,8 +11,8 @@ def upload_session_id(self): return self._upload_session_id @property - def file_size(self): - return self._file_size + def file_size(self) -> int: + return int(self._file_size) @classmethod def from_response(cls, resp, ns): diff --git a/tableauserverclient/models/permissions_item.py b/tableauserverclient/models/permissions_item.py index 3bdc63092..1602b077f 100644 --- a/tableauserverclient/models/permissions_item.py +++ b/tableauserverclient/models/permissions_item.py @@ -9,7 +9,7 @@ from .reference_item import ResourceReference from .user_item import UserItem -logger = logging.getLogger("tableau.models.permissions_item") +from tableauserverclient.helpers.logging import logger class Permission: diff --git a/tableauserverclient/models/server_info_item.py b/tableauserverclient/models/server_info_item.py index 5f9395880..b180665dd 100644 --- a/tableauserverclient/models/server_info_item.py +++ b/tableauserverclient/models/server_info_item.py @@ -3,6 +3,7 @@ import xml from defusedxml.ElementTree import fromstring +from tableauserverclient.helpers.logging import logger class ServerInfoItem(object): @@ -36,7 +37,6 @@ def rest_api_version(self): @classmethod def from_response(cls, resp, ns): - logger = logging.getLogger("TSC.ServerInfo") try: parsed_response = fromstring(resp) except xml.etree.ElementTree.ParseError as error: diff --git a/tableauserverclient/server/__init__.py b/tableauserverclient/server/__init__.py index bcea2604e..5abe19446 100644 --- a/tableauserverclient/server/__init__.py +++ b/tableauserverclient/server/__init__.py @@ -10,9 +10,7 @@ from .filter import Filter from .sort import Sort -from ..models import * from .endpoint import * from .server import Server from .pager import Pager -from .exceptions import NotSignedInError -from ..helpers import * +from .endpoint.exceptions import NotSignedInError diff --git a/tableauserverclient/server/endpoint/__init__.py b/tableauserverclient/server/endpoint/__init__.py index e8e1bc0f9..c018d8334 100644 --- a/tableauserverclient/server/endpoint/__init__.py +++ b/tableauserverclient/server/endpoint/__init__.py @@ -5,11 +5,7 @@ from .databases_endpoint import Databases from .datasources_endpoint import Datasources from .endpoint import Endpoint, QuerysetEndpoint -from .exceptions import ( - ServerResponseError, - MissingRequiredFieldError, - ServerInfoEndpointNotFoundError, -) +from .exceptions import ServerResponseError, MissingRequiredFieldError from .favorites_endpoint import Favorites from .fileuploads_endpoint import Fileuploads from .flow_runs_endpoint import FlowRuns diff --git a/tableauserverclient/server/endpoint/auth_endpoint.py b/tableauserverclient/server/endpoint/auth_endpoint.py index 68d75eaa8..6f1ddc35e 100644 --- a/tableauserverclient/server/endpoint/auth_endpoint.py +++ b/tableauserverclient/server/endpoint/auth_endpoint.py @@ -6,7 +6,7 @@ from .exceptions import ServerResponseError from ..request_factory import RequestFactory -logger = logging.getLogger("tableau.endpoint.auth") +from tableauserverclient.helpers.logging import logger class Auth(Endpoint): diff --git a/tableauserverclient/server/endpoint/custom_views_endpoint.py b/tableauserverclient/server/endpoint/custom_views_endpoint.py index 778cafecc..119580609 100644 --- a/tableauserverclient/server/endpoint/custom_views_endpoint.py +++ b/tableauserverclient/server/endpoint/custom_views_endpoint.py @@ -6,7 +6,7 @@ from tableauserverclient.models import CustomViewItem, PaginationItem from tableauserverclient.server import RequestFactory, RequestOptions, ImageRequestOptions -logger = logging.getLogger("tableau.endpoint.custom_views") +from tableauserverclient.helpers.logging import logger """ Get a list of custom views on a site diff --git a/tableauserverclient/server/endpoint/data_acceleration_report_endpoint.py b/tableauserverclient/server/endpoint/data_acceleration_report_endpoint.py index 28e5495c5..256a6e766 100644 --- a/tableauserverclient/server/endpoint/data_acceleration_report_endpoint.py +++ b/tableauserverclient/server/endpoint/data_acceleration_report_endpoint.py @@ -5,7 +5,7 @@ from .permissions_endpoint import _PermissionsEndpoint from tableauserverclient.models import DataAccelerationReportItem -logger = logging.getLogger("tableau.endpoint.data_acceleration_report") +from tableauserverclient.helpers.logging import logger class DataAccelerationReport(Endpoint): diff --git a/tableauserverclient/server/endpoint/data_alert_endpoint.py b/tableauserverclient/server/endpoint/data_alert_endpoint.py index 5af4e0464..fd02d2e4a 100644 --- a/tableauserverclient/server/endpoint/data_alert_endpoint.py +++ b/tableauserverclient/server/endpoint/data_alert_endpoint.py @@ -5,7 +5,7 @@ from tableauserverclient.server import RequestFactory from tableauserverclient.models import DataAlertItem, PaginationItem, UserItem -logger = logging.getLogger("tableau.endpoint.dataAlerts") +from tableauserverclient.helpers.logging import logger from typing import List, Optional, TYPE_CHECKING, Tuple, Union diff --git a/tableauserverclient/server/endpoint/databases_endpoint.py b/tableauserverclient/server/endpoint/databases_endpoint.py index 2522ef53e..125996277 100644 --- a/tableauserverclient/server/endpoint/databases_endpoint.py +++ b/tableauserverclient/server/endpoint/databases_endpoint.py @@ -8,7 +8,7 @@ from tableauserverclient.server import RequestFactory from tableauserverclient.models import DatabaseItem, TableItem, PaginationItem, Resource -logger = logging.getLogger("tableau.endpoint.databases") +from tableauserverclient.helpers.logging import logger class Databases(Endpoint): diff --git a/tableauserverclient/server/endpoint/datasources_endpoint.py b/tableauserverclient/server/endpoint/datasources_endpoint.py index 0c5b8ba61..c60f8f919 100644 --- a/tableauserverclient/server/endpoint/datasources_endpoint.py +++ b/tableauserverclient/server/endpoint/datasources_endpoint.py @@ -1,7 +1,6 @@ import cgi import copy import json -import logging import io import os @@ -20,13 +19,14 @@ from .permissions_endpoint import _PermissionsEndpoint from .resource_tagger import _ResourceTagger -from tableauserverclient.server import RequestFactory, RequestOptions +from tableauserverclient.config import ALLOWED_FILE_EXTENSIONS, FILESIZE_LIMIT_MB, BYTES_PER_MB, CHUNK_SIZE_MB from tableauserverclient.filesys_helpers import ( - to_filename, make_download_path, get_file_type, get_file_object_size, + to_filename, ) +from tableauserverclient.helpers.logging import logger from tableauserverclient.models import ( ConnectionCredentials, ConnectionItem, @@ -35,6 +35,7 @@ RevisionItem, PaginationItem, ) +from tableauserverclient.server import RequestFactory, RequestOptions io_types = (io.BytesIO, io.BufferedReader) io_types_r = (io.BytesIO, io.BufferedReader) @@ -44,13 +45,6 @@ FileObject = Union[io.BufferedReader, io.BytesIO] PathOrFile = Union[FilePath, FileObject] -# The maximum size of a file that can be published in a single request is 64MB -FILESIZE_LIMIT = 1024 * 1024 * 64 # 64MB - -ALLOWED_FILE_EXTENSIONS = ["tds", "tdsx", "tde", "hyper", "parquet"] - -logger = logging.getLogger("tableau.endpoint.datasources") - FilePath = Union[str, os.PathLike] FileObjectR = Union[io.BufferedReader, io.BytesIO] FileObjectW = Union[io.BufferedWriter, io.BytesIO] @@ -162,12 +156,20 @@ def update(self, datasource_item: DatasourceItem) -> DatasourceItem: # Update datasource connections @api(version="2.3") - def update_connection(self, datasource_item: DatasourceItem, connection_item: ConnectionItem) -> ConnectionItem: + def update_connection( + self, datasource_item: DatasourceItem, connection_item: ConnectionItem + ) -> Optional[ConnectionItem]: url = "{0}/{1}/connections/{2}".format(self.baseurl, datasource_item.id, connection_item.id) update_req = RequestFactory.Connection.update_req(connection_item) server_response = self.put_request(url, update_req) - connection = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace)[0] + connections = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace) + if not connections: + return None + + if len(connections) > 1: + logger.debug("Multiple connections returned ({0})".format(len(connections))) + connection = list(filter(lambda x: x.id == connection_item.id, connections))[0] logger.info( "Updated datasource item (ID: {0} & connection item {1}".format(datasource_item.id, connection_item.id) @@ -220,7 +222,7 @@ def publish( filename = os.path.basename(file) file_extension = os.path.splitext(filename)[1][1:] file_size = os.path.getsize(file) - + logger.debug("Publishing file `{}`, size `{}`".format(filename, file_size)) # If name is not defined, grab the name from the file to publish if not datasource_item.name: datasource_item.name = os.path.splitext(filename)[0] @@ -261,8 +263,12 @@ def publish( url += "&{0}=true".format("asJob") # Determine if chunking is required (64MB is the limit for single upload method) - if file_size >= FILESIZE_LIMIT: - logger.info("Publishing {0} to server with chunking method (datasource over 64MB)".format(filename)) + if file_size >= FILESIZE_LIMIT_MB * BYTES_PER_MB: + logger.info( + "Publishing {} to server with chunking method (datasource over {}MB, chunk size {}MB)".format( + filename, FILESIZE_LIMIT_MB, CHUNK_SIZE_MB + ) + ) upload_session_id = self.parent_srv.fileuploads.upload(file) url = "{0}&uploadSessionId={1}".format(url, upload_session_id) xml_request, content_type = RequestFactory.Datasource.publish_req_chunked( diff --git a/tableauserverclient/server/endpoint/default_permissions_endpoint.py b/tableauserverclient/server/endpoint/default_permissions_endpoint.py index b0d16efaf..19112d713 100644 --- a/tableauserverclient/server/endpoint/default_permissions_endpoint.py +++ b/tableauserverclient/server/endpoint/default_permissions_endpoint.py @@ -10,7 +10,7 @@ from ..server import Server from ..request_options import RequestOptions -logger = logging.getLogger(__name__) +from tableauserverclient.helpers.logging import logger # these are the only two items that can hold default permissions for another type BaseItem = Union[DatabaseItem, ProjectItem] diff --git a/tableauserverclient/server/endpoint/dqw_endpoint.py b/tableauserverclient/server/endpoint/dqw_endpoint.py index 96cb7c5f9..5296523ee 100644 --- a/tableauserverclient/server/endpoint/dqw_endpoint.py +++ b/tableauserverclient/server/endpoint/dqw_endpoint.py @@ -5,7 +5,7 @@ from tableauserverclient.server import RequestFactory from tableauserverclient.models import DQWItem -logger = logging.getLogger(__name__) +from tableauserverclient.helpers.logging import logger class _DataQualityWarningEndpoint(Endpoint): diff --git a/tableauserverclient/server/endpoint/endpoint.py b/tableauserverclient/server/endpoint/endpoint.py index 9c933c9dd..c11a3fb27 100644 --- a/tableauserverclient/server/endpoint/endpoint.py +++ b/tableauserverclient/server/endpoint/endpoint.py @@ -1,24 +1,31 @@ +from threading import Thread +from time import sleep +from tableauserverclient import datetime_helpers as datetime + import requests -import logging from packaging.version import Version from functools import wraps from xml.etree.ElementTree import ParseError -from typing import Any, Callable, Dict, Optional, TYPE_CHECKING +from typing import Any, Callable, Dict, Optional, TYPE_CHECKING, Union from .exceptions import ( ServerResponseError, InternalServerError, NonXMLResponseError, - EndpointUnavailableError, + NotSignedInError, ) +from ..exceptions import EndpointUnavailableError + from tableauserverclient.server.query import QuerySet from tableauserverclient import helpers, get_versions +from tableauserverclient.helpers.logging import logger +from tableauserverclient.config import DELAY_SLEEP_SECONDS + if TYPE_CHECKING: from ..server import Server from requests import Response -logger = logging.getLogger("tableau.endpoint") Success_codes = [200, 201, 202, 204] @@ -34,6 +41,8 @@ class Endpoint(object): def __init__(self, parent_srv: "Server"): self.parent_srv = parent_srv + async_response = None + @staticmethod def set_parameters(http_options, auth_token, content, content_type, parameters) -> Dict[str, Any]: parameters = parameters or {} @@ -53,6 +62,8 @@ def set_parameters(http_options, auth_token, content, content_type, parameters) @staticmethod def set_user_agent(parameters): + if "headers" not in parameters: + parameters["headers"] = {} if USER_AGENT_HEADER not in parameters["headers"]: if USER_AGENT_HEADER in parameters: parameters["headers"][USER_AGENT_HEADER] = parameters[USER_AGENT_HEADER] @@ -65,6 +76,59 @@ def set_user_agent(parameters): # return explicitly for testing only return parameters + def _blocking_request(self, method, url, parameters={}) -> Optional["Response"]: + self.async_response = None + response = None + logger.debug("[{}] Begin blocking request to {}".format(datetime.timestamp(), url)) + try: + response = method(url, **parameters) + self.async_response = response + logger.debug("[{}] Call finished".format(datetime.timestamp())) + except Exception as e: + logger.debug("Error making request to server: {}".format(e)) + self.async_response = e + finally: + if response and not self.async_response: + logger.debug("Request response not saved") + return None + logger.debug("[{}] Request complete".format(datetime.timestamp())) + return self.async_response + + def send_request_while_show_progress_threaded( + self, method, url, parameters={}, request_timeout=0 + ) -> Optional["Response"]: + try: + request_thread = Thread(target=self._blocking_request, args=(method, url, parameters)) + request_thread.async_response = -1 # type:ignore # this is an invented attribute for thread comms + request_thread.start() + except Exception as e: + logger.debug("Error starting server request on separate thread: {}".format(e)) + return None + seconds = 0 + minutes = 0 + sleep(1) + if self.async_response != -1: + # a quick return for any immediate responses + return self.async_response + while self.async_response == -1 and (request_timeout == 0 or seconds < request_timeout): + self.log_wait_time_then_sleep(minutes, seconds, url) + seconds = seconds + DELAY_SLEEP_SECONDS + if seconds >= 60: + seconds = 0 + minutes = minutes + 1 + return self.async_response + + def log_wait_time_then_sleep(self, minutes, seconds, url): + logger.debug("{} Waiting....".format(datetime.timestamp())) + if seconds >= 60: # detailed log message ~every minute + if minutes % 5 == 0: + logger.info( + "[{}] Waiting ({} minutes so far) for request to {}".format(datetime.timestamp(), minutes, url) + ) + else: + logger.debug("[{}] Waiting for request to {}".format(datetime.timestamp(), url)) + sleep(DELAY_SLEEP_SECONDS) + def _make_request( self, method: Callable[..., "Response"], @@ -80,36 +144,59 @@ def _make_request( logger.debug("request method {}, url: {}".format(method.__name__, url)) if content: - redacted = helpers.strings.redact_xml(content[:1000]) + redacted = helpers.strings.redact_xml(content[:200]) + # this needs to be under a trace or something, it's a LOT # logger.debug("request content: {}".format(redacted)) - server_response = method(url, **parameters) + # a request can, for stuff like publishing, spin for ages waiting for a response. + # we need some user-facing activity so they know it's not dead. + request_timeout = self.parent_srv.http_options.get("timeout") or 0 + server_response: Optional["Response"] = self.send_request_while_show_progress_threaded( + method, url, parameters, request_timeout + ) + logger.debug("[{}] Async request returned: received {}".format(datetime.timestamp(), server_response)) + # is this blocking retry really necessary? I guess if it was just the threading messing it up? + if server_response is None: + logger.debug(server_response) + logger.debug("[{}] Async request failed: retrying".format(datetime.timestamp())) + server_response = self._blocking_request(method, url, parameters) + if server_response is None: + logger.debug("[{}] Request failed".format(datetime.timestamp())) + raise RuntimeError self._check_status(server_response, url) loggable_response = self.log_response_safely(server_response) - # logger.debug("Server response from {0}:\n\t{1}".format(url, loggable_response)) + logger.debug("Server response from {0}".format(url)) + # logger.debug("\n\t{1}".format(loggable_response)) if content_type == "application/xml": self.parent_srv._namespace.detect(server_response.content) return server_response - def _check_status(self, server_response, url: Optional[str] = None): + def _check_status(self, server_response: "Response", url: Optional[str] = None): + logger.debug("Response status: {}".format(server_response)) + if not hasattr(server_response, "status_code"): + raise EnvironmentError("Response is not a http response?") if server_response.status_code >= 500: raise InternalServerError(server_response, url) elif server_response.status_code not in Success_codes: try: + if server_response.status_code == 401: + # TODO: catch this in server.py and attempt to sign in again, in case it's a session expiry + raise NotSignedInError(server_response.content, url) + raise ServerResponseError.from_response(server_response.content, self.parent_srv.namespace, url) except ParseError: # This will happen if we get a non-success HTTP code that doesn't return an xml error object - # e.g metadata endpoints, 503 pages, totally different servers + # e.g. metadata endpoints, 503 pages, totally different servers # we convert this to a better exception and pass through the raw response body raise NonXMLResponseError(server_response.content) except Exception: # anything else re-raise here raise - def log_response_safely(self, server_response: requests.Response) -> str: + def log_response_safely(self, server_response: "Response") -> str: # Checking the content type header prevents eager evaluation of streaming requests. content_type = server_response.headers.get("Content-Type") @@ -117,7 +204,7 @@ def log_response_safely(self, server_response: requests.Response) -> str: # content-type is an octet-stream accomplishes the same goal without eagerly loading content. # This check is to determine if the response is a text response (xml or otherwise) # so that we do not attempt to log bytes and other binary data. - loggable_response = "Content type {}".format(content_type) + loggable_response = "Content type `{}`".format(content_type) if content_type == "application/octet-stream": loggable_response = "A stream of type {} [Truncated File Contents]".format(content_type) elif server_response.encoding and len(server_response.content) > 0: diff --git a/tableauserverclient/server/endpoint/exceptions.py b/tableauserverclient/server/endpoint/exceptions.py index d7b1d5ad2..9dfd38da6 100644 --- a/tableauserverclient/server/endpoint/exceptions.py +++ b/tableauserverclient/server/endpoint/exceptions.py @@ -47,11 +47,7 @@ class MissingRequiredFieldError(TableauError): pass -class ServerInfoEndpointNotFoundError(TableauError): - pass - - -class EndpointUnavailableError(TableauError): +class NotSignedInError(TableauError): pass diff --git a/tableauserverclient/server/endpoint/favorites_endpoint.py b/tableauserverclient/server/endpoint/favorites_endpoint.py index dcddca259..ac9e4b185 100644 --- a/tableauserverclient/server/endpoint/favorites_endpoint.py +++ b/tableauserverclient/server/endpoint/favorites_endpoint.py @@ -1,25 +1,22 @@ -import logging - from .endpoint import Endpoint, api from requests import Response -from tableauserverclient.server import RequestFactory, RequestOptions, Resource +from tableauserverclient.helpers.logging import logger from tableauserverclient.models import ( DatasourceItem, FavoriteItem, FlowItem, MetricItem, ProjectItem, + Resource, + TableauItem, UserItem, ViewItem, WorkbookItem, - TableauItem, ) - +from tableauserverclient.server import RequestFactory, RequestOptions from typing import Optional -logger = logging.getLogger("tableau.endpoint.favorites") - class Favorites(Endpoint): @property diff --git a/tableauserverclient/server/endpoint/fileuploads_endpoint.py b/tableauserverclient/server/endpoint/fileuploads_endpoint.py index 9a8e9560d..a0e29e508 100644 --- a/tableauserverclient/server/endpoint/fileuploads_endpoint.py +++ b/tableauserverclient/server/endpoint/fileuploads_endpoint.py @@ -1,13 +1,10 @@ -import logging - from .endpoint import Endpoint, api -from tableauserverclient.server import RequestFactory -from tableauserverclient.models import FileuploadItem +from tableauserverclient import datetime_helpers as datetime +from tableauserverclient.helpers.logging import logger -# For when a datasource is over 64MB, break it into 5MB(standard chunk size) chunks -CHUNK_SIZE = 1024 * 1024 * 5 # 5MB - -logger = logging.getLogger("tableau.endpoint.fileuploads") +from tableauserverclient.config import BYTES_PER_MB, CHUNK_SIZE_MB +from tableauserverclient.models import FileuploadItem +from tableauserverclient.server import RequestFactory class Fileuploads(Endpoint): @@ -44,7 +41,7 @@ def _read_chunks(self, file): try: while True: - chunked_content = file_content.read(CHUNK_SIZE) + chunked_content = file_content.read(CHUNK_SIZE_MB * BYTES_PER_MB) if not chunked_content: break yield chunked_content @@ -55,8 +52,12 @@ def _read_chunks(self, file): def upload(self, file): upload_id = self.initiate() for chunk in self._read_chunks(file): + logger.debug("{} processing chunk...".format(datetime.timestamp())) request, content_type = RequestFactory.Fileupload.chunk_req(chunk) + logger.debug("{} created chunk request".format(datetime.timestamp())) fileupload_item = self.append(upload_id, request, content_type) - logger.info("\tPublished {0}MB".format(fileupload_item.file_size)) + logger.info( + "\t{0} Published {1}MB".format(datetime.timestamp(), (fileupload_item.file_size / BYTES_PER_MB)) + ) logger.info("File upload finished (ID: {0})".format(upload_id)) return upload_id diff --git a/tableauserverclient/server/endpoint/flow_runs_endpoint.py b/tableauserverclient/server/endpoint/flow_runs_endpoint.py index 3bca93a7f..63b32e006 100644 --- a/tableauserverclient/server/endpoint/flow_runs_endpoint.py +++ b/tableauserverclient/server/endpoint/flow_runs_endpoint.py @@ -6,7 +6,7 @@ from tableauserverclient.models import FlowRunItem, PaginationItem from tableauserverclient.exponential_backoff import ExponentialBackoffTimer -logger = logging.getLogger("tableau.endpoint.flowruns") +from tableauserverclient.helpers.logging import logger if TYPE_CHECKING: from ..server import Server diff --git a/tableauserverclient/server/endpoint/flows_endpoint.py b/tableauserverclient/server/endpoint/flows_endpoint.py index 4d97110c4..ba8a152d7 100644 --- a/tableauserverclient/server/endpoint/flows_endpoint.py +++ b/tableauserverclient/server/endpoint/flows_endpoint.py @@ -32,13 +32,13 @@ ALLOWED_FILE_EXTENSIONS = ["tfl", "tflx"] -logger = logging.getLogger("tableau.endpoint.flows") +from tableauserverclient.helpers.logging import logger if TYPE_CHECKING: - from .. import DQWItem - from ..request_options import RequestOptions - from ...models.permissions_item import PermissionsRule - from .schedules_endpoint import AddResponse + from tableauserverclient.models import DQWItem + from tableauserverclient.models.permissions_item import PermissionsRule + from tableauserverclient.server.request_options import RequestOptions + from tableauserverclient.server.endpoint.schedules_endpoint import AddResponse FilePath = Union[str, os.PathLike] diff --git a/tableauserverclient/server/endpoint/groups_endpoint.py b/tableauserverclient/server/endpoint/groups_endpoint.py index ba5b6649b..ad3828568 100644 --- a/tableauserverclient/server/endpoint/groups_endpoint.py +++ b/tableauserverclient/server/endpoint/groups_endpoint.py @@ -6,7 +6,7 @@ from tableauserverclient.models import GroupItem, UserItem, PaginationItem, JobItem from ..pager import Pager -logger = logging.getLogger("tableau.endpoint.groups") +from tableauserverclient.helpers.logging import logger from typing import List, Optional, TYPE_CHECKING, Tuple, Union diff --git a/tableauserverclient/server/endpoint/jobs_endpoint.py b/tableauserverclient/server/endpoint/jobs_endpoint.py index dd210d990..d0b865e21 100644 --- a/tableauserverclient/server/endpoint/jobs_endpoint.py +++ b/tableauserverclient/server/endpoint/jobs_endpoint.py @@ -6,7 +6,7 @@ from ..request_options import RequestOptionsBase from tableauserverclient.exponential_backoff import ExponentialBackoffTimer -logger = logging.getLogger("tableau.endpoint.jobs") +from tableauserverclient.helpers.logging import logger from typing import List, Optional, Tuple, Union diff --git a/tableauserverclient/server/endpoint/metadata_endpoint.py b/tableauserverclient/server/endpoint/metadata_endpoint.py index 06339fa79..39146d062 100644 --- a/tableauserverclient/server/endpoint/metadata_endpoint.py +++ b/tableauserverclient/server/endpoint/metadata_endpoint.py @@ -4,7 +4,7 @@ from .endpoint import Endpoint, api from .exceptions import GraphQLError, InvalidGraphQLQuery -logger = logging.getLogger("tableau.endpoint.metadata") +from tableauserverclient.helpers.logging import logger def is_valid_paged_query(parsed_query): diff --git a/tableauserverclient/server/endpoint/metrics_endpoint.py b/tableauserverclient/server/endpoint/metrics_endpoint.py index 8443726cd..a0e984475 100644 --- a/tableauserverclient/server/endpoint/metrics_endpoint.py +++ b/tableauserverclient/server/endpoint/metrics_endpoint.py @@ -15,7 +15,7 @@ from ...server import Server -logger = logging.getLogger("tableau.endpoint.metrics") +from tableauserverclient.helpers.logging import logger class Metrics(QuerysetEndpoint): diff --git a/tableauserverclient/server/endpoint/permissions_endpoint.py b/tableauserverclient/server/endpoint/permissions_endpoint.py index e50e32945..4433625f2 100644 --- a/tableauserverclient/server/endpoint/permissions_endpoint.py +++ b/tableauserverclient/server/endpoint/permissions_endpoint.py @@ -8,7 +8,7 @@ from typing import Callable, TYPE_CHECKING, List, Optional, Union -logger = logging.getLogger(__name__) +from tableauserverclient.helpers.logging import logger if TYPE_CHECKING: from ..server import Server diff --git a/tableauserverclient/server/endpoint/projects_endpoint.py b/tableauserverclient/server/endpoint/projects_endpoint.py index 440940606..510f1ff3d 100644 --- a/tableauserverclient/server/endpoint/projects_endpoint.py +++ b/tableauserverclient/server/endpoint/projects_endpoint.py @@ -13,7 +13,7 @@ from ..server import Server from ..request_options import RequestOptions -logger = logging.getLogger("tableau.endpoint.projects") +from tableauserverclient.helpers.logging import logger class Projects(QuerysetEndpoint): diff --git a/tableauserverclient/server/endpoint/resource_tagger.py b/tableauserverclient/server/endpoint/resource_tagger.py index 18c38798e..8177bd733 100644 --- a/tableauserverclient/server/endpoint/resource_tagger.py +++ b/tableauserverclient/server/endpoint/resource_tagger.py @@ -1,13 +1,13 @@ import copy -import logging import urllib.parse from .endpoint import Endpoint -from .exceptions import EndpointUnavailableError, ServerResponseError +from .exceptions import ServerResponseError +from ..exceptions import EndpointUnavailableError from tableauserverclient.server import RequestFactory from tableauserverclient.models import TagItem -logger = logging.getLogger("tableau.endpoint.resource_tagger") +from tableauserverclient.helpers.logging import logger class _ResourceTagger(Endpoint): diff --git a/tableauserverclient/server/endpoint/schedules_endpoint.py b/tableauserverclient/server/endpoint/schedules_endpoint.py index 7cca1f5d5..cfaee3324 100644 --- a/tableauserverclient/server/endpoint/schedules_endpoint.py +++ b/tableauserverclient/server/endpoint/schedules_endpoint.py @@ -9,7 +9,8 @@ from tableauserverclient.server import RequestFactory from tableauserverclient.models import PaginationItem, ScheduleItem, TaskItem -logger = logging.getLogger("tableau.endpoint.schedules") +from tableauserverclient.helpers.logging import logger + AddResponse = namedtuple("AddResponse", ("result", "error", "warnings", "task_created")) OK = AddResponse(result=True, error=None, warnings=None, task_created=None) diff --git a/tableauserverclient/server/endpoint/server_info_endpoint.py b/tableauserverclient/server/endpoint/server_info_endpoint.py index b396a1f87..26aaf2910 100644 --- a/tableauserverclient/server/endpoint/server_info_endpoint.py +++ b/tableauserverclient/server/endpoint/server_info_endpoint.py @@ -1,15 +1,13 @@ import logging from .endpoint import Endpoint, api -from .exceptions import ( - ServerResponseError, +from .exceptions import ServerResponseError +from ..exceptions import ( ServerInfoEndpointNotFoundError, EndpointUnavailableError, ) from tableauserverclient.models import ServerInfoItem -logger = logging.getLogger("tableau.endpoint.server_info") - class ServerInfo(Endpoint): def __init__(self, server): diff --git a/tableauserverclient/server/endpoint/sites_endpoint.py b/tableauserverclient/server/endpoint/sites_endpoint.py index a4c765484..dfec49ae1 100644 --- a/tableauserverclient/server/endpoint/sites_endpoint.py +++ b/tableauserverclient/server/endpoint/sites_endpoint.py @@ -6,7 +6,7 @@ from tableauserverclient.server import RequestFactory from tableauserverclient.models import SiteItem, PaginationItem -logger = logging.getLogger("tableau.endpoint.sites") +from tableauserverclient.helpers.logging import logger from typing import TYPE_CHECKING, List, Optional, Tuple diff --git a/tableauserverclient/server/endpoint/subscriptions_endpoint.py b/tableauserverclient/server/endpoint/subscriptions_endpoint.py index a81a2fbf0..a9f2e7bf5 100644 --- a/tableauserverclient/server/endpoint/subscriptions_endpoint.py +++ b/tableauserverclient/server/endpoint/subscriptions_endpoint.py @@ -5,7 +5,7 @@ from tableauserverclient.server import RequestFactory from tableauserverclient.models import SubscriptionItem, PaginationItem -logger = logging.getLogger("tableau.endpoint.subscriptions") +from tableauserverclient.helpers.logging import logger from typing import List, Optional, TYPE_CHECKING, Tuple diff --git a/tableauserverclient/server/endpoint/tables_endpoint.py b/tableauserverclient/server/endpoint/tables_endpoint.py index e51f885d7..dfb2e6d7c 100644 --- a/tableauserverclient/server/endpoint/tables_endpoint.py +++ b/tableauserverclient/server/endpoint/tables_endpoint.py @@ -8,7 +8,7 @@ from tableauserverclient.models import TableItem, ColumnItem, PaginationItem from ..pager import Pager -logger = logging.getLogger("tableau.endpoint.tables") +from tableauserverclient.helpers.logging import logger class Tables(Endpoint): diff --git a/tableauserverclient/server/endpoint/tasks_endpoint.py b/tableauserverclient/server/endpoint/tasks_endpoint.py index b903ac634..ad1702f58 100644 --- a/tableauserverclient/server/endpoint/tasks_endpoint.py +++ b/tableauserverclient/server/endpoint/tasks_endpoint.py @@ -5,7 +5,7 @@ from tableauserverclient.models import TaskItem, PaginationItem from tableauserverclient.server import RequestFactory -logger = logging.getLogger("tableau.endpoint.tasks") +from tableauserverclient.helpers.logging import logger class Tasks(Endpoint): diff --git a/tableauserverclient/server/endpoint/users_endpoint.py b/tableauserverclient/server/endpoint/users_endpoint.py index 5a9c74619..e8c5cc962 100644 --- a/tableauserverclient/server/endpoint/users_endpoint.py +++ b/tableauserverclient/server/endpoint/users_endpoint.py @@ -8,7 +8,7 @@ from tableauserverclient.models import UserItem, WorkbookItem, PaginationItem, GroupItem from ..pager import Pager -logger = logging.getLogger("tableau.endpoint.users") +from tableauserverclient.helpers.logging import logger class Users(QuerysetEndpoint): diff --git a/tableauserverclient/server/endpoint/views_endpoint.py b/tableauserverclient/server/endpoint/views_endpoint.py index c060298ba..9c4b90657 100644 --- a/tableauserverclient/server/endpoint/views_endpoint.py +++ b/tableauserverclient/server/endpoint/views_endpoint.py @@ -7,7 +7,7 @@ from .resource_tagger import _ResourceTagger from tableauserverclient.models import ViewItem, PaginationItem -logger = logging.getLogger("tableau.endpoint.views") +from tableauserverclient.helpers.logging import logger from typing import Iterator, List, Optional, Tuple, TYPE_CHECKING diff --git a/tableauserverclient/server/endpoint/webhooks_endpoint.py b/tableauserverclient/server/endpoint/webhooks_endpoint.py index 69a958988..597f9c425 100644 --- a/tableauserverclient/server/endpoint/webhooks_endpoint.py +++ b/tableauserverclient/server/endpoint/webhooks_endpoint.py @@ -4,7 +4,7 @@ from tableauserverclient.server import RequestFactory from tableauserverclient.models import WebhookItem, PaginationItem -logger = logging.getLogger("tableau.endpoint.webhooks") +from tableauserverclient.helpers.logging import logger from typing import List, Optional, TYPE_CHECKING, Tuple diff --git a/tableauserverclient/server/endpoint/workbooks_endpoint.py b/tableauserverclient/server/endpoint/workbooks_endpoint.py index 5e2784b55..a73b0f0d5 100644 --- a/tableauserverclient/server/endpoint/workbooks_endpoint.py +++ b/tableauserverclient/server/endpoint/workbooks_endpoint.py @@ -44,7 +44,8 @@ ALLOWED_FILE_EXTENSIONS = ["twb", "twbx"] -logger = logging.getLogger("tableau.endpoint.workbooks") +from tableauserverclient.helpers.logging import logger + FilePath = Union[str, os.PathLike] FileObject = Union[io.BufferedReader, io.BytesIO] FileObjectR = Union[io.BufferedReader, io.BytesIO] diff --git a/tableauserverclient/server/exceptions.py b/tableauserverclient/server/exceptions.py index 09d3d0541..6c9bbcefc 100644 --- a/tableauserverclient/server/exceptions.py +++ b/tableauserverclient/server/exceptions.py @@ -1,2 +1,9 @@ -class NotSignedInError(Exception): +# These errors can be thrown without even talking to Tableau Server + + +class ServerInfoEndpointNotFoundError(Exception): + pass + + +class EndpointUnavailableError(Exception): pass diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index 4140794b4..4bd30bb2c 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -197,6 +197,8 @@ def update_req(self, datasource_item): if datasource_item.owner_id: owner_element = ET.SubElement(datasource_element, "owner") owner_element.attrib["id"] = datasource_item.owner_id + if datasource_item.use_remote_query_agent is not None: + datasource_element.attrib["useRemoteQueryAgent"] = str(datasource_item.use_remote_query_agent).lower() datasource_element.attrib["isCertified"] = str(datasource_item.certified).lower() diff --git a/tableauserverclient/server/request_options.py b/tableauserverclient/server/request_options.py index 299b9db2f..1ee18e9df 100644 --- a/tableauserverclient/server/request_options.py +++ b/tableauserverclient/server/request_options.py @@ -1,7 +1,7 @@ from tableauserverclient.models.property_decorators import property_is_int import logging -logger = logging.getLogger("tableau.request_options") +from tableauserverclient.helpers.logging import logger class RequestOptionsBase(object): diff --git a/tableauserverclient/server/server.py b/tableauserverclient/server/server.py index 887b9de6d..ee23789b1 100644 --- a/tableauserverclient/server/server.py +++ b/tableauserverclient/server/server.py @@ -1,4 +1,4 @@ -import logging +from tableauserverclient.helpers.logging import logger import requests import urllib3 @@ -34,11 +34,11 @@ Metrics, Endpoint, ) -from .endpoint.exceptions import ( +from .exceptions import ( ServerInfoEndpointNotFoundError, EndpointUnavailableError, ) -from .exceptions import NotSignedInError +from .endpoint.exceptions import NotSignedInError from ..namespace import Namespace @@ -99,8 +99,6 @@ def __init__(self, server_address, use_server_version=False, http_options=None, self.metrics = Metrics(self) self.custom_views = CustomViews(self) - self.logger = logging.getLogger("TSC.server") - self._session = self._session_factory() self._http_options = dict() # must set this before making a server call if http_options: @@ -114,7 +112,8 @@ def __init__(self, server_address, use_server_version=False, http_options=None, def validate_connection_settings(self): try: - Endpoint(self).set_parameters(self._http_options, None, None, None, None) + params = Endpoint(self).set_parameters(self._http_options, None, None, None, None) + Endpoint.set_user_agent(params) if not self._server_address.startswith("http://") and not self._server_address.startswith("https://"): self._server_address = "http://" + self._server_address self._session.prepare_request(requests.Request("GET", url=self._server_address, params=self._http_options)) @@ -156,8 +155,8 @@ def _get_legacy_version(self): try: info_xml = fromstring(response.content) except ParseError as parseError: - self.logger.info(parseError) - self.logger.info("Could not read server version info. The server may not be running or configured.") + logger.info(parseError) + logger.info("Could not read server version info. The server may not be running or configured.") return self.version prod_version = info_xml.find(".//product_version").text version = _PRODUCT_TO_REST_VERSION.get(prod_version, minimum_supported_server_version) @@ -168,15 +167,15 @@ def _determine_highest_version(self): old_version = self.version version = self.server_info.get().rest_api_version except ServerInfoEndpointNotFoundError as e: - self.logger.info("Could not get version info from server: {}{}".format(e.__class__, e)) + logger.info("Could not get version info from server: {}{}".format(e.__class__, e)) version = self._get_legacy_version() except EndpointUnavailableError as e: - self.logger.info("Could not get version info from server: {}{}".format(e.__class__, e)) + logger.info("Could not get version info from server: {}{}".format(e.__class__, e)) version = self._get_legacy_version() except Exception as e: - self.logger.info("Could not get version info from server: {}{}".format(e.__class__, e)) + logger.info("Could not get version info from server: {}{}".format(e.__class__, e)) version = None - self.logger.info("versions: {}, {}".format(version, old_version)) + logger.info("versions: {}, {}".format(version, old_version)) return version or old_version def use_server_version(self): @@ -184,7 +183,7 @@ def use_server_version(self): def use_highest_version(self): self.use_server_version() - self.logger.info("use use_server_version instead", DeprecationWarning) + logger.info("use use_server_version instead", DeprecationWarning) def check_at_least_version(self, target: str): server_version = Version(self.version or "2.4") diff --git a/test/test_auth.py b/test/test_auth.py index 40255f627..eaf13481e 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -63,7 +63,7 @@ def test_sign_in_error(self): with requests_mock.mock() as m: m.post(self.baseurl + "/signin", text=response_xml, status_code=401) tableau_auth = TSC.TableauAuth("testuser", "wrongpassword") - self.assertRaises(TSC.ServerResponseError, self.server.auth.sign_in, tableau_auth) + self.assertRaises(TSC.NotSignedInError, self.server.auth.sign_in, tableau_auth) def test_sign_in_invalid_token(self): with open(SIGN_IN_ERROR_XML, "rb") as f: @@ -71,7 +71,7 @@ def test_sign_in_invalid_token(self): with requests_mock.mock() as m: m.post(self.baseurl + "/signin", text=response_xml, status_code=401) tableau_auth = TSC.PersonalAccessTokenAuth(token_name="mytoken", personal_access_token="invalid") - self.assertRaises(TSC.ServerResponseError, self.server.auth.sign_in, tableau_auth) + self.assertRaises(TSC.NotSignedInError, self.server.auth.sign_in, tableau_auth) def test_sign_in_without_auth(self): with open(SIGN_IN_ERROR_XML, "rb") as f: @@ -79,7 +79,7 @@ def test_sign_in_without_auth(self): with requests_mock.mock() as m: m.post(self.baseurl + "/signin", text=response_xml, status_code=401) tableau_auth = TSC.TableauAuth("", "") - self.assertRaises(TSC.ServerResponseError, self.server.auth.sign_in, tableau_auth) + self.assertRaises(TSC.NotSignedInError, self.server.auth.sign_in, tableau_auth) def test_sign_out(self): with open(SIGN_IN_XML, "rb") as f: diff --git a/test/test_datasource.py b/test/test_datasource.py index 4f3529762..730e382da 100644 --- a/test/test_datasource.py +++ b/test/test_datasource.py @@ -145,9 +145,9 @@ def test_update_copy_fields(self) -> None: def test_update_tags(self) -> None: add_tags_xml, update_xml = read_xml_assets(ADD_TAGS_XML, UPDATE_XML) with requests_mock.mock() as m: - m.put(self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/tags", text=add_tags_xml) m.delete(self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/tags/b", status_code=204) m.delete(self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/tags/d", status_code=204) + m.put(self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/tags", text=add_tags_xml) m.put(self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", text=update_xml) single_datasource = TSC.DatasourceItem("1d0304cd-3796-429f-b815-7258370b9b74") single_datasource._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" @@ -191,7 +191,7 @@ def test_update_connection(self) -> None: self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/connections/be786ae0-d2bf-4a4b-9b34-e2de8d2d4488", text=response_xml, ) - single_datasource = TSC.DatasourceItem("1d0304cd-3796-429f-b815-7258370b9b74") + single_datasource = TSC.DatasourceItem("be786ae0-d2bf-4a4b-9b34-e2de8d2d4488") single_datasource.owner_id = "dd2239f6-ddf1-4107-981a-4cf94e415794" single_datasource._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" self.server.datasources.populate_connections(single_datasource) @@ -610,7 +610,7 @@ def test_synchronous_publish_timeout_error(self) -> None: new_datasource = TSC.DatasourceItem(project_id="") publish_mode = self.server.PublishMode.CreateNew - + # http://test/api/2.4/sites/dad65087-b08b-4603-af4e-2887b8aafc67/datasources?datasourceType=tds self.assertRaisesRegex( InternalServerError, "Please use asynchronous publishing to avoid timeouts.", diff --git a/test/test_endpoint.py b/test/test_endpoint.py index 0d8ae84f2..3d2d1c995 100644 --- a/test/test_endpoint.py +++ b/test/test_endpoint.py @@ -15,9 +15,32 @@ def setUp(self) -> None: # Fake signin self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - return super().setUp() + def test_fallback_request_logic(self) -> None: + url = "http://test/" + endpoint = TSC.server.Endpoint(self.server) + with requests_mock.mock() as m: + m.get(url) + response = endpoint.get_request(url=url) + self.assertIsNotNone(response) + + def test_user_friendly_request_returns(self) -> None: + url = "http://test/" + endpoint = TSC.server.Endpoint(self.server) + with requests_mock.mock() as m: + m.get(url) + response = endpoint.send_request_while_show_progress_threaded( + endpoint.parent_srv.session.get, url=url, request_timeout=2 + ) + self.assertIsNotNone(response) + + def test_blocking_request_returns(self) -> None: + url = "http://test/" + endpoint = TSC.server.Endpoint(self.server) + response = endpoint._blocking_request(endpoint.parent_srv.session.get, url=url) + self.assertIsNotNone(response) + def test_get_request_stream(self) -> None: url = "http://test/" endpoint = TSC.server.Endpoint(self.server) diff --git a/test/test_fileuploads.py b/test/test_fileuploads.py index 4d3b0c864..cf0861e24 100644 --- a/test/test_fileuploads.py +++ b/test/test_fileuploads.py @@ -43,7 +43,7 @@ def test_upload_chunks_file_path(self): append_response_xml = f.read().decode("utf-8") with requests_mock.mock() as m: m.post(self.baseurl, text=initialize_response_xml) - m.put(self.baseurl + "/" + upload_id, text=append_response_xml) + m.put("{}/{}".format(self.baseurl, upload_id), text=append_response_xml) actual = self.server.fileuploads.upload(file_path) self.assertEqual(upload_id, actual) @@ -58,7 +58,7 @@ def test_upload_chunks_file_object(self): append_response_xml = f.read().decode("utf-8") with requests_mock.mock() as m: m.post(self.baseurl, text=initialize_response_xml) - m.put(self.baseurl + "/" + upload_id, text=append_response_xml) + m.put("{}/{}".format(self.baseurl, upload_id), text=append_response_xml) actual = self.server.fileuploads.upload(file_content) self.assertEqual(upload_id, actual) diff --git a/test/test_request_option.py b/test/test_request_option.py index 9dacbe033..5d8bdf05e 100644 --- a/test/test_request_option.py +++ b/test/test_request_option.py @@ -22,7 +22,7 @@ class RequestOptionTests(unittest.TestCase): def setUp(self) -> None: - self.server = TSC.Server("http://test", False) + self.server = TSC.Server("http://test", False, http_options={"timeout": 5}) # Fake signin self.server.version = "3.10" @@ -151,7 +151,7 @@ def test_multiple_filter_options(self) -> None: ) ) req_option.filter.add(TSC.Filter(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Operator.Equals, "foo")) - for _ in range(100): + for _ in range(5): matching_workbooks, pagination_item = self.server.workbooks.get(req_option) self.assertEqual(3, pagination_item.total_available) @@ -245,7 +245,7 @@ def test_multiple_filter_options_shorthand(self) -> None: ) m.get(url, text=response_xml) - for _ in range(100): + for _ in range(5): matching_workbooks = self.server.workbooks.filter(tags__in=["sample", "safari", "weather"], name="foo") self.assertEqual(3, matching_workbooks.total_available) diff --git a/test/test_webhook.py b/test/test_webhook.py index ff8b7048e..5f26266b2 100644 --- a/test/test_webhook.py +++ b/test/test_webhook.py @@ -4,7 +4,8 @@ import requests_mock import tableauserverclient as TSC -from tableauserverclient.server import RequestFactory, WebhookItem +from tableauserverclient.server import RequestFactory +from tableauserverclient.models import WebhookItem from ._utils import asset TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), "assets")