diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index d70539582..7e1533eef 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -13,7 +13,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13-dev'] runs-on: ${{ matrix.os }} diff --git a/pyproject.toml b/pyproject.toml index 3bf47ea23..c3cb67eda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,29 +15,29 @@ dependencies = [ 'defusedxml>=0.7.1', # latest as at 7/31/23 'packaging>=23.1', # latest as at 7/31/23 'requests>=2.31', # latest as at 7/31/23 - 'urllib3==2.2.2', # dependabot + 'urllib3>=2.2.2,<3', 'typing_extensions>=4.0.1', ] -requires-python = ">=3.7" +requires-python = ">=3.9" classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12" + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13" ] [project.urls] repository = "https://github.com/tableau/server-client-python" [project.optional-dependencies] -test = ["black==23.7", "build", "mypy==1.4", "pytest>=7.0", "pytest-cov", "pytest-subtests", +test = ["black==24.8", "build", "mypy==1.4", "pytest>=7.0", "pytest-cov", "pytest-subtests", "requests-mock>=1.0,<2.0"] [tool.black] line-length = 120 -target-version = ['py37', 'py38', 'py39', 'py310', 'py311', 'py312'] +target-version = ['py39', 'py310', 'py311', 'py312', 'py313'] [tool.mypy] check_untyped_defs = false @@ -46,7 +46,7 @@ disable_error_code = [ # tableauserverclient\server\endpoint\datasources_endpoint.py:48: error: Cannot assign multiple types to name "FilePath" without an explicit "Type[...]" annotation [misc] 'annotation-unchecked' # can be removed when check_untyped_defs = true ] -files = ["tableauserverclient", "test"] +files = ["tableauserverclient", "test", "samples"] show_error_codes = true ignore_missing_imports = true # defusedxml library has no types no_implicit_reexport = true diff --git a/samples/add_default_permission.py b/samples/add_default_permission.py index 5a450e8ab..d26d009e2 100644 --- a/samples/add_default_permission.py +++ b/samples/add_default_permission.py @@ -63,10 +63,10 @@ def main(): for permission in new_default_permissions: grantee = permission.grantee capabilities = permission.capabilities - print("\nCapabilities for {0} {1}:".format(grantee.tag_name, grantee.id)) + print(f"\nCapabilities for {grantee.tag_name} {grantee.id}:") for capability in capabilities: - print("\t{0} - {1}".format(capability, capabilities[capability])) + print(f"\t{capability} - {capabilities[capability]}") # Uncomment lines below to DELETE the new capability and the new project # rules_to_delete = TSC.PermissionsRule( diff --git a/samples/create_group.py b/samples/create_group.py index f4c6a9ca9..aca3e895b 100644 --- a/samples/create_group.py +++ b/samples/create_group.py @@ -11,7 +11,6 @@ import os from datetime import time -from typing import List import tableauserverclient as TSC from tableauserverclient import ServerResponseError @@ -63,23 +62,23 @@ def main(): if args.file: filepath = os.path.abspath(args.file) - print("Add users to site from file {}:".format(filepath)) - added: List[TSC.UserItem] - failed: List[TSC.UserItem, TSC.ServerResponseError] + print(f"Add users to site from file {filepath}:") + added: list[TSC.UserItem] + failed: list[TSC.UserItem, TSC.ServerResponseError] added, failed = server.users.create_from_file(filepath) for user, error in failed: print(user, error.code) if error.code == "409017": user = server.users.filter(name=user.name)[0] added.append(user) - print("Adding users to group:{}".format(added)) + print(f"Adding users to group:{added}") for user in added: - print("Adding user {}".format(user)) + print(f"Adding user {user}") try: server.groups.add_user(group, user.id) except ServerResponseError as serverError: if serverError.code == "409011": - print("user {} is already a member of group {}".format(user.name, group.name)) + print(f"user {user.name} is already a member of group {group.name}") else: raise rError diff --git a/samples/create_project.py b/samples/create_project.py index 1fc649f8c..d775902aa 100644 --- a/samples/create_project.py +++ b/samples/create_project.py @@ -84,7 +84,7 @@ def main(): server.projects.populate_datasource_default_permissions(changed_project), server.projects.populate_permissions(changed_project) # Projects have default permissions set for the object types they contain - print("Permissions from project {}:".format(changed_project.id)) + print(f"Permissions from project {changed_project.id}:") print(changed_project.permissions) print( changed_project.default_workbook_permissions, diff --git a/samples/create_schedules.py b/samples/create_schedules.py index dee088571..c23a2eced 100644 --- a/samples/create_schedules.py +++ b/samples/create_schedules.py @@ -55,7 +55,7 @@ def main(): ) try: hourly_schedule = server.schedules.create(hourly_schedule) - print("Hourly schedule created (ID: {}).".format(hourly_schedule.id)) + print(f"Hourly schedule created (ID: {hourly_schedule.id}).") except Exception as e: print(e) @@ -71,7 +71,7 @@ def main(): ) try: daily_schedule = server.schedules.create(daily_schedule) - print("Daily schedule created (ID: {}).".format(daily_schedule.id)) + print(f"Daily schedule created (ID: {daily_schedule.id}).") except Exception as e: print(e) @@ -89,7 +89,7 @@ def main(): ) try: weekly_schedule = server.schedules.create(weekly_schedule) - print("Weekly schedule created (ID: {}).".format(weekly_schedule.id)) + print(f"Weekly schedule created (ID: {weekly_schedule.id}).") except Exception as e: print(e) options = TSC.RequestOptions() @@ -112,7 +112,7 @@ def main(): ) try: monthly_schedule = server.schedules.create(monthly_schedule) - print("Monthly schedule created (ID: {}).".format(monthly_schedule.id)) + print(f"Monthly schedule created (ID: {monthly_schedule.id}).") except Exception as e: print(e) diff --git a/samples/explore_datasource.py b/samples/explore_datasource.py index fb45cb45e..877c5f08d 100644 --- a/samples/explore_datasource.py +++ b/samples/explore_datasource.py @@ -54,13 +54,13 @@ def main(): new_datasource = server.datasources.publish( new_datasource, args.publish, TSC.Server.PublishMode.Overwrite ) - print("Datasource published. ID: {}".format(new_datasource.id)) + print(f"Datasource published. ID: {new_datasource.id}") else: print("Publish failed. Could not find the default project.") # Gets all datasource items all_datasources, pagination_item = server.datasources.get() - print("\nThere are {} datasources on site: ".format(pagination_item.total_available)) + print(f"\nThere are {pagination_item.total_available} datasources on site: ") print([datasource.name for datasource in all_datasources]) if all_datasources: @@ -69,20 +69,15 @@ def main(): # Populate connections server.datasources.populate_connections(sample_datasource) - print("\nConnections for {}: ".format(sample_datasource.name)) - print( - [ - "{0}({1})".format(connection.id, connection.datasource_name) - for connection in sample_datasource.connections - ] - ) + print(f"\nConnections for {sample_datasource.name}: ") + print([f"{connection.id}({connection.datasource_name})" for connection in sample_datasource.connections]) # Add some tags to the datasource original_tag_set = set(sample_datasource.tags) sample_datasource.tags.update("a", "b", "c", "d") server.datasources.update(sample_datasource) - print("\nOld tag set: {}".format(original_tag_set)) - print("New tag set: {}".format(sample_datasource.tags)) + print(f"\nOld tag set: {original_tag_set}") + print(f"New tag set: {sample_datasource.tags}") # Delete all tags that were added by setting tags to original sample_datasource.tags = original_tag_set diff --git a/samples/explore_favorites.py b/samples/explore_favorites.py index 243e91954..f199522ed 100644 --- a/samples/explore_favorites.py +++ b/samples/explore_favorites.py @@ -3,7 +3,7 @@ import argparse import logging import tableauserverclient as TSC -from tableauserverclient import Resource +from tableauserverclient.models import Resource def main(): @@ -39,15 +39,15 @@ def main(): # get all favorites on site for the logged on user user: TSC.UserItem = TSC.UserItem() user.id = server.user_id - print("Favorites for user: {}".format(user.id)) + print(f"Favorites for user: {user.id}") server.favorites.get(user) print(user.favorites) # get list of workbooks all_workbook_items, pagination_item = server.workbooks.get() if all_workbook_items is not None and len(all_workbook_items) > 0: - my_workbook: TSC.WorkbookItem = all_workbook_items[0] - server.favorites.add_favorite(server, user, Resource.Workbook.name(), all_workbook_items[0]) + my_workbook = all_workbook_items[0] + server.favorites.add_favorite(user, Resource.Workbook, all_workbook_items[0]) print( "Workbook added to favorites. Workbook Name: {}, Workbook ID: {}".format( my_workbook.name, my_workbook.id @@ -57,7 +57,7 @@ def main(): if views is not None and len(views) > 0: my_view = views[0] server.favorites.add_favorite_view(user, my_view) - print("View added to favorites. View Name: {}, View ID: {}".format(my_view.name, my_view.id)) + print(f"View added to favorites. View Name: {my_view.name}, View ID: {my_view.id}") all_datasource_items, pagination_item = server.datasources.get() if all_datasource_items: @@ -70,12 +70,10 @@ def main(): ) server.favorites.delete_favorite_workbook(user, my_workbook) - print( - "Workbook deleted from favorites. Workbook Name: {}, Workbook ID: {}".format(my_workbook.name, my_workbook.id) - ) + print(f"Workbook deleted from favorites. Workbook Name: {my_workbook.name}, Workbook ID: {my_workbook.id}") server.favorites.delete_favorite_view(user, my_view) - print("View deleted from favorites. View Name: {}, View ID: {}".format(my_view.name, my_view.id)) + print(f"View deleted from favorites. View Name: {my_view.name}, View ID: {my_view.id}") server.favorites.delete_favorite_datasource(user, my_datasource) print( diff --git a/samples/explore_site.py b/samples/explore_site.py index a2274f1a7..eb9eba0de 100644 --- a/samples/explore_site.py +++ b/samples/explore_site.py @@ -49,7 +49,7 @@ def main(): if args.delete: print("You can only delete the site you are currently in") - print("Delete site `{}`?".format(current_site.name)) + print(f"Delete site `{current_site.name}`?") # server.sites.delete(server.site_id) elif args.create: diff --git a/samples/explore_webhooks.py b/samples/explore_webhooks.py index 77802b1db..f25c41849 100644 --- a/samples/explore_webhooks.py +++ b/samples/explore_webhooks.py @@ -52,11 +52,11 @@ def main(): new_webhook.event = "datasource-created" print(new_webhook) new_webhook = server.webhooks.create(new_webhook) - print("Webhook created. ID: {}".format(new_webhook.id)) + print(f"Webhook created. ID: {new_webhook.id}") # Gets all webhook items all_webhooks, pagination_item = server.webhooks.get() - print("\nThere are {} webhooks on site: ".format(pagination_item.total_available)) + print(f"\nThere are {pagination_item.total_available} webhooks on site: ") print([webhook.name for webhook in all_webhooks]) if all_webhooks: diff --git a/samples/explore_workbook.py b/samples/explore_workbook.py index 57f88aa07..f51639ab3 100644 --- a/samples/explore_workbook.py +++ b/samples/explore_workbook.py @@ -59,13 +59,13 @@ def main(): if default_project is not None: new_workbook = TSC.WorkbookItem(default_project.id) new_workbook = server.workbooks.publish(new_workbook, args.publish, overwrite_true) - print("Workbook published. ID: {}".format(new_workbook.id)) + print(f"Workbook published. ID: {new_workbook.id}") else: print("Publish failed. Could not find the default project.") # Gets all workbook items all_workbooks, pagination_item = server.workbooks.get() - print("\nThere are {} workbooks on site: ".format(pagination_item.total_available)) + print(f"\nThere are {pagination_item.total_available} workbooks on site: ") print([workbook.name for workbook in all_workbooks]) if all_workbooks: @@ -78,27 +78,22 @@ def main(): # Populate views server.workbooks.populate_views(sample_workbook) - print("\nName of views in {}: ".format(sample_workbook.name)) + print(f"\nName of views in {sample_workbook.name}: ") print([view.name for view in sample_workbook.views]) # Populate connections server.workbooks.populate_connections(sample_workbook) - print("\nConnections for {}: ".format(sample_workbook.name)) - print( - [ - "{0}({1})".format(connection.id, connection.datasource_name) - for connection in sample_workbook.connections - ] - ) + print(f"\nConnections for {sample_workbook.name}: ") + print([f"{connection.id}({connection.datasource_name})" for connection in sample_workbook.connections]) # Update tags and show_tabs flag original_tag_set = set(sample_workbook.tags) sample_workbook.tags.update("a", "b", "c", "d") sample_workbook.show_tabs = True server.workbooks.update(sample_workbook) - print("\nWorkbook's old tag set: {}".format(original_tag_set)) - print("Workbook's new tag set: {}".format(sample_workbook.tags)) - print("Workbook tabbed: {}".format(sample_workbook.show_tabs)) + print(f"\nWorkbook's old tag set: {original_tag_set}") + print(f"Workbook's new tag set: {sample_workbook.tags}") + print(f"Workbook tabbed: {sample_workbook.show_tabs}") # Delete all tags that were added by setting tags to original sample_workbook.tags = original_tag_set @@ -109,8 +104,8 @@ def main(): original_tag_set = set(sample_view.tags) sample_view.tags.add("view_tag") server.views.update(sample_view) - print("\nView's old tag set: {}".format(original_tag_set)) - print("View's new tag set: {}".format(sample_view.tags)) + print(f"\nView's old tag set: {original_tag_set}") + print(f"View's new tag set: {sample_view.tags}") # Delete tag from just one view sample_view.tags = original_tag_set @@ -119,14 +114,14 @@ def main(): if args.download: # Download path = server.workbooks.download(sample_workbook.id, args.download) - print("\nDownloaded workbook to {}".format(path)) + print(f"\nDownloaded workbook to {path}") if args.preview_image: # Populate workbook preview image server.workbooks.populate_preview_image(sample_workbook) with open(args.preview_image, "wb") as f: f.write(sample_workbook.preview_image) - print("\nDownloaded preview image of workbook to {}".format(os.path.abspath(args.preview_image))) + print(f"\nDownloaded preview image of workbook to {os.path.abspath(args.preview_image)}") # get custom views cvs, _ = server.custom_views.get() @@ -153,10 +148,10 @@ def main(): server.workbooks.populate_powerpoint(sample_workbook) with open(args.powerpoint, "wb") as f: f.write(sample_workbook.powerpoint) - print("\nDownloaded powerpoint of workbook to {}".format(os.path.abspath(args.powerpoint))) + print(f"\nDownloaded powerpoint of workbook to {os.path.abspath(args.powerpoint)}") if args.delete: - print("deleting {}".format(c.id)) + print(f"deleting {c.id}") unlucky = TSC.CustomViewItem(c.id) server.custom_views.delete(unlucky.id) diff --git a/samples/export.py b/samples/export.py index f2783fa6e..815ec8b51 100644 --- a/samples/export.py +++ b/samples/export.py @@ -60,10 +60,10 @@ def main(): item = server.views.get_by_id(args.resource_id) if not item: - print("No item found for id {}".format(args.resource_id)) + print(f"No item found for id {args.resource_id}") exit(1) - print("Item found: {}".format(item.name)) + print(f"Item found: {item.name}") # We have a number of different types and functions for each different export type. # We encode that information above in the const=(...) parameter to the add_argument function to make # the code automatically adapt for the type of export the user is doing. @@ -83,7 +83,7 @@ def main(): if args.file: filename = args.file else: - filename = "out.{}".format(extension) + filename = f"out.{extension}" populate(item, options) with open(filename, "wb") as f: diff --git a/samples/extracts.py b/samples/extracts.py index 9bd87a473..d21bfdd0b 100644 --- a/samples/extracts.py +++ b/samples/extracts.py @@ -47,7 +47,7 @@ def main(): with server.auth.sign_in(tableau_auth): # Gets all workbook items all_workbooks, pagination_item = server.workbooks.get() - print("\nThere are {} workbooks on site: ".format(pagination_item.total_available)) + print(f"\nThere are {pagination_item.total_available} workbooks on site: ") print([workbook.name for workbook in all_workbooks]) if all_workbooks: diff --git a/samples/filter_sort_groups.py b/samples/filter_sort_groups.py index 042af32e2..d967659ad 100644 --- a/samples/filter_sort_groups.py +++ b/samples/filter_sort_groups.py @@ -71,7 +71,7 @@ def main(): group_name = filtered_groups.pop().name print(group_name) else: - error = "No project named '{}' found".format(filter_group_name) + error = f"No project named '{filter_group_name}' found" print(error) # Or, try the above with the django style filtering diff --git a/samples/filter_sort_projects.py b/samples/filter_sort_projects.py index 7aa62a5c1..6c3a85dcd 100644 --- a/samples/filter_sort_projects.py +++ b/samples/filter_sort_projects.py @@ -68,7 +68,7 @@ def main(): project_name = filtered_projects.pop().name print(project_name) else: - error = "No project named '{}' found".format(filter_project_name) + error = f"No project named '{filter_project_name}' found" print(error) create_example_project(name="Example 1", server=server) diff --git a/samples/getting_started/1_hello_server.py b/samples/getting_started/1_hello_server.py index 454b225de..5f8cfa238 100644 --- a/samples/getting_started/1_hello_server.py +++ b/samples/getting_started/1_hello_server.py @@ -12,8 +12,8 @@ def main(): # This is the domain for Tableau's Developer Program server_url = "https://10ax.online.tableau.com" server = TSC.Server(server_url) - print("Connected to {}".format(server.server_info.baseurl)) - print("Server information: {}".format(server.server_info)) + print(f"Connected to {server.server_info.baseurl}") + print(f"Server information: {server.server_info}") print("Sign up for a test site at https://www.tableau.com/developer") diff --git a/samples/getting_started/2_hello_site.py b/samples/getting_started/2_hello_site.py index d62896059..8635947a8 100644 --- a/samples/getting_started/2_hello_site.py +++ b/samples/getting_started/2_hello_site.py @@ -19,7 +19,7 @@ def main(): use_ssl = True server = TSC.Server(server_url, use_server_version=True, http_options={"verify": use_ssl}) - print("Connected to {}".format(server.server_info.baseurl)) + print(f"Connected to {server.server_info.baseurl}") # 3 - replace with your site name exactly as it looks in the url # e.g https://my-server/#/site/this-is-your-site-url-name/not-this-part @@ -39,7 +39,7 @@ def main(): with server.auth.sign_in(tableau_auth): projects, pagination = server.projects.get() if projects: - print("{} projects".format(pagination.total_available)) + print(f"{pagination.total_available} projects") project = projects[0] print(project.name) diff --git a/samples/getting_started/3_hello_universe.py b/samples/getting_started/3_hello_universe.py index 21de97831..a2c4301d0 100644 --- a/samples/getting_started/3_hello_universe.py +++ b/samples/getting_started/3_hello_universe.py @@ -17,7 +17,7 @@ def main(): use_ssl = True server = TSC.Server(server_url, use_server_version=True, http_options={"verify": use_ssl}) - print("Connected to {}".format(server.server_info.baseurl)) + print(f"Connected to {server.server_info.baseurl}") # 3 - replace with your site name exactly as it looks in a url # e.g https://my-server/#/this-is-your-site-url-name/ @@ -36,55 +36,55 @@ def main(): with server.auth.sign_in(tableau_auth): projects, pagination = server.projects.get() if projects: - print("{} projects".format(pagination.total_available)) + print(f"{pagination.total_available} projects") for project in projects: print(project.name) workbooks, pagination = server.datasources.get() if workbooks: - print("{} workbooks".format(pagination.total_available)) + print(f"{pagination.total_available} workbooks") print(workbooks[0]) views, pagination = server.views.get() if views: - print("{} views".format(pagination.total_available)) + print(f"{pagination.total_available} views") print(views[0]) datasources, pagination = server.datasources.get() if datasources: - print("{} datasources".format(pagination.total_available)) + print(f"{pagination.total_available} datasources") print(datasources[0]) # I think all these other content types can go to a hello_universe script # data alert, dqw, flow, ... do any of these require any add-ons? jobs, pagination = server.jobs.get() if jobs: - print("{} jobs".format(pagination.total_available)) + print(f"{pagination.total_available} jobs") print(jobs[0]) schedules, pagination = server.schedules.get() if schedules: - print("{} schedules".format(pagination.total_available)) + print(f"{pagination.total_available} schedules") print(schedules[0]) tasks, pagination = server.tasks.get() if tasks: - print("{} tasks".format(pagination.total_available)) + print(f"{pagination.total_available} tasks") print(tasks[0]) webhooks, pagination = server.webhooks.get() if webhooks: - print("{} webhooks".format(pagination.total_available)) + print(f"{pagination.total_available} webhooks") print(webhooks[0]) users, pagination = server.users.get() if users: - print("{} users".format(pagination.total_available)) + print(f"{pagination.total_available} users") print(users[0]) groups, pagination = server.groups.get() if groups: - print("{} groups".format(pagination.total_available)) + print(f"{pagination.total_available} groups") print(groups[0]) diff --git a/samples/initialize_server.py b/samples/initialize_server.py index cb3d9e1d0..cdfaf27a8 100644 --- a/samples/initialize_server.py +++ b/samples/initialize_server.py @@ -51,7 +51,7 @@ def main(): # Create the site if it doesn't exist if existing_site is None: - print("Site not found: {0} Creating it...".format(args.site_id)) + print(f"Site not found: {args.site_id} Creating it...") new_site = TSC.SiteItem( name=args.site_id, content_url=args.site_id.replace(" ", ""), @@ -59,7 +59,7 @@ def main(): ) server.sites.create(new_site) else: - print("Site {0} exists. Moving on...".format(args.site_id)) + print(f"Site {args.site_id} exists. Moving on...") ################################################################################ # Step 3: Sign-in to our target site @@ -81,7 +81,7 @@ def main(): # Create our project if it doesn't exist if project is None: - print("Project not found: {0} Creating it...".format(args.project)) + print(f"Project not found: {args.project} Creating it...") new_project = TSC.ProjectItem(name=args.project) project = server_upload.projects.create(new_project) @@ -100,7 +100,7 @@ def publish_datasources_to_site(server_object, project, folder): for fname in glob.glob(path): new_ds = TSC.DatasourceItem(project.id) new_ds = server_object.datasources.publish(new_ds, fname, server_object.PublishMode.Overwrite) - print("Datasource published. ID: {0}".format(new_ds.id)) + print(f"Datasource published. ID: {new_ds.id}") def publish_workbooks_to_site(server_object, project, folder): @@ -110,7 +110,7 @@ def publish_workbooks_to_site(server_object, project, folder): new_workbook = TSC.WorkbookItem(project.id) new_workbook.show_tabs = True new_workbook = server_object.workbooks.publish(new_workbook, fname, server_object.PublishMode.Overwrite) - print("Workbook published. ID: {0}".format(new_workbook.id)) + print(f"Workbook published. ID: {new_workbook.id}") if __name__ == "__main__": diff --git a/samples/list.py b/samples/list.py index 8d72fb620..2675a2954 100644 --- a/samples/list.py +++ b/samples/list.py @@ -48,6 +48,9 @@ def main(): "webhooks": server.webhooks, "workbook": server.workbooks, }.get(args.resource_type) + if endpoint is None: + print("Resource type not found.") + sys.exit(1) options = TSC.RequestOptions() options.sort.add(TSC.Sort(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Direction.Desc)) @@ -59,7 +62,7 @@ def main(): print(resource.name[:18], " ") # , resource._connections()) if count > 100: break - print("Total: {}".format(count)) + print(f"Total: {count}") if __name__ == "__main__": diff --git a/samples/login.py b/samples/login.py index 6a3e9e8b3..847d3558f 100644 --- a/samples/login.py +++ b/samples/login.py @@ -59,7 +59,7 @@ def sample_connect_to_server(args): password = args.password or getpass.getpass("Password: ") tableau_auth = TSC.TableauAuth(args.username, password, site_id=args.site) - print("\nSigning in...\nServer: {}\nSite: {}\nUsername: {}".format(args.server, args.site, args.username)) + print(f"\nSigning in...\nServer: {args.server}\nSite: {args.site}\nUsername: {args.username}") else: # Trying to authenticate using personal access tokens. @@ -68,7 +68,7 @@ def sample_connect_to_server(args): tableau_auth = TSC.PersonalAccessTokenAuth( token_name=args.token_name, personal_access_token=token, site_id=args.site ) - print("\nSigning in...\nServer: {}\nSite: {}\nToken name: {}".format(args.server, args.site, args.token_name)) + print(f"\nSigning in...\nServer: {args.server}\nSite: {args.site}\nToken name: {args.token_name}") if not tableau_auth: raise TabError("Did not create authentication object. Check arguments.") diff --git a/samples/move_workbook_sites.py b/samples/move_workbook_sites.py index 47af1f2f9..e82c75cf9 100644 --- a/samples/move_workbook_sites.py +++ b/samples/move_workbook_sites.py @@ -59,7 +59,7 @@ def main(): # Step 3: Download workbook to a temp directory if len(all_workbooks) == 0: - print("No workbook named {} found.".format(args.workbook_name)) + print(f"No workbook named {args.workbook_name} found.") else: tmpdir = tempfile.mkdtemp() try: @@ -68,10 +68,10 @@ def main(): # Step 4: Check if destination site exists, then sign in to the site all_sites, pagination_info = source_server.sites.get() found_destination_site = any( - (True for site in all_sites if args.destination_site.lower() == site.content_url.lower()) + True for site in all_sites if args.destination_site.lower() == site.content_url.lower() ) if not found_destination_site: - error = "No site named {} found.".format(args.destination_site) + error = f"No site named {args.destination_site} found." raise LookupError(error) tableau_auth.site_id = args.destination_site @@ -85,7 +85,7 @@ def main(): new_workbook = dest_server.workbooks.publish( new_workbook, workbook_path, mode=TSC.Server.PublishMode.Overwrite ) - print("Successfully moved {0} ({1})".format(new_workbook.name, new_workbook.id)) + print(f"Successfully moved {new_workbook.name} ({new_workbook.id})") # Step 6: Delete workbook from source site and delete temp directory source_server.workbooks.delete(all_workbooks[0].id) diff --git a/samples/pagination_sample.py b/samples/pagination_sample.py index a7ae6dc89..a68eed4b3 100644 --- a/samples/pagination_sample.py +++ b/samples/pagination_sample.py @@ -57,7 +57,7 @@ def main(): for wb in TSC.Pager(server.workbooks, page_options): print(wb.name) count = count + 1 - print("Total: {}\n".format(count)) + print(f"Total: {count}\n") count = 0 page_options = TSC.RequestOptions(2, 3) @@ -65,7 +65,7 @@ def main(): for wb in TSC.Pager(server.workbooks, page_options): print(wb.name) count = count + 1 - print("Truncated Total: {}\n".format(count)) + print(f"Truncated Total: {count}\n") print("Your id: ", you.name, you.id, "\n") count = 0 @@ -76,7 +76,7 @@ def main(): for wb in TSC.Pager(server.workbooks, filtered_page_options): print(wb.name, " -- ", wb.owner_id) count = count + 1 - print("Filtered Total: {}\n".format(count)) + print(f"Filtered Total: {count}\n") # 2. QuerySet offers a fluent interface on top of the RequestOptions object print("Fetching workbooks again - this time filtered with QuerySet") @@ -90,7 +90,7 @@ def main(): count = count + 1 more = queryset.total_available > count page = page + 1 - print("QuerySet Total: {}".format(count)) + print(f"QuerySet Total: {count}") # 3. QuerySet also allows you to iterate over all objects without explicitly paging. print("Fetching again - this time without manually paging") diff --git a/samples/publish_datasource.py b/samples/publish_datasource.py index 5ac768674..85f63fb35 100644 --- a/samples/publish_datasource.py +++ b/samples/publish_datasource.py @@ -111,14 +111,14 @@ def main(): new_job = server.datasources.publish( new_datasource, args.file, publish_mode, connection_credentials=new_conn_creds, as_job=True ) - print("Datasource published asynchronously. Job ID: {0}".format(new_job.id)) + print(f"Datasource published asynchronously. Job ID: {new_job.id}") else: # Normal publishing, returns a datasource_item new_datasource = server.datasources.publish( new_datasource, args.file, publish_mode, connection_credentials=new_conn_creds ) print( - "{0}Datasource published. Datasource ID: {1}".format( + "{}Datasource published. Datasource ID: {}".format( new_datasource.id, tableauserverclient.datetime_helpers.timestamp() ) ) diff --git a/samples/publish_workbook.py b/samples/publish_workbook.py index 8a9f45279..d31978c0f 100644 --- a/samples/publish_workbook.py +++ b/samples/publish_workbook.py @@ -80,7 +80,7 @@ def main(): as_job=args.as_job, skip_connection_check=args.skip_connection_check, ) - print("Workbook published. JOB ID: {0}".format(new_job.id)) + print(f"Workbook published. JOB ID: {new_job.id}") else: new_workbook = server.workbooks.publish( new_workbook, @@ -90,7 +90,7 @@ def main(): as_job=args.as_job, skip_connection_check=args.skip_connection_check, ) - print("Workbook published. ID: {0}".format(new_workbook.id)) + print(f"Workbook published. ID: {new_workbook.id}") else: error = "The default project could not be found." raise LookupError(error) diff --git a/samples/query_permissions.py b/samples/query_permissions.py index 4e509cd97..3309acd90 100644 --- a/samples/query_permissions.py +++ b/samples/query_permissions.py @@ -57,17 +57,15 @@ def main(): permissions = resource.permissions # Print result - print( - "\n{0} permission rule(s) found for {1} {2}.".format(len(permissions), args.resource_type, args.resource_id) - ) + print(f"\n{len(permissions)} permission rule(s) found for {args.resource_type} {args.resource_id}.") for permission in permissions: grantee = permission.grantee capabilities = permission.capabilities - print("\nCapabilities for {0} {1}:".format(grantee.tag_name, grantee.id)) + print(f"\nCapabilities for {grantee.tag_name} {grantee.id}:") for capability in capabilities: - print("\t{0} - {1}".format(capability, capabilities[capability])) + print(f"\t{capability} - {capabilities[capability]}") if __name__ == "__main__": diff --git a/samples/refresh_tasks.py b/samples/refresh_tasks.py index 03daedf16..c95000898 100644 --- a/samples/refresh_tasks.py +++ b/samples/refresh_tasks.py @@ -19,12 +19,12 @@ def handle_run(server, args): def handle_list(server, _): tasks, pagination = server.tasks.get() for task in tasks: - print("{}".format(task)) + print(f"{task}") def handle_info(server, args): task = server.tasks.get_by_id(args.id) - print("{}".format(task)) + print(f"{task}") def main(): diff --git a/samples/update_workbook_data_acceleration.py b/samples/update_workbook_data_acceleration.py index 75f12262f..57a1363ed 100644 --- a/samples/update_workbook_data_acceleration.py +++ b/samples/update_workbook_data_acceleration.py @@ -43,7 +43,7 @@ def main(): with server.auth.sign_in(tableau_auth): # Get workbook all_workbooks, pagination_item = server.workbooks.get() - print("\nThere are {} workbooks on site: ".format(pagination_item.total_available)) + print(f"\nThere are {pagination_item.total_available} workbooks on site: ") print([workbook.name for workbook in all_workbooks]) if all_workbooks: diff --git a/samples/update_workbook_data_freshness_policy.py b/samples/update_workbook_data_freshness_policy.py index 9e4d63dc1..c23e3717f 100644 --- a/samples/update_workbook_data_freshness_policy.py +++ b/samples/update_workbook_data_freshness_policy.py @@ -45,7 +45,7 @@ def main(): with server.auth.sign_in(tableau_auth): # Get workbook all_workbooks, pagination_item = server.workbooks.get() - print("\nThere are {} workbooks on site: ".format(pagination_item.total_available)) + print(f"\nThere are {pagination_item.total_available} workbooks on site: ") print([workbook.name for workbook in all_workbooks]) if all_workbooks: diff --git a/tableauserverclient/_version.py b/tableauserverclient/_version.py index d47374097..5d1dca9df 100644 --- a/tableauserverclient/_version.py +++ b/tableauserverclient/_version.py @@ -84,7 +84,7 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env= stderr=(subprocess.PIPE if hide_stderr else None), ) break - except EnvironmentError: + except OSError: e = sys.exc_info()[1] if e.errno == errno.ENOENT: continue @@ -94,7 +94,7 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env= return None, None else: if verbose: - print("unable to find command, tried %s" % (commands,)) + print("unable to find command, tried {}".format(commands)) return None, None stdout = p.communicate()[0].strip() if sys.version_info[0] >= 3: @@ -131,7 +131,7 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): root = os.path.dirname(root) # up a level if verbose: - print("Tried directories %s but none started with prefix %s" % (str(rootdirs), parentdir_prefix)) + print("Tried directories {} but none started with prefix {}".format(str(rootdirs), parentdir_prefix)) raise NotThisMethod("rootdir doesn't start with parentdir_prefix") @@ -144,7 +144,7 @@ def git_get_keywords(versionfile_abs): # _version.py. keywords = {} try: - f = open(versionfile_abs, "r") + f = open(versionfile_abs) for line in f.readlines(): if line.strip().startswith("git_refnames ="): mo = re.search(r'=\s*"(.*)"', line) @@ -159,7 +159,7 @@ def git_get_keywords(versionfile_abs): if mo: keywords["date"] = mo.group(1) f.close() - except EnvironmentError: + except OSError: pass return keywords @@ -183,11 +183,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): if verbose: print("keywords are unexpanded, not using") raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) + refs = {r.strip() for r in refnames.strip("()").split(",")} # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " - tags = set([r[len(TAG) :] for r in refs if r.startswith(TAG)]) + tags = {r[len(TAG) :] for r in refs if r.startswith(TAG)} if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %d @@ -196,7 +196,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r"\d", r)]) + tags = {r for r in refs if re.search(r"\d", r)} if verbose: print("discarding '%s', no digits" % ",".join(refs - tags)) if verbose: @@ -299,7 +299,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): if verbose: fmt = "tag '%s' doesn't start with prefix '%s'" print(fmt % (full_tag, tag_prefix)) - pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % ( + pieces["error"] = "tag '{}' doesn't start with prefix '{}'".format( full_tag, tag_prefix, ) diff --git a/tableauserverclient/models/column_item.py b/tableauserverclient/models/column_item.py index df936e315..3a7416e28 100644 --- a/tableauserverclient/models/column_item.py +++ b/tableauserverclient/models/column_item.py @@ -3,7 +3,7 @@ from .property_decorators import property_not_empty -class ColumnItem(object): +class ColumnItem: def __init__(self, name, description=None): self._id = None self.description = description diff --git a/tableauserverclient/models/connection_credentials.py b/tableauserverclient/models/connection_credentials.py index d61bbb751..bb2cbbba9 100644 --- a/tableauserverclient/models/connection_credentials.py +++ b/tableauserverclient/models/connection_credentials.py @@ -1,7 +1,7 @@ from .property_decorators import property_is_boolean -class ConnectionCredentials(object): +class ConnectionCredentials: """Connection Credentials for Workbooks and Datasources publish request. Consider removing this object and other variables holding secrets diff --git a/tableauserverclient/models/connection_item.py b/tableauserverclient/models/connection_item.py index 62ff530c9..937e43481 100644 --- a/tableauserverclient/models/connection_item.py +++ b/tableauserverclient/models/connection_item.py @@ -1,5 +1,5 @@ import logging -from typing import List, Optional +from typing import Optional from defusedxml.ElementTree import fromstring @@ -8,7 +8,7 @@ from tableauserverclient.helpers.logging import logger -class ConnectionItem(object): +class ConnectionItem: def __init__(self): self._datasource_id: Optional[str] = None self._datasource_name: Optional[str] = None @@ -48,7 +48,7 @@ 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.debug( - "Cannot update value: Query tagging is always enabled for {} connections".format(self._connection_type) + f"Cannot update value: Query tagging is always enabled for {self._connection_type} connections" ) return self._query_tagging = value @@ -59,7 +59,7 @@ def __repr__(self): ) @classmethod - def from_response(cls, resp, ns) -> List["ConnectionItem"]: + def from_response(cls, resp, ns) -> list["ConnectionItem"]: all_connection_items = list() parsed_response = fromstring(resp) all_connection_xml = parsed_response.findall(".//t:connection", namespaces=ns) @@ -82,7 +82,7 @@ def from_response(cls, resp, ns) -> List["ConnectionItem"]: return all_connection_items @classmethod - def from_xml_element(cls, parsed_response, ns) -> List["ConnectionItem"]: + def from_xml_element(cls, parsed_response, ns) -> list["ConnectionItem"]: """ @@ -93,7 +93,7 @@ def from_xml_element(cls, parsed_response, ns) -> List["ConnectionItem"]: """ - all_connection_items: List["ConnectionItem"] = list() + all_connection_items: list["ConnectionItem"] = list() all_connection_xml = parsed_response.findall(".//t:connection", namespaces=ns) for connection_xml in all_connection_xml: diff --git a/tableauserverclient/models/custom_view_item.py b/tableauserverclient/models/custom_view_item.py index 246a19e7f..de917bf4a 100644 --- a/tableauserverclient/models/custom_view_item.py +++ b/tableauserverclient/models/custom_view_item.py @@ -2,7 +2,7 @@ from defusedxml import ElementTree from defusedxml.ElementTree import fromstring, tostring -from typing import Callable, List, Optional +from typing import Callable, Optional from .exceptions import UnpopulatedPropertyError from .user_item import UserItem @@ -11,7 +11,7 @@ from ..datetime_helpers import parse_datetime -class CustomViewItem(object): +class CustomViewItem: def __init__(self, id: Optional[str] = None, name: Optional[str] = None) -> None: self._content_url: Optional[str] = None # ? self._created_at: Optional["datetime"] = None @@ -35,7 +35,7 @@ def __repr__(self: "CustomViewItem"): owner_info = "" if self._owner: owner_info = " owner='{}'".format(self._owner.name or self._owner.id or "unknown") - return "".format(self.id, self.name, view_info, wb_info, owner_info) + return f"" def _set_image(self, image): self._image = image @@ -104,7 +104,7 @@ def from_response(cls, resp, ns, workbook_id="") -> Optional["CustomViewItem"]: return item[0] @classmethod - def list_from_response(cls, resp, ns, workbook_id="") -> List["CustomViewItem"]: + def list_from_response(cls, resp, ns, workbook_id="") -> list["CustomViewItem"]: return cls.from_xml_element(fromstring(resp), ns, workbook_id) """ @@ -121,7 +121,7 @@ def list_from_response(cls, resp, ns, workbook_id="") -> List["CustomViewItem"]: """ @classmethod - def from_xml_element(cls, parsed_response, ns, workbook_id="") -> List["CustomViewItem"]: + def from_xml_element(cls, parsed_response, ns, workbook_id="") -> list["CustomViewItem"]: all_view_items = list() all_view_xml = parsed_response.findall(".//t:customView", namespaces=ns) for custom_view_xml in all_view_xml: diff --git a/tableauserverclient/models/data_acceleration_report_item.py b/tableauserverclient/models/data_acceleration_report_item.py index 7424e6b95..3a8883bed 100644 --- a/tableauserverclient/models/data_acceleration_report_item.py +++ b/tableauserverclient/models/data_acceleration_report_item.py @@ -1,8 +1,8 @@ from defusedxml.ElementTree import fromstring -class DataAccelerationReportItem(object): - class ComparisonRecord(object): +class DataAccelerationReportItem: + class ComparisonRecord: def __init__( self, site, diff --git a/tableauserverclient/models/data_alert_item.py b/tableauserverclient/models/data_alert_item.py index 65be233e3..7285ee609 100644 --- a/tableauserverclient/models/data_alert_item.py +++ b/tableauserverclient/models/data_alert_item.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import List, Optional +from typing import Optional from defusedxml.ElementTree import fromstring @@ -10,7 +10,7 @@ ) -class DataAlertItem(object): +class DataAlertItem: class Frequency: Once = "Once" Frequently = "Frequently" @@ -34,7 +34,7 @@ def __init__(self): self._workbook_name: Optional[str] = None self._project_id: Optional[str] = None self._project_name: Optional[str] = None - self._recipients: Optional[List[str]] = None + self._recipients: Optional[list[str]] = None def __repr__(self) -> str: return " Optional[str]: return self._creatorId @property - def recipients(self) -> List[str]: + def recipients(self) -> list[str]: return self._recipients or list() @property @@ -174,7 +174,7 @@ def _set_values( self._recipients = recipients @classmethod - def from_response(cls, resp, ns) -> List["DataAlertItem"]: + def from_response(cls, resp, ns) -> list["DataAlertItem"]: all_alert_items = list() parsed_response = fromstring(resp) all_alert_xml = parsed_response.findall(".//t:dataAlert", namespaces=ns) diff --git a/tableauserverclient/models/data_freshness_policy_item.py b/tableauserverclient/models/data_freshness_policy_item.py index f567c501c..6e0cb9001 100644 --- a/tableauserverclient/models/data_freshness_policy_item.py +++ b/tableauserverclient/models/data_freshness_policy_item.py @@ -1,6 +1,6 @@ import xml.etree.ElementTree as ET -from typing import Optional, Union, List +from typing import Optional from tableauserverclient.models.property_decorators import property_is_enum, property_not_nullable from .interval_item import IntervalItem @@ -50,11 +50,11 @@ class Frequency: Week = "Week" Month = "Month" - def __init__(self, frequency: str, time: str, timezone, interval_item: Optional[List[str]] = None): + def __init__(self, frequency: str, time: str, timezone, interval_item: Optional[list[str]] = None): self.frequency = frequency self.time = time self.timezone = timezone - self.interval_item: Optional[List[str]] = interval_item + self.interval_item: Optional[list[str]] = interval_item def __repr__(self): return ( @@ -62,11 +62,11 @@ def __repr__(self): ).format(**vars(self)) @property - def interval_item(self) -> Optional[List[str]]: + def interval_item(self) -> Optional[list[str]]: return self._interval_item @interval_item.setter - def interval_item(self, value: List[str]): + def interval_item(self, value: list[str]): self._interval_item = value @property @@ -186,7 +186,7 @@ def parse_week_intervals(interval_values): def parse_month_intervals(interval_values): - error = "Invalid interval value for a monthly frequency: {}.".format(interval_values) + error = f"Invalid interval value for a monthly frequency: {interval_values}." # Month interval can have value either only ['LastDay'] or list of dates e.g. ["1", 20", "30"] # First check if the list only have LastDay value. When using LastDay, there shouldn't be diff --git a/tableauserverclient/models/database_item.py b/tableauserverclient/models/database_item.py index dfc58e1bb..4d4604461 100644 --- a/tableauserverclient/models/database_item.py +++ b/tableauserverclient/models/database_item.py @@ -10,7 +10,7 @@ ) -class DatabaseItem(object): +class DatabaseItem: class ContentPermissions: LockedToProject = "LockedToDatabase" ManagedByOwner = "ManagedByOwner" @@ -45,7 +45,7 @@ def __init__(self, name, description=None, content_permissions=None): self._tables = None # Not implemented yet def __str__(self): - return "".format(self._id, self.name) + return f"" def __repr__(self): return self.__str__() + " { " + ", ".join(" % s: % s" % item for item in vars(self).items()) + "}" @@ -250,7 +250,7 @@ def _set_tables(self, tables): self._tables = tables def _set_default_permissions(self, permissions, content_type): - attr = "_default_{content}_permissions".format(content=content_type) + attr = f"_default_{content_type}_permissions" setattr( self, attr, diff --git a/tableauserverclient/models/datasource_item.py b/tableauserverclient/models/datasource_item.py index e4e71c4a2..1b082c157 100644 --- a/tableauserverclient/models/datasource_item.py +++ b/tableauserverclient/models/datasource_item.py @@ -1,7 +1,7 @@ import copy import datetime import xml.etree.ElementTree as ET -from typing import Dict, List, Optional, Set, Tuple +from typing import Optional from defusedxml.ElementTree import fromstring @@ -18,14 +18,14 @@ from tableauserverclient.models.tag_item import TagItem -class DatasourceItem(object): +class DatasourceItem: class AskDataEnablement: Enabled = "Enabled" Disabled = "Disabled" SiteDefault = "SiteDefault" def __repr__(self): - return "".format( + return "".format( self._id, self.name, self.description or "No Description", @@ -44,7 +44,7 @@ def __init__(self, project_id: Optional[str] = None, name: Optional[str] = None) self._encrypt_extracts = None self._has_extracts = None self._id: Optional[str] = None - self._initial_tags: Set = set() + self._initial_tags: set = set() self._project_name: Optional[str] = None self._revisions = None self._size: Optional[int] = None @@ -55,7 +55,7 @@ def __init__(self, project_id: Optional[str] = None, name: Optional[str] = None) self.name = name self.owner_id: Optional[str] = None self.project_id = project_id - self.tags: Set[str] = set() + self.tags: set[str] = set() self._permissions = None self._data_quality_warnings = None @@ -72,14 +72,14 @@ def ask_data_enablement(self, value: Optional[AskDataEnablement]): self._ask_data_enablement = value @property - def connections(self) -> Optional[List[ConnectionItem]]: + def connections(self) -> Optional[list[ConnectionItem]]: if self._connections is None: error = "Datasource item must be populated with connections first." raise UnpopulatedPropertyError(error) return self._connections() @property - def permissions(self) -> Optional[List[PermissionsRule]]: + def permissions(self) -> Optional[list[PermissionsRule]]: if self._permissions is None: error = "Project item must be populated with permissions first." raise UnpopulatedPropertyError(error) @@ -177,7 +177,7 @@ def webpage_url(self) -> Optional[str]: return self._webpage_url @property - def revisions(self) -> List[RevisionItem]: + def revisions(self) -> list[RevisionItem]: if self._revisions is None: error = "Datasource item must be populated with revisions first." raise UnpopulatedPropertyError(error) @@ -309,7 +309,7 @@ def _set_values( self._size = int(size) @classmethod - def from_response(cls, resp: str, ns: Dict) -> List["DatasourceItem"]: + def from_response(cls, resp: str, ns: dict) -> list["DatasourceItem"]: all_datasource_items = list() parsed_response = fromstring(resp) all_datasource_xml = parsed_response.findall(".//t:datasource", namespaces=ns) @@ -326,7 +326,7 @@ def from_xml(cls, datasource_xml, ns): return datasource_item @staticmethod - def _parse_element(datasource_xml: ET.Element, ns: Dict) -> Tuple: + def _parse_element(datasource_xml: ET.Element, ns: dict) -> tuple: id_ = datasource_xml.get("id", None) name = datasource_xml.get("name", None) datasource_type = datasource_xml.get("type", None) diff --git a/tableauserverclient/models/dqw_item.py b/tableauserverclient/models/dqw_item.py index ada041481..fbda9d9f2 100644 --- a/tableauserverclient/models/dqw_item.py +++ b/tableauserverclient/models/dqw_item.py @@ -3,7 +3,7 @@ from tableauserverclient.datetime_helpers import parse_datetime -class DQWItem(object): +class DQWItem: class WarningType: WARNING = "WARNING" DEPRECATED = "DEPRECATED" diff --git a/tableauserverclient/models/favorites_item.py b/tableauserverclient/models/favorites_item.py index caff755e3..4fea280f7 100644 --- a/tableauserverclient/models/favorites_item.py +++ b/tableauserverclient/models/favorites_item.py @@ -1,28 +1,27 @@ import logging +from typing import Union from defusedxml.ElementTree import fromstring -from tableauserverclient.models.tableau_types import TableauItem +from tableauserverclient.models.tableau_types import TableauItem from tableauserverclient.models.datasource_item import DatasourceItem from tableauserverclient.models.flow_item import FlowItem from tableauserverclient.models.project_item import ProjectItem from tableauserverclient.models.metric_item import MetricItem from tableauserverclient.models.view_item import ViewItem from tableauserverclient.models.workbook_item import WorkbookItem -from typing import Dict, List from tableauserverclient.helpers.logging import logger -from typing import Dict, List, Union -FavoriteType = Dict[ +FavoriteType = dict[ str, - List[TableauItem], + list[TableauItem], ] class FavoriteItem: @classmethod - def from_response(cls, xml: str, namespace: Dict) -> FavoriteType: + def from_response(cls, xml: Union[str, bytes], namespace: dict) -> FavoriteType: favorites: FavoriteType = { "datasources": [], "flows": [], diff --git a/tableauserverclient/models/fileupload_item.py b/tableauserverclient/models/fileupload_item.py index e9bdd25b2..aea4dfe1f 100644 --- a/tableauserverclient/models/fileupload_item.py +++ b/tableauserverclient/models/fileupload_item.py @@ -1,7 +1,7 @@ from defusedxml.ElementTree import fromstring -class FileuploadItem(object): +class FileuploadItem: def __init__(self): self._file_size = None self._upload_session_id = None diff --git a/tableauserverclient/models/flow_item.py b/tableauserverclient/models/flow_item.py index edce2ec97..9bcad5e89 100644 --- a/tableauserverclient/models/flow_item.py +++ b/tableauserverclient/models/flow_item.py @@ -1,7 +1,7 @@ import copy import datetime import xml.etree.ElementTree as ET -from typing import List, Optional, Set +from typing import Optional from defusedxml.ElementTree import fromstring @@ -14,9 +14,9 @@ from tableauserverclient.models.tag_item import TagItem -class FlowItem(object): +class FlowItem: def __repr__(self): - return " None: self._webpage_url: Optional[str] = None self._created_at: Optional[datetime.datetime] = None self._id: Optional[str] = None - self._initial_tags: Set[str] = set() + self._initial_tags: set[str] = set() self._project_name: Optional[str] = None self._updated_at: Optional[datetime.datetime] = None self.name: Optional[str] = name self.owner_id: Optional[str] = None self.project_id: str = project_id - self.tags: Set[str] = set() + self.tags: set[str] = set() self.description: Optional[str] = None self._connections: Optional[ConnectionItem] = None @@ -170,7 +170,7 @@ def _set_values( self.owner_id = owner_id @classmethod - def from_response(cls, resp, ns) -> List["FlowItem"]: + def from_response(cls, resp, ns) -> list["FlowItem"]: all_flow_items = list() parsed_response = fromstring(resp) all_flow_xml = parsed_response.findall(".//t:flow", namespaces=ns) diff --git a/tableauserverclient/models/flow_run_item.py b/tableauserverclient/models/flow_run_item.py index 12281f4f8..f2f1d561f 100644 --- a/tableauserverclient/models/flow_run_item.py +++ b/tableauserverclient/models/flow_run_item.py @@ -1,13 +1,13 @@ import itertools from datetime import datetime -from typing import Dict, List, Optional, Type +from typing import Optional from defusedxml.ElementTree import fromstring from tableauserverclient.datetime_helpers import parse_datetime -class FlowRunItem(object): +class FlowRunItem: def __init__(self) -> None: self._id: str = "" self._flow_id: Optional[str] = None @@ -71,7 +71,7 @@ def _set_values( self._background_job_id = background_job_id @classmethod - def from_response(cls: Type["FlowRunItem"], resp: bytes, ns: Optional[Dict]) -> List["FlowRunItem"]: + def from_response(cls: type["FlowRunItem"], resp: bytes, ns: Optional[dict]) -> list["FlowRunItem"]: all_flowrun_items = list() parsed_response = fromstring(resp) all_flowrun_xml = itertools.chain( diff --git a/tableauserverclient/models/group_item.py b/tableauserverclient/models/group_item.py index 6c8f7eb01..6871f8b16 100644 --- a/tableauserverclient/models/group_item.py +++ b/tableauserverclient/models/group_item.py @@ -1,4 +1,4 @@ -from typing import Callable, List, Optional, TYPE_CHECKING +from typing import Callable, Optional, TYPE_CHECKING from defusedxml.ElementTree import fromstring @@ -11,7 +11,7 @@ from tableauserverclient.server import Pager -class GroupItem(object): +class GroupItem: tag_name: str = "group" class LicenseMode: @@ -27,7 +27,7 @@ def __init__(self, name=None, domain_name=None) -> None: self.domain_name: Optional[str] = domain_name def __repr__(self): - return "{}({!r})".format(self.__class__.__name__, self.__dict__) + return f"{self.__class__.__name__}({self.__dict__!r})" @property def domain_name(self) -> Optional[str]: @@ -79,7 +79,7 @@ def _set_users(self, users: Callable[..., "Pager"]) -> None: self._users = users @classmethod - def from_response(cls, resp, ns) -> List["GroupItem"]: + def from_response(cls, resp, ns) -> list["GroupItem"]: all_group_items = list() parsed_response = fromstring(resp) all_group_xml = parsed_response.findall(".//t:group", namespaces=ns) diff --git a/tableauserverclient/models/groupset_item.py b/tableauserverclient/models/groupset_item.py index ffb57adf5..aa653a79e 100644 --- a/tableauserverclient/models/groupset_item.py +++ b/tableauserverclient/models/groupset_item.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Optional +from typing import Optional import xml.etree.ElementTree as ET from defusedxml.ElementTree import fromstring @@ -13,7 +13,7 @@ class GroupSetItem: def __init__(self, name: Optional[str] = None) -> None: self.name = name self.id: Optional[str] = None - self.groups: List["GroupItem"] = [] + self.groups: list["GroupItem"] = [] self.group_count: int = 0 def __str__(self) -> str: @@ -25,13 +25,13 @@ def __repr__(self) -> str: return self.__str__() @classmethod - def from_response(cls, response: bytes, ns: Dict[str, str]) -> List["GroupSetItem"]: + def from_response(cls, response: bytes, ns: dict[str, str]) -> list["GroupSetItem"]: parsed_response = fromstring(response) all_groupset_xml = parsed_response.findall(".//t:groupSet", namespaces=ns) return [cls.from_xml(xml, ns) for xml in all_groupset_xml] @classmethod - def from_xml(cls, groupset_xml: ET.Element, ns: Dict[str, str]) -> "GroupSetItem": + def from_xml(cls, groupset_xml: ET.Element, ns: dict[str, str]) -> "GroupSetItem": def get_group(group_xml: ET.Element) -> GroupItem: group_item = GroupItem() group_item._id = group_xml.get("id") diff --git a/tableauserverclient/models/interval_item.py b/tableauserverclient/models/interval_item.py index 444674e19..d7cf891cc 100644 --- a/tableauserverclient/models/interval_item.py +++ b/tableauserverclient/models/interval_item.py @@ -1,7 +1,7 @@ from .property_decorators import property_is_valid_time, property_not_nullable -class IntervalItem(object): +class IntervalItem: class Frequency: Hourly = "Hourly" Daily = "Daily" @@ -25,7 +25,7 @@ class Day: LastDay = "LastDay" -class HourlyInterval(object): +class HourlyInterval: def __init__(self, start_time, end_time, interval_value): self.start_time = start_time self.end_time = end_time @@ -73,12 +73,12 @@ def interval(self, intervals): for interval in intervals: # if an hourly interval is a string, then it is a weekDay interval if isinstance(interval, str) and not interval.isnumeric() and not hasattr(IntervalItem.Day, interval): - error = "Invalid weekDay interval {}".format(interval) + error = f"Invalid weekDay interval {interval}" raise ValueError(error) # if an hourly interval is a number, it is an hours or minutes interval if isinstance(interval, (int, float)) and float(interval) not in VALID_INTERVALS: - error = "Invalid interval {} not in {}".format(interval, str(VALID_INTERVALS)) + error = f"Invalid interval {interval} not in {str(VALID_INTERVALS)}" raise ValueError(error) self._interval = intervals @@ -108,7 +108,7 @@ def _interval_type_pairs(self): return interval_type_pairs -class DailyInterval(object): +class DailyInterval: def __init__(self, start_time, *interval_values): self.start_time = start_time self.interval = interval_values @@ -141,12 +141,12 @@ def interval(self, intervals): for interval in intervals: # if an hourly interval is a string, then it is a weekDay interval if isinstance(interval, str) and not interval.isnumeric() and not hasattr(IntervalItem.Day, interval): - error = "Invalid weekDay interval {}".format(interval) + error = f"Invalid weekDay interval {interval}" raise ValueError(error) # if an hourly interval is a number, it is an hours or minutes interval if isinstance(interval, (int, float)) and float(interval) not in VALID_INTERVALS: - error = "Invalid interval {} not in {}".format(interval, str(VALID_INTERVALS)) + error = f"Invalid interval {interval} not in {str(VALID_INTERVALS)}" raise ValueError(error) self._interval = intervals @@ -176,7 +176,7 @@ def _interval_type_pairs(self): return interval_type_pairs -class WeeklyInterval(object): +class WeeklyInterval: def __init__(self, start_time, *interval_values): self.start_time = start_time self.interval = interval_values @@ -213,7 +213,7 @@ def _interval_type_pairs(self): return [(IntervalItem.Occurrence.WeekDay, day) for day in self.interval] -class MonthlyInterval(object): +class MonthlyInterval: def __init__(self, start_time, interval_value): self.start_time = start_time diff --git a/tableauserverclient/models/job_item.py b/tableauserverclient/models/job_item.py index 155ce668b..cc7cd5811 100644 --- a/tableauserverclient/models/job_item.py +++ b/tableauserverclient/models/job_item.py @@ -1,5 +1,5 @@ import datetime -from typing import List, Optional +from typing import Optional from defusedxml.ElementTree import fromstring @@ -7,7 +7,7 @@ from tableauserverclient.models.flow_run_item import FlowRunItem -class JobItem(object): +class JobItem: class FinishCode: """ Status codes as documented on @@ -27,7 +27,7 @@ def __init__( started_at: Optional[datetime.datetime] = None, completed_at: Optional[datetime.datetime] = None, finish_code: int = 0, - notes: Optional[List[str]] = None, + notes: Optional[list[str]] = None, mode: Optional[str] = None, workbook_id: Optional[str] = None, datasource_id: Optional[str] = None, @@ -43,7 +43,7 @@ def __init__( self._started_at = started_at self._completed_at = completed_at self._finish_code = finish_code - self._notes: List[str] = notes or [] + self._notes: list[str] = notes or [] self._mode = mode self._workbook_id = workbook_id self._datasource_id = datasource_id @@ -81,7 +81,7 @@ def finish_code(self) -> int: return self._finish_code @property - def notes(self) -> List[str]: + def notes(self) -> list[str]: return self._notes @property @@ -139,7 +139,7 @@ def __repr__(self): return self.__str__() + " { " + ", ".join(" % s: % s" % item for item in vars(self).items()) + "}" @classmethod - def from_response(cls, xml, ns) -> List["JobItem"]: + def from_response(cls, xml, ns) -> list["JobItem"]: parsed_response = fromstring(xml) all_tasks_xml = parsed_response.findall(".//t:job", namespaces=ns) @@ -191,7 +191,7 @@ def _parse_element(cls, element, ns): ) -class BackgroundJobItem(object): +class BackgroundJobItem: class Status: Pending: str = "Pending" InProgress: str = "InProgress" @@ -270,7 +270,7 @@ def priority(self) -> int: return self._priority @classmethod - def from_response(cls, xml, ns) -> List["BackgroundJobItem"]: + def from_response(cls, xml, ns) -> list["BackgroundJobItem"]: parsed_response = fromstring(xml) all_tasks_xml = parsed_response.findall(".//t:backgroundJob", namespaces=ns) return [cls._parse_element(x, ns) for x in all_tasks_xml] diff --git a/tableauserverclient/models/linked_tasks_item.py b/tableauserverclient/models/linked_tasks_item.py index ae9b60425..14a0e4978 100644 --- a/tableauserverclient/models/linked_tasks_item.py +++ b/tableauserverclient/models/linked_tasks_item.py @@ -1,5 +1,5 @@ import datetime as dt -from typing import List, Optional +from typing import Optional from defusedxml.ElementTree import fromstring @@ -14,7 +14,7 @@ def __init__(self) -> None: self.schedule: Optional[ScheduleItem] = None @classmethod - def from_response(cls, resp: bytes, namespace) -> List["LinkedTaskItem"]: + def from_response(cls, resp: bytes, namespace) -> list["LinkedTaskItem"]: parsed_response = fromstring(resp) return [ cls._parse_element(x, namespace) @@ -35,10 +35,10 @@ def __init__(self) -> None: self.id: Optional[str] = None self.step_number: Optional[int] = None self.stop_downstream_on_failure: Optional[bool] = None - self.task_details: List[LinkedTaskFlowRunItem] = [] + self.task_details: list[LinkedTaskFlowRunItem] = [] @classmethod - def from_task_xml(cls, xml, namespace) -> List["LinkedTaskStepItem"]: + def from_task_xml(cls, xml, namespace) -> list["LinkedTaskStepItem"]: return [cls._parse_element(x, namespace) for x in xml.findall(".//t:linkedTaskSteps[@id]", namespace)] @classmethod @@ -61,7 +61,7 @@ def __init__(self) -> None: self.flow_name: Optional[str] = None @classmethod - def _parse_element(cls, xml, namespace) -> List["LinkedTaskFlowRunItem"]: + def _parse_element(cls, xml, namespace) -> list["LinkedTaskFlowRunItem"]: all_tasks = [] for flow_run in xml.findall(".//t:flowRun[@id]", namespace): task = cls() diff --git a/tableauserverclient/models/metric_item.py b/tableauserverclient/models/metric_item.py index d8ba8e825..432fd861a 100644 --- a/tableauserverclient/models/metric_item.py +++ b/tableauserverclient/models/metric_item.py @@ -1,6 +1,6 @@ import xml.etree.ElementTree as ET from datetime import datetime -from typing import List, Optional, Set +from typing import Optional from tableauserverclient.datetime_helpers import parse_datetime from .property_decorators import property_is_boolean, property_is_datetime @@ -8,7 +8,7 @@ from .permissions_item import Permission -class MetricItem(object): +class MetricItem: def __init__(self, name: Optional[str] = None): self._id: Optional[str] = None self._name: Optional[str] = name @@ -21,8 +21,8 @@ def __init__(self, name: Optional[str] = None): self._project_name: Optional[str] = None self._owner_id: Optional[str] = None self._view_id: Optional[str] = None - self._initial_tags: Set[str] = set() - self.tags: Set[str] = set() + self._initial_tags: set[str] = set() + self.tags: set[str] = set() self._permissions: Optional[Permission] = None @property @@ -126,7 +126,7 @@ def from_response( cls, resp: bytes, ns, - ) -> List["MetricItem"]: + ) -> list["MetricItem"]: all_metric_items = list() parsed_response = ET.fromstring(resp) all_metric_xml = parsed_response.findall(".//t:metric", namespaces=ns) diff --git a/tableauserverclient/models/pagination_item.py b/tableauserverclient/models/pagination_item.py index 8cebd1c86..f30519be5 100644 --- a/tableauserverclient/models/pagination_item.py +++ b/tableauserverclient/models/pagination_item.py @@ -1,7 +1,7 @@ from defusedxml.ElementTree import fromstring -class PaginationItem(object): +class PaginationItem: def __init__(self): self._page_number = None self._page_size = None diff --git a/tableauserverclient/models/permissions_item.py b/tableauserverclient/models/permissions_item.py index 26f4ee7e8..3e4fec22a 100644 --- a/tableauserverclient/models/permissions_item.py +++ b/tableauserverclient/models/permissions_item.py @@ -1,5 +1,5 @@ import xml.etree.ElementTree as ET -from typing import Dict, List, Optional +from typing import Optional from defusedxml.ElementTree import fromstring @@ -47,12 +47,12 @@ def __repr__(self): class PermissionsRule: - def __init__(self, grantee: ResourceReference, capabilities: Dict[str, str]) -> None: + def __init__(self, grantee: ResourceReference, capabilities: dict[str, str]) -> None: self.grantee = grantee self.capabilities = capabilities def __repr__(self): - return "".format(self.grantee, self.capabilities) + return f"" def __eq__(self, other: object) -> bool: if not hasattr(other, "grantee") or not hasattr(other, "capabilities"): @@ -66,7 +66,7 @@ def __and__(self, other: "PermissionsRule") -> "PermissionsRule": if self.capabilities == other.capabilities: return self - capabilities = set((*self.capabilities.keys(), *other.capabilities.keys())) + capabilities = {*self.capabilities.keys(), *other.capabilities.keys()} new_capabilities = {} for capability in capabilities: if (self.capabilities.get(capability), other.capabilities.get(capability)) == ( @@ -86,7 +86,7 @@ def __or__(self, other: "PermissionsRule") -> "PermissionsRule": if self.capabilities == other.capabilities: return self - capabilities = set((*self.capabilities.keys(), *other.capabilities.keys())) + capabilities = {*self.capabilities.keys(), *other.capabilities.keys()} new_capabilities = {} for capability in capabilities: if Permission.Mode.Allow in (self.capabilities.get(capability), other.capabilities.get(capability)): @@ -100,14 +100,14 @@ def __or__(self, other: "PermissionsRule") -> "PermissionsRule": return PermissionsRule(self.grantee, new_capabilities) @classmethod - def from_response(cls, resp, ns=None) -> List["PermissionsRule"]: + def from_response(cls, resp, ns=None) -> list["PermissionsRule"]: parsed_response = fromstring(resp) rules = [] permissions_rules_list_xml = parsed_response.findall(".//t:granteeCapabilities", namespaces=ns) for grantee_capability_xml in permissions_rules_list_xml: - capability_dict: Dict[str, str] = {} + capability_dict: dict[str, str] = {} grantee = PermissionsRule._parse_grantee_element(grantee_capability_xml, ns) @@ -116,7 +116,7 @@ def from_response(cls, resp, ns=None) -> List["PermissionsRule"]: mode = capability_xml.get("mode") if name is None or mode is None: - logger.error("Capability was not valid: {}".format(capability_xml)) + logger.error(f"Capability was not valid: {capability_xml}") raise UnpopulatedPropertyError() else: capability_dict[name] = mode @@ -127,7 +127,7 @@ def from_response(cls, resp, ns=None) -> List["PermissionsRule"]: return rules @staticmethod - def _parse_grantee_element(grantee_capability_xml: ET.Element, ns: Optional[Dict[str, str]]) -> ResourceReference: + def _parse_grantee_element(grantee_capability_xml: ET.Element, ns: Optional[dict[str, str]]) -> ResourceReference: """Use Xpath magic and some string splitting to get the right object type from the xml""" # Get the first element in the tree with an 'id' attribute @@ -146,6 +146,6 @@ def _parse_grantee_element(grantee_capability_xml: ET.Element, ns: Optional[Dict elif grantee_type == "groupSet": grantee = GroupSetItem.as_reference(grantee_id) else: - raise UnknownGranteeTypeError("No support for grantee type of {}".format(grantee_type)) + raise UnknownGranteeTypeError(f"No support for grantee type of {grantee_type}") return grantee diff --git a/tableauserverclient/models/project_item.py b/tableauserverclient/models/project_item.py index 9fb382885..d875abbdf 100644 --- a/tableauserverclient/models/project_item.py +++ b/tableauserverclient/models/project_item.py @@ -1,6 +1,6 @@ import logging import xml.etree.ElementTree as ET -from typing import List, Optional +from typing import Optional from defusedxml.ElementTree import fromstring @@ -8,14 +8,14 @@ from tableauserverclient.models.property_decorators import property_is_enum, property_not_empty -class ProjectItem(object): +class ProjectItem: class ContentPermissions: LockedToProject: str = "LockedToProject" ManagedByOwner: str = "ManagedByOwner" LockedToProjectWithoutNested: str = "LockedToProjectWithoutNested" def __repr__(self): - return "".format( + return "".format( self._id, self.name, self.parent_id or "None (Top level)", self.content_permissions or "Not Set" ) @@ -158,7 +158,7 @@ def _set_permissions(self, permissions): self._permissions = permissions def _set_default_permissions(self, permissions, content_type): - attr = "_default_{content}_permissions".format(content=content_type) + attr = f"_default_{content_type}_permissions" setattr( self, attr, @@ -166,7 +166,7 @@ def _set_default_permissions(self, permissions, content_type): ) @classmethod - def from_response(cls, resp, ns) -> List["ProjectItem"]: + def from_response(cls, resp, ns) -> list["ProjectItem"]: all_project_items = list() parsed_response = fromstring(resp) all_project_xml = parsed_response.findall(".//t:project", namespaces=ns) diff --git a/tableauserverclient/models/property_decorators.py b/tableauserverclient/models/property_decorators.py index ce31b1428..5048b3498 100644 --- a/tableauserverclient/models/property_decorators.py +++ b/tableauserverclient/models/property_decorators.py @@ -1,7 +1,8 @@ import datetime import re from functools import wraps -from typing import Any, Container, Optional, Tuple +from typing import Any, Optional +from collections.abc import Container from tableauserverclient.datetime_helpers import parse_datetime @@ -11,7 +12,7 @@ def property_type_decorator(func): @wraps(func) def wrapper(self, value): if value is not None and not hasattr(enum_type, value): - error = "Invalid value: {0}. {1} must be of type {2}.".format(value, func.__name__, enum_type.__name__) + error = f"Invalid value: {value}. {func.__name__} must be of type {enum_type.__name__}." raise ValueError(error) return func(self, value) @@ -24,7 +25,7 @@ def property_is_boolean(func): @wraps(func) def wrapper(self, value): if not isinstance(value, bool): - error = "Boolean expected for {0} flag.".format(func.__name__) + error = f"Boolean expected for {func.__name__} flag." raise ValueError(error) return func(self, value) @@ -35,7 +36,7 @@ def property_not_nullable(func): @wraps(func) def wrapper(self, value): if value is None: - error = "{0} must be defined.".format(func.__name__) + error = f"{func.__name__} must be defined." raise ValueError(error) return func(self, value) @@ -46,7 +47,7 @@ def property_not_empty(func): @wraps(func) def wrapper(self, value): if not value: - error = "{0} must not be empty.".format(func.__name__) + error = f"{func.__name__} must not be empty." raise ValueError(error) return func(self, value) @@ -66,7 +67,7 @@ def wrapper(self, value): return wrapper -def property_is_int(range: Tuple[int, int], allowed: Optional[Container[Any]] = None): +def property_is_int(range: tuple[int, int], allowed: Optional[Container[Any]] = None): """Takes a range of ints and a list of exemptions to check against when setting a property on a model. The range is a tuple of (min, max) and the allowed list (empty by default) allows values outside that range. @@ -81,7 +82,7 @@ def property_is_int(range: Tuple[int, int], allowed: Optional[Container[Any]] = def property_type_decorator(func): @wraps(func) def wrapper(self, value): - error = "Invalid property defined: '{}'. Integer value expected.".format(value) + error = f"Invalid property defined: '{value}'. Integer value expected." if range is None: if isinstance(value, int): @@ -133,7 +134,7 @@ def wrapper(self, value): return func(self, value) if not isinstance(value, str): raise ValueError( - "Cannot convert {} into a datetime, cannot update {}".format(value.__class__.__name__, func.__name__) + f"Cannot convert {value.__class__.__name__} into a datetime, cannot update {func.__name__}" ) dt = parse_datetime(value) @@ -146,11 +147,11 @@ def property_is_data_acceleration_config(func): @wraps(func) def wrapper(self, value): if not isinstance(value, dict): - raise ValueError("{} is not type 'dict', cannot update {})".format(value.__class__.__name__, func.__name__)) + raise ValueError(f"{value.__class__.__name__} is not type 'dict', cannot update {func.__name__})") if len(value) < 2 or not all(attr in value.keys() for attr in ("acceleration_enabled", "accelerate_now")): - error = "{} should have 2 keys ".format(func.__name__) + error = f"{func.__name__} should have 2 keys " error += "'acceleration_enabled' and 'accelerate_now'" - error += "instead you have {}".format(value.keys()) + error += f"instead you have {value.keys()}" raise ValueError(error) return func(self, value) diff --git a/tableauserverclient/models/reference_item.py b/tableauserverclient/models/reference_item.py index 710548fcc..4c1fff564 100644 --- a/tableauserverclient/models/reference_item.py +++ b/tableauserverclient/models/reference_item.py @@ -1,10 +1,10 @@ -class ResourceReference(object): +class ResourceReference: def __init__(self, id_, tag_name): self.id = id_ self.tag_name = tag_name def __str__(self): - return "".format(self._id, self._tag_name) + return f"" __repr__ = __str__ diff --git a/tableauserverclient/models/revision_item.py b/tableauserverclient/models/revision_item.py index a0e6a1bd5..1b4cc6249 100644 --- a/tableauserverclient/models/revision_item.py +++ b/tableauserverclient/models/revision_item.py @@ -1,12 +1,12 @@ from datetime import datetime -from typing import List, Optional +from typing import Optional from defusedxml.ElementTree import fromstring from tableauserverclient.datetime_helpers import parse_datetime -class RevisionItem(object): +class RevisionItem: def __init__(self): self._resource_id: Optional[str] = None self._resource_name: Optional[str] = None @@ -56,7 +56,7 @@ def __repr__(self): ) @classmethod - def from_response(cls, resp: bytes, ns, resource_item) -> List["RevisionItem"]: + def from_response(cls, resp: bytes, ns, resource_item) -> list["RevisionItem"]: all_revision_items = list() parsed_response = fromstring(resp) all_revision_xml = parsed_response.findall(".//t:revision", namespaces=ns) diff --git a/tableauserverclient/models/schedule_item.py b/tableauserverclient/models/schedule_item.py index e416643ba..e39042058 100644 --- a/tableauserverclient/models/schedule_item.py +++ b/tableauserverclient/models/schedule_item.py @@ -19,7 +19,7 @@ Interval = Union[HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval] -class ScheduleItem(object): +class ScheduleItem: class Type: Extract = "Extract" Flow = "Flow" @@ -336,7 +336,7 @@ def parse_add_to_schedule_response(response, ns): all_task_xml = parsed_response.findall(".//t:task", namespaces=ns) error = ( - "Status {}: {}".format(response.status_code, response.reason) + f"Status {response.status_code}: {response.reason}" if response.status_code < 200 or response.status_code >= 300 else None ) diff --git a/tableauserverclient/models/server_info_item.py b/tableauserverclient/models/server_info_item.py index 57fc51af9..5c3f6acc7 100644 --- a/tableauserverclient/models/server_info_item.py +++ b/tableauserverclient/models/server_info_item.py @@ -6,7 +6,7 @@ from tableauserverclient.helpers.logging import logger -class ServerInfoItem(object): +class ServerInfoItem: def __init__(self, product_version, build_number, rest_api_version): self._product_version = product_version self._build_number = build_number @@ -40,11 +40,11 @@ def from_response(cls, resp, ns): try: parsed_response = fromstring(resp) except xml.etree.ElementTree.ParseError as error: - logger.info("Unexpected response for ServerInfo: {}".format(resp)) + logger.info(f"Unexpected response for ServerInfo: {resp}") logger.info(error) return cls("Unknown", "Unknown", "Unknown") except Exception as error: - logger.info("Unexpected response for ServerInfo: {}".format(resp)) + logger.info(f"Unexpected response for ServerInfo: {resp}") logger.info(error) return cls("Unknown", "Unknown", "Unknown") diff --git a/tableauserverclient/models/site_item.py b/tableauserverclient/models/site_item.py index b651e5773..2d9f014a2 100644 --- a/tableauserverclient/models/site_item.py +++ b/tableauserverclient/models/site_item.py @@ -14,13 +14,13 @@ VALID_CONTENT_URL_RE = r"^[a-zA-Z0-9_\-]*$" -from typing import List, Optional, Union, TYPE_CHECKING +from typing import Optional, Union, TYPE_CHECKING if TYPE_CHECKING: from tableauserverclient.server import Server -class SiteItem(object): +class SiteItem: _user_quota: Optional[int] = None _tier_creator_capacity: Optional[int] = None _tier_explorer_capacity: Optional[int] = None @@ -873,7 +873,7 @@ def _set_values( self.auto_suspend_refresh_inactivity_window = auto_suspend_refresh_inactivity_window @classmethod - def from_response(cls, resp, ns) -> List["SiteItem"]: + def from_response(cls, resp, ns) -> list["SiteItem"]: all_site_items = list() parsed_response = fromstring(resp) all_site_xml = parsed_response.findall(".//t:site", namespaces=ns) diff --git a/tableauserverclient/models/subscription_item.py b/tableauserverclient/models/subscription_item.py index e96fcc448..61c75e2d6 100644 --- a/tableauserverclient/models/subscription_item.py +++ b/tableauserverclient/models/subscription_item.py @@ -1,4 +1,4 @@ -from typing import List, Type, TYPE_CHECKING +from typing import TYPE_CHECKING from defusedxml.ElementTree import fromstring @@ -10,7 +10,7 @@ from .target import Target -class SubscriptionItem(object): +class SubscriptionItem: def __init__(self, subject: str, schedule_id: str, user_id: str, target: "Target") -> None: self._id = None self.attach_image = True @@ -79,7 +79,7 @@ def suspended(self, value: bool) -> None: self._suspended = value @classmethod - def from_response(cls: Type, xml: bytes, ns) -> List["SubscriptionItem"]: + def from_response(cls: type, xml: bytes, ns) -> list["SubscriptionItem"]: parsed_response = fromstring(xml) all_subscriptions_xml = parsed_response.findall(".//t:subscription", namespaces=ns) diff --git a/tableauserverclient/models/table_item.py b/tableauserverclient/models/table_item.py index f9df8a8f3..0afdd4df3 100644 --- a/tableauserverclient/models/table_item.py +++ b/tableauserverclient/models/table_item.py @@ -4,7 +4,7 @@ from .property_decorators import property_not_empty, property_is_boolean -class TableItem(object): +class TableItem: def __init__(self, name, description=None): self._id = None self.description = description diff --git a/tableauserverclient/models/tableau_auth.py b/tableauserverclient/models/tableau_auth.py index 10cf58723..c1e9d62bf 100644 --- a/tableauserverclient/models/tableau_auth.py +++ b/tableauserverclient/models/tableau_auth.py @@ -1,5 +1,5 @@ import abc -from typing import Dict, Optional +from typing import Optional class Credentials(abc.ABC): @@ -9,7 +9,7 @@ def __init__(self, site_id: Optional[str] = None, user_id_to_impersonate: Option @property @abc.abstractmethod - def credentials(self) -> Dict[str, str]: + def credentials(self) -> dict[str, str]: credentials = ( "Credentials can be username/password, Personal Access Token, or JWT" "This method returns values to set as an attribute on the credentials element of the request" @@ -42,7 +42,7 @@ def __init__( self.username = username @property - def credentials(self) -> Dict[str, str]: + def credentials(self) -> dict[str, str]: return {"name": self.username, "password": self.password} def __repr__(self): @@ -69,7 +69,7 @@ def __init__( self.personal_access_token = personal_access_token @property - def credentials(self) -> Dict[str, str]: + def credentials(self) -> dict[str, str]: return { "personalAccessTokenName": self.token_name, "personalAccessTokenSecret": self.personal_access_token, @@ -95,7 +95,7 @@ def __init__(self, jwt: str, site_id: Optional[str] = None, user_id_to_impersona self.jwt = jwt @property - def credentials(self) -> Dict[str, str]: + def credentials(self) -> dict[str, str]: return {"jwt": self.jwt} def __repr__(self): diff --git a/tableauserverclient/models/tableau_types.py b/tableauserverclient/models/tableau_types.py index bac072076..ea2a5e4f8 100644 --- a/tableauserverclient/models/tableau_types.py +++ b/tableauserverclient/models/tableau_types.py @@ -32,4 +32,4 @@ def plural_type(content_type: Resource) -> str: if content_type == Resource.Lens: return "lenses" else: - return "{}s".format(content_type) + return f"{content_type}s" diff --git a/tableauserverclient/models/tag_item.py b/tableauserverclient/models/tag_item.py index afa0a0762..cde755f05 100644 --- a/tableauserverclient/models/tag_item.py +++ b/tableauserverclient/models/tag_item.py @@ -1,16 +1,15 @@ import xml.etree.ElementTree as ET -from typing import Set from defusedxml.ElementTree import fromstring -class TagItem(object): +class TagItem: @classmethod - def from_response(cls, resp: bytes, ns) -> Set[str]: + def from_response(cls, resp: bytes, ns) -> set[str]: return cls.from_xml_element(fromstring(resp), ns) @classmethod - def from_xml_element(cls, parsed_response: ET.Element, ns) -> Set[str]: + def from_xml_element(cls, parsed_response: ET.Element, ns) -> set[str]: all_tags = set() tag_elem = parsed_response.findall(".//t:tag", namespaces=ns) for tag_xml in tag_elem: diff --git a/tableauserverclient/models/task_item.py b/tableauserverclient/models/task_item.py index 01cfcfb11..fa6f782ba 100644 --- a/tableauserverclient/models/task_item.py +++ b/tableauserverclient/models/task_item.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import List, Optional +from typing import Optional from defusedxml.ElementTree import fromstring @@ -8,7 +8,7 @@ from tableauserverclient.models.target import Target -class TaskItem(object): +class TaskItem: class Type: ExtractRefresh = "extractRefresh" DataAcceleration = "dataAcceleration" @@ -48,9 +48,9 @@ def __repr__(self) -> str: ) @classmethod - def from_response(cls, xml, ns, task_type=Type.ExtractRefresh) -> List["TaskItem"]: + def from_response(cls, xml, ns, task_type=Type.ExtractRefresh) -> list["TaskItem"]: parsed_response = fromstring(xml) - all_tasks_xml = parsed_response.findall(".//t:task/t:{}".format(task_type), namespaces=ns) + all_tasks_xml = parsed_response.findall(f".//t:task/t:{task_type}", namespaces=ns) all_tasks = (TaskItem._parse_element(x, ns) for x in all_tasks_xml) diff --git a/tableauserverclient/models/user_item.py b/tableauserverclient/models/user_item.py index fe659575a..fb29492e4 100644 --- a/tableauserverclient/models/user_item.py +++ b/tableauserverclient/models/user_item.py @@ -2,7 +2,7 @@ import xml.etree.ElementTree as ET from datetime import datetime from enum import IntEnum -from typing import Dict, List, Optional, TYPE_CHECKING, Tuple +from typing import Optional, TYPE_CHECKING from defusedxml.ElementTree import fromstring @@ -18,7 +18,7 @@ from tableauserverclient.server import Pager -class UserItem(object): +class UserItem: tag_name: str = "user" class Roles: @@ -57,7 +57,7 @@ def __init__( self._id: Optional[str] = None self._last_login: Optional[datetime] = None self._workbooks = None - self._favorites: Optional[Dict[str, List]] = None + self._favorites: Optional[dict[str, list]] = None self._groups = None self.email: Optional[str] = None self.fullname: Optional[str] = None @@ -69,7 +69,7 @@ def __init__( def __str__(self) -> str: str_site_role = self.site_role or "None" - return "".format(self.id, self.name, str_site_role) + return f"" def __repr__(self): return self.__str__() + " { " + ", ".join(" % s: % s" % item for item in vars(self).items()) + "}" @@ -141,7 +141,7 @@ def workbooks(self) -> "Pager": return self._workbooks() @property - def favorites(self) -> Dict[str, List]: + def favorites(self) -> dict[str, list]: if self._favorites is None: error = "User item must be populated with favorites first." raise UnpopulatedPropertyError(error) @@ -210,12 +210,12 @@ def _set_values( self._domain_name = domain_name @classmethod - def from_response(cls, resp, ns) -> List["UserItem"]: + def from_response(cls, resp, ns) -> list["UserItem"]: element_name = ".//t:user" return cls._parse_xml(element_name, resp, ns) @classmethod - def from_response_as_owner(cls, resp, ns) -> List["UserItem"]: + def from_response_as_owner(cls, resp, ns) -> list["UserItem"]: element_name = ".//t:owner" return cls._parse_xml(element_name, resp, ns) @@ -283,7 +283,7 @@ def _parse_element(user_xml, ns): domain_name, ) - class CSVImport(object): + class CSVImport: """ This class includes hardcoded options and logic for the CSV file format defined for user import https://help.tableau.com/current/server/en-us/users_import.htm @@ -308,7 +308,7 @@ def create_user_from_line(line: str): if line is None or line is False or line == "\n" or line == "": return None line = line.strip().lower() - values: List[str] = list(map(str.strip, line.split(","))) + values: list[str] = list(map(str.strip, line.split(","))) user = UserItem(values[UserItem.CSVImport.ColumnType.USERNAME]) if len(values) > 1: if len(values) > UserItem.CSVImport.ColumnType.MAX: @@ -337,7 +337,7 @@ def create_user_from_line(line: str): # Read through an entire CSV file meant for user import # Return the number of valid lines and a list of all the invalid lines @staticmethod - def validate_file_for_import(csv_file: io.TextIOWrapper, logger) -> Tuple[int, List[str]]: + def validate_file_for_import(csv_file: io.TextIOWrapper, logger) -> tuple[int, list[str]]: num_valid_lines = 0 invalid_lines = [] csv_file.seek(0) # set to start of file in case it has been read earlier @@ -345,11 +345,11 @@ def validate_file_for_import(csv_file: io.TextIOWrapper, logger) -> Tuple[int, L while line and line != "": try: # do not print passwords - logger.info("Reading user {}".format(line[:4])) + logger.info(f"Reading user {line[:4]}") UserItem.CSVImport._validate_import_line_or_throw(line, logger) num_valid_lines += 1 except Exception as exc: - logger.info("Error parsing {}: {}".format(line[:4], exc)) + logger.info(f"Error parsing {line[:4]}: {exc}") invalid_lines.append(line) line = csv_file.readline() return num_valid_lines, invalid_lines @@ -358,7 +358,7 @@ def validate_file_for_import(csv_file: io.TextIOWrapper, logger) -> Tuple[int, L # Iterate through each field and validate the given value against hardcoded constraints @staticmethod def _validate_import_line_or_throw(incoming, logger) -> None: - _valid_attributes: List[List[str]] = [ + _valid_attributes: list[list[str]] = [ [], [], [], @@ -373,23 +373,23 @@ def _validate_import_line_or_throw(incoming, logger) -> None: if len(line) > UserItem.CSVImport.ColumnType.MAX: raise AttributeError("Too many attributes in line") username = line[UserItem.CSVImport.ColumnType.USERNAME.value] - logger.debug("> details - {}".format(username)) + logger.debug(f"> details - {username}") UserItem.validate_username_or_throw(username) for i in range(1, len(line)): - logger.debug("column {}: {}".format(UserItem.CSVImport.ColumnType(i).name, line[i])) + logger.debug(f"column {UserItem.CSVImport.ColumnType(i).name}: {line[i]}") UserItem.CSVImport._validate_attribute_value( line[i], _valid_attributes[i], UserItem.CSVImport.ColumnType(i) ) # Given a restricted set of possible values, confirm the item is in that set @staticmethod - def _validate_attribute_value(item: str, possible_values: List[str], column_type) -> None: + def _validate_attribute_value(item: str, possible_values: list[str], column_type) -> None: if item is None or item == "": # value can be empty for any column except user, which is checked elsewhere return if item in possible_values or possible_values == []: return - raise AttributeError("Invalid value {} for {}".format(item, column_type)) + raise AttributeError(f"Invalid value {item} for {column_type}") # https://help.tableau.com/current/server/en-us/csvguidelines.htm#settings_and_site_roles # This logic is hardcoded to match the existing rules for import csv files diff --git a/tableauserverclient/models/view_item.py b/tableauserverclient/models/view_item.py index a26e364a3..dc5f37a48 100644 --- a/tableauserverclient/models/view_item.py +++ b/tableauserverclient/models/view_item.py @@ -1,7 +1,8 @@ import copy from datetime import datetime from requests import Response -from typing import Callable, Iterator, List, Optional, Set +from typing import Callable, Optional +from collections.abc import Iterator from defusedxml.ElementTree import fromstring @@ -11,13 +12,13 @@ from .tag_item import TagItem -class ViewItem(object): +class ViewItem: def __init__(self) -> None: self._content_url: Optional[str] = None self._created_at: Optional[datetime] = None self._id: Optional[str] = None self._image: Optional[Callable[[], bytes]] = None - self._initial_tags: Set[str] = set() + self._initial_tags: set[str] = set() self._name: Optional[str] = None self._owner_id: Optional[str] = None self._preview_image: Optional[Callable[[], bytes]] = None @@ -29,15 +30,15 @@ def __init__(self) -> None: self._sheet_type: Optional[str] = None self._updated_at: Optional[datetime] = None self._workbook_id: Optional[str] = None - self._permissions: Optional[Callable[[], List[PermissionsRule]]] = None - self.tags: Set[str] = set() + self._permissions: Optional[Callable[[], list[PermissionsRule]]] = None + self.tags: set[str] = set() self._data_acceleration_config = { "acceleration_enabled": None, "acceleration_status": None, } def __str__(self): - return "".format( + return "".format( self._id, self.name, self.content_url, self.project_id ) @@ -146,21 +147,21 @@ def data_acceleration_config(self, value): self._data_acceleration_config = value @property - def permissions(self) -> List[PermissionsRule]: + def permissions(self) -> list[PermissionsRule]: if self._permissions is None: error = "View item must be populated with permissions first." raise UnpopulatedPropertyError(error) return self._permissions() - def _set_permissions(self, permissions: Callable[[], List[PermissionsRule]]) -> None: + def _set_permissions(self, permissions: Callable[[], list[PermissionsRule]]) -> None: self._permissions = permissions @classmethod - def from_response(cls, resp: "Response", ns, workbook_id="") -> List["ViewItem"]: + def from_response(cls, resp: "Response", ns, workbook_id="") -> list["ViewItem"]: return cls.from_xml_element(fromstring(resp), ns, workbook_id) @classmethod - def from_xml_element(cls, parsed_response, ns, workbook_id="") -> List["ViewItem"]: + def from_xml_element(cls, parsed_response, ns, workbook_id="") -> list["ViewItem"]: all_view_items = list() all_view_xml = parsed_response.findall(".//t:view", namespaces=ns) for view_xml in all_view_xml: diff --git a/tableauserverclient/models/virtual_connection_item.py b/tableauserverclient/models/virtual_connection_item.py index 76a3b5dea..e9e22be1e 100644 --- a/tableauserverclient/models/virtual_connection_item.py +++ b/tableauserverclient/models/virtual_connection_item.py @@ -1,6 +1,7 @@ import datetime as dt import json -from typing import Callable, Dict, Iterable, List, Optional +from typing import Callable, Optional +from collections.abc import Iterable from xml.etree.ElementTree import Element from defusedxml.ElementTree import fromstring @@ -23,7 +24,7 @@ def __init__(self, name: str) -> None: self._connections: Optional[Callable[[], Iterable[ConnectionItem]]] = None self.project_id: Optional[str] = None self.owner_id: Optional[str] = None - self.content: Optional[Dict[str, dict]] = None + self.content: Optional[dict[str, dict]] = None self.certification_note: Optional[str] = None def __str__(self) -> str: @@ -40,7 +41,7 @@ def id(self) -> Optional[str]: return self._id @property - def permissions(self) -> List[PermissionsRule]: + def permissions(self) -> list[PermissionsRule]: if self._permissions is None: error = "Workbook item must be populated with permissions first." raise UnpopulatedPropertyError(error) @@ -53,12 +54,12 @@ def connections(self) -> Iterable[ConnectionItem]: return self._connections() @classmethod - def from_response(cls, response: bytes, ns: Dict[str, str]) -> List["VirtualConnectionItem"]: + def from_response(cls, response: bytes, ns: dict[str, str]) -> list["VirtualConnectionItem"]: parsed_response = fromstring(response) return [cls.from_xml(xml, ns) for xml in parsed_response.findall(".//t:virtualConnection[@name]", ns)] @classmethod - def from_xml(cls, xml: Element, ns: Dict[str, str]) -> "VirtualConnectionItem": + def from_xml(cls, xml: Element, ns: dict[str, str]) -> "VirtualConnectionItem": v_conn = cls(xml.get("name", "")) v_conn._id = xml.get("id", None) v_conn.webpage_url = xml.get("webpageUrl", None) diff --git a/tableauserverclient/models/webhook_item.py b/tableauserverclient/models/webhook_item.py index e4d5e4aa0..98d821fb4 100644 --- a/tableauserverclient/models/webhook_item.py +++ b/tableauserverclient/models/webhook_item.py @@ -1,6 +1,6 @@ import re import xml.etree.ElementTree as ET -from typing import List, Optional, Tuple, Type +from typing import Optional from defusedxml.ElementTree import fromstring @@ -13,7 +13,7 @@ def _parse_event(events): return NAMESPACE_RE.sub("", event.tag) -class WebhookItem(object): +class WebhookItem: def __init__(self): self._id: Optional[str] = None self.name: Optional[str] = None @@ -45,10 +45,10 @@ def event(self) -> Optional[str]: @event.setter def event(self, value: str) -> None: - self._event = "webhook-source-event-{}".format(value) + self._event = f"webhook-source-event-{value}" @classmethod - def from_response(cls: Type["WebhookItem"], resp: bytes, ns) -> List["WebhookItem"]: + def from_response(cls: type["WebhookItem"], resp: bytes, ns) -> list["WebhookItem"]: all_webhooks_items = list() parsed_response = fromstring(resp) all_webhooks_xml = parsed_response.findall(".//t:webhook", namespaces=ns) @@ -61,7 +61,7 @@ def from_response(cls: Type["WebhookItem"], resp: bytes, ns) -> List["WebhookIte return all_webhooks_items @staticmethod - def _parse_element(webhook_xml: ET.Element, ns) -> Tuple: + def _parse_element(webhook_xml: ET.Element, ns) -> tuple: id = webhook_xml.get("id", None) name = webhook_xml.get("name", None) @@ -82,4 +82,4 @@ def _parse_element(webhook_xml: ET.Element, ns) -> Tuple: return id, name, url, event, owner_id def __repr__(self) -> str: - return "".format(self.id, self.name, self.url, self.event) + return f"" diff --git a/tableauserverclient/models/workbook_item.py b/tableauserverclient/models/workbook_item.py index 58fd2a9a9..ab5ff4157 100644 --- a/tableauserverclient/models/workbook_item.py +++ b/tableauserverclient/models/workbook_item.py @@ -2,7 +2,7 @@ import datetime import uuid import xml.etree.ElementTree as ET -from typing import Callable, Dict, List, Optional, Set +from typing import Callable, Optional from defusedxml.ElementTree import fromstring @@ -20,7 +20,7 @@ from .data_freshness_policy_item import DataFreshnessPolicyItem -class WorkbookItem(object): +class WorkbookItem: def __init__(self, project_id: Optional[str] = None, name: Optional[str] = None, show_tabs: bool = False) -> None: self._connections = None self._content_url = None @@ -35,15 +35,15 @@ def __init__(self, project_id: Optional[str] = None, name: Optional[str] = None, self._revisions = None self._size = None self._updated_at = None - self._views: Optional[Callable[[], List[ViewItem]]] = None + self._views: Optional[Callable[[], list[ViewItem]]] = None self.name = name self._description = None self.owner_id: Optional[str] = None # workaround for Personal Space workbooks without a project self.project_id: Optional[str] = project_id or uuid.uuid4().__str__() self.show_tabs = show_tabs - self.hidden_views: Optional[List[str]] = None - self.tags: Set[str] = set() + self.hidden_views: Optional[list[str]] = None + self.tags: set[str] = set() self.data_acceleration_config = { "acceleration_enabled": None, "accelerate_now": None, @@ -56,7 +56,7 @@ def __init__(self, project_id: Optional[str] = None, name: Optional[str] = None, return None def __str__(self): - return "".format( + return "".format( self._id, self.name, self.content_url, self.project_id ) @@ -64,14 +64,14 @@ def __repr__(self): return self.__str__() + " { " + ", ".join(" % s: % s" % item for item in vars(self).items()) + "}" @property - def connections(self) -> List[ConnectionItem]: + def connections(self) -> list[ConnectionItem]: if self._connections is None: error = "Workbook item must be populated with connections first." raise UnpopulatedPropertyError(error) return self._connections() @property - def permissions(self) -> List[PermissionsRule]: + def permissions(self) -> list[PermissionsRule]: if self._permissions is None: error = "Workbook item must be populated with permissions first." raise UnpopulatedPropertyError(error) @@ -152,7 +152,7 @@ def updated_at(self) -> Optional[datetime.datetime]: return self._updated_at @property - def views(self) -> List[ViewItem]: + def views(self) -> list[ViewItem]: # Views can be set in an initial workbook response OR by a call # to Server. Without getting too fancy, I think we can rely on # returning a list from the response, until they call @@ -191,7 +191,7 @@ def data_freshness_policy(self, value): self._data_freshness_policy = value @property - def revisions(self) -> List[RevisionItem]: + def revisions(self) -> list[RevisionItem]: if self._revisions is None: error = "Workbook item must be populated with revisions first." raise UnpopulatedPropertyError(error) @@ -203,7 +203,7 @@ def _set_connections(self, connections): def _set_permissions(self, permissions): self._permissions = permissions - def _set_views(self, views: Callable[[], List[ViewItem]]) -> None: + def _set_views(self, views: Callable[[], list[ViewItem]]) -> None: self._views = views def _set_pdf(self, pdf: Callable[[], bytes]) -> None: @@ -316,7 +316,7 @@ def _set_values( self.data_freshness_policy = data_freshness_policy @classmethod - def from_response(cls, resp: str, ns: Dict[str, str]) -> List["WorkbookItem"]: + def from_response(cls, resp: str, ns: dict[str, str]) -> list["WorkbookItem"]: all_workbook_items = list() parsed_response = fromstring(resp) all_workbook_xml = parsed_response.findall(".//t:workbook", namespaces=ns) diff --git a/tableauserverclient/namespace.py b/tableauserverclient/namespace.py index d225ecff6..54ac46d8d 100644 --- a/tableauserverclient/namespace.py +++ b/tableauserverclient/namespace.py @@ -11,7 +11,7 @@ class UnknownNamespaceError(Exception): pass -class Namespace(object): +class Namespace: def __init__(self): self._namespace = {"t": NEW_NAMESPACE} self._detected = False diff --git a/tableauserverclient/server/endpoint/auth_endpoint.py b/tableauserverclient/server/endpoint/auth_endpoint.py index 468d469a7..231052f73 100644 --- a/tableauserverclient/server/endpoint/auth_endpoint.py +++ b/tableauserverclient/server/endpoint/auth_endpoint.py @@ -16,7 +16,7 @@ class Auth(Endpoint): - class contextmgr(object): + class contextmgr: def __init__(self, callback): self._callback = callback @@ -28,7 +28,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): @property def baseurl(self) -> str: - return "{0}/auth".format(self.parent_srv.baseurl) + return f"{self.parent_srv.baseurl}/auth" @api(version="2.0") def sign_in(self, auth_req: "Credentials") -> contextmgr: @@ -42,7 +42,7 @@ def sign_in(self, auth_req: "Credentials") -> contextmgr: Creates a context manager that will sign out of the server upon exit. """ - url = "{0}/{1}".format(self.baseurl, "signin") + url = f"{self.baseurl}/signin" signin_req = RequestFactory.Auth.signin_req(auth_req) server_response = self.parent_srv.session.post( url, data=signin_req, **self.parent_srv.http_options, allow_redirects=False @@ -63,7 +63,7 @@ def sign_in(self, auth_req: "Credentials") -> contextmgr: user_id = parsed_response.find(".//t:user", namespaces=self.parent_srv.namespace).get("id", None) auth_token = parsed_response.find("t:credentials", namespaces=self.parent_srv.namespace).get("token", None) self.parent_srv._set_auth(site_id, user_id, auth_token) - logger.info("Signed into {0} as user with id {1}".format(self.parent_srv.server_address, user_id)) + logger.info(f"Signed into {self.parent_srv.server_address} as user with id {user_id}") return Auth.contextmgr(self.sign_out) # We use the same request that username/password login uses for all auth types. @@ -78,7 +78,7 @@ def sign_in_with_json_web_token(self, auth_req: "Credentials") -> contextmgr: @api(version="2.0") def sign_out(self) -> None: - url = "{0}/{1}".format(self.baseurl, "signout") + url = f"{self.baseurl}/signout" # If there are no auth tokens you're already signed out. No-op if not self.parent_srv.is_signed_in(): return @@ -88,7 +88,7 @@ def sign_out(self) -> None: @api(version="2.6") def switch_site(self, site_item: "SiteItem") -> contextmgr: - url = "{0}/{1}".format(self.baseurl, "switchSite") + url = f"{self.baseurl}/switchSite" switch_req = RequestFactory.Auth.switch_req(site_item.content_url) try: server_response = self.post_request(url, switch_req) @@ -104,11 +104,11 @@ def switch_site(self, site_item: "SiteItem") -> contextmgr: user_id = parsed_response.find(".//t:user", namespaces=self.parent_srv.namespace).get("id", None) auth_token = parsed_response.find("t:credentials", namespaces=self.parent_srv.namespace).get("token", None) self.parent_srv._set_auth(site_id, user_id, auth_token) - logger.info("Signed into {0} as user with id {1}".format(self.parent_srv.server_address, user_id)) + logger.info(f"Signed into {self.parent_srv.server_address} as user with id {user_id}") return Auth.contextmgr(self.sign_out) @api(version="3.10") def revoke_all_server_admin_tokens(self) -> None: - url = "{0}/{1}".format(self.baseurl, "revokeAllServerAdminTokens") + url = f"{self.baseurl}/revokeAllServerAdminTokens" self.post_request(url, "") logger.info("Revoked all tokens for all server admins") diff --git a/tableauserverclient/server/endpoint/custom_views_endpoint.py b/tableauserverclient/server/endpoint/custom_views_endpoint.py index b7fc45d58..63899ba0c 100644 --- a/tableauserverclient/server/endpoint/custom_views_endpoint.py +++ b/tableauserverclient/server/endpoint/custom_views_endpoint.py @@ -2,7 +2,7 @@ import logging import os from pathlib import Path -from typing import List, Optional, Tuple, Union +from typing import Optional, Union from tableauserverclient.config import BYTES_PER_MB, config from tableauserverclient.filesys_helpers import get_file_object_size @@ -33,11 +33,11 @@ class CustomViews(QuerysetEndpoint[CustomViewItem]): def __init__(self, parent_srv): - super(CustomViews, self).__init__(parent_srv) + super().__init__(parent_srv) @property def baseurl(self) -> str: - return "{0}/sites/{1}/customviews".format(self.parent_srv.baseurl, self.parent_srv.site_id) + return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/customviews" @property def expurl(self) -> str: @@ -55,7 +55,7 @@ def expurl(self) -> str: """ @api(version="3.18") - def get(self, req_options: Optional["RequestOptions"] = None) -> Tuple[List[CustomViewItem], PaginationItem]: + def get(self, req_options: Optional["RequestOptions"] = None) -> tuple[list[CustomViewItem], PaginationItem]: logger.info("Querying all custom views on site") url = self.baseurl server_response = self.get_request(url, req_options) @@ -68,8 +68,8 @@ def get_by_id(self, view_id: str) -> Optional[CustomViewItem]: if not view_id: error = "Custom view item missing ID." raise MissingRequiredFieldError(error) - logger.info("Querying custom view (ID: {0})".format(view_id)) - url = "{0}/{1}".format(self.baseurl, view_id) + logger.info(f"Querying custom view (ID: {view_id})") + url = f"{self.baseurl}/{view_id}" server_response = self.get_request(url) return CustomViewItem.from_response(server_response.content, self.parent_srv.namespace) @@ -83,10 +83,10 @@ def image_fetcher(): return self._get_view_image(view_item, req_options) view_item._set_image(image_fetcher) - logger.info("Populated image for custom view (ID: {0})".format(view_item.id)) + logger.info(f"Populated image for custom view (ID: {view_item.id})") def _get_view_image(self, view_item: CustomViewItem, req_options: Optional["ImageRequestOptions"]) -> bytes: - url = "{0}/{1}/image".format(self.baseurl, view_item.id) + url = f"{self.baseurl}/{view_item.id}/image" server_response = self.get_request(url, req_options) image = server_response.content return image @@ -105,10 +105,10 @@ def update(self, view_item: CustomViewItem) -> Optional[CustomViewItem]: return view_item # Update the custom view owner or name - url = "{0}/{1}".format(self.baseurl, view_item.id) + url = f"{self.baseurl}/{view_item.id}" update_req = RequestFactory.CustomView.update_req(view_item) server_response = self.put_request(url, update_req) - logger.info("Updated custom view (ID: {0})".format(view_item.id)) + logger.info(f"Updated custom view (ID: {view_item.id})") return CustomViewItem.from_response(server_response.content, self.parent_srv.namespace) # Delete 1 view by id @@ -117,9 +117,9 @@ def delete(self, view_id: str) -> None: if not view_id: error = "Custom View ID undefined." raise ValueError(error) - url = "{0}/{1}".format(self.baseurl, view_id) + url = f"{self.baseurl}/{view_id}" self.delete_request(url) - logger.info("Deleted single custom view (ID: {0})".format(view_id)) + logger.info(f"Deleted single custom view (ID: {view_id})") @api(version="3.21") def download(self, view_item: CustomViewItem, file: PathOrFileW) -> PathOrFileW: diff --git a/tableauserverclient/server/endpoint/data_acceleration_report_endpoint.py b/tableauserverclient/server/endpoint/data_acceleration_report_endpoint.py index 256a6e766..579001156 100644 --- a/tableauserverclient/server/endpoint/data_acceleration_report_endpoint.py +++ b/tableauserverclient/server/endpoint/data_acceleration_report_endpoint.py @@ -10,14 +10,14 @@ class DataAccelerationReport(Endpoint): def __init__(self, parent_srv): - super(DataAccelerationReport, self).__init__(parent_srv) + super().__init__(parent_srv) self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl) self._default_permissions = _DefaultPermissionsEndpoint(parent_srv, lambda: self.baseurl) @property def baseurl(self): - return "{0}/sites/{1}/dataAccelerationReport".format(self.parent_srv.baseurl, self.parent_srv.site_id) + return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/dataAccelerationReport" @api(version="3.8") def get(self, req_options=None): diff --git a/tableauserverclient/server/endpoint/data_alert_endpoint.py b/tableauserverclient/server/endpoint/data_alert_endpoint.py index fd02d2e4a..ba3ecd74f 100644 --- a/tableauserverclient/server/endpoint/data_alert_endpoint.py +++ b/tableauserverclient/server/endpoint/data_alert_endpoint.py @@ -7,7 +7,7 @@ from tableauserverclient.helpers.logging import logger -from typing import List, Optional, TYPE_CHECKING, Tuple, Union +from typing import Optional, TYPE_CHECKING, Union if TYPE_CHECKING: @@ -17,14 +17,14 @@ class DataAlerts(Endpoint): def __init__(self, parent_srv: "Server") -> None: - super(DataAlerts, self).__init__(parent_srv) + super().__init__(parent_srv) @property def baseurl(self) -> str: - return "{0}/sites/{1}/dataAlerts".format(self.parent_srv.baseurl, self.parent_srv.site_id) + return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/dataAlerts" @api(version="3.2") - def get(self, req_options: Optional["RequestOptions"] = None) -> Tuple[List[DataAlertItem], PaginationItem]: + def get(self, req_options: Optional["RequestOptions"] = None) -> tuple[list[DataAlertItem], PaginationItem]: logger.info("Querying all dataAlerts on site") url = self.baseurl server_response = self.get_request(url, req_options) @@ -38,8 +38,8 @@ def get_by_id(self, dataAlert_id: str) -> DataAlertItem: if not dataAlert_id: error = "dataAlert ID undefined." raise ValueError(error) - logger.info("Querying single dataAlert (ID: {0})".format(dataAlert_id)) - url = "{0}/{1}".format(self.baseurl, dataAlert_id) + logger.info(f"Querying single dataAlert (ID: {dataAlert_id})") + url = f"{self.baseurl}/{dataAlert_id}" server_response = self.get_request(url) return DataAlertItem.from_response(server_response.content, self.parent_srv.namespace)[0] @@ -55,9 +55,9 @@ def delete(self, dataAlert: Union[DataAlertItem, str]) -> None: error = "Dataalert ID undefined." raise ValueError(error) # DELETE /api/api-version/sites/site-id/dataAlerts/data-alert-id/users/user-id - url = "{0}/{1}".format(self.baseurl, dataAlert_id) + url = f"{self.baseurl}/{dataAlert_id}" self.delete_request(url) - logger.info("Deleted single dataAlert (ID: {0})".format(dataAlert_id)) + logger.info(f"Deleted single dataAlert (ID: {dataAlert_id})") @api(version="3.2") def delete_user_from_alert(self, dataAlert: Union[DataAlertItem, str], user: Union[UserItem, str]) -> None: @@ -80,9 +80,9 @@ def delete_user_from_alert(self, dataAlert: Union[DataAlertItem, str], user: Uni error = "User ID undefined." raise ValueError(error) # DELETE /api/api-version/sites/site-id/dataAlerts/data-alert-id/users/user-id - url = "{0}/{1}/users/{2}".format(self.baseurl, dataAlert_id, user_id) + url = f"{self.baseurl}/{dataAlert_id}/users/{user_id}" self.delete_request(url) - logger.info("Deleted User (ID {0}) from dataAlert (ID: {1})".format(user_id, dataAlert_id)) + logger.info(f"Deleted User (ID {user_id}) from dataAlert (ID: {dataAlert_id})") @api(version="3.2") def add_user_to_alert(self, dataAlert_item: DataAlertItem, user: Union[UserItem, str]) -> UserItem: @@ -98,10 +98,10 @@ def add_user_to_alert(self, dataAlert_item: DataAlertItem, user: Union[UserItem, if not user_id: error = "User ID undefined." raise ValueError(error) - url = "{0}/{1}/users".format(self.baseurl, dataAlert_item.id) + url = f"{self.baseurl}/{dataAlert_item.id}/users" update_req = RequestFactory.DataAlert.add_user_to_alert(dataAlert_item, user_id) server_response = self.post_request(url, update_req) - logger.info("Added user (ID {0}) to dataAlert item (ID: {1})".format(user_id, dataAlert_item.id)) + logger.info(f"Added user (ID {user_id}) to dataAlert item (ID: {dataAlert_item.id})") added_user = UserItem.from_response(server_response.content, self.parent_srv.namespace)[0] return added_user @@ -111,9 +111,9 @@ def update(self, dataAlert_item: DataAlertItem) -> DataAlertItem: error = "Dataalert item missing ID." raise MissingRequiredFieldError(error) - url = "{0}/{1}".format(self.baseurl, dataAlert_item.id) + url = f"{self.baseurl}/{dataAlert_item.id}" update_req = RequestFactory.DataAlert.update_req(dataAlert_item) server_response = self.put_request(url, update_req) - logger.info("Updated dataAlert item (ID: {0})".format(dataAlert_item.id)) + logger.info(f"Updated dataAlert item (ID: {dataAlert_item.id})") updated_dataAlert = DataAlertItem.from_response(server_response.content, self.parent_srv.namespace)[0] return updated_dataAlert diff --git a/tableauserverclient/server/endpoint/databases_endpoint.py b/tableauserverclient/server/endpoint/databases_endpoint.py index 2f8fece07..c0e106eb2 100644 --- a/tableauserverclient/server/endpoint/databases_endpoint.py +++ b/tableauserverclient/server/endpoint/databases_endpoint.py @@ -1,5 +1,6 @@ import logging -from typing import Union, Iterable, Set +from typing import Union +from collections.abc import Iterable from tableauserverclient.server.endpoint.default_permissions_endpoint import _DefaultPermissionsEndpoint from tableauserverclient.server.endpoint.dqw_endpoint import _DataQualityWarningEndpoint @@ -15,7 +16,7 @@ class Databases(Endpoint, TaggingMixin): def __init__(self, parent_srv): - super(Databases, self).__init__(parent_srv) + super().__init__(parent_srv) self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl) self._default_permissions = _DefaultPermissionsEndpoint(parent_srv, lambda: self.baseurl) @@ -23,7 +24,7 @@ def __init__(self, parent_srv): @property def baseurl(self): - return "{0}/sites/{1}/databases".format(self.parent_srv.baseurl, self.parent_srv.site_id) + return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/databases" @api(version="3.5") def get(self, req_options=None): @@ -40,8 +41,8 @@ def get_by_id(self, database_id): if not database_id: error = "database ID undefined." raise ValueError(error) - logger.info("Querying single database (ID: {0})".format(database_id)) - url = "{0}/{1}".format(self.baseurl, database_id) + logger.info(f"Querying single database (ID: {database_id})") + url = f"{self.baseurl}/{database_id}" server_response = self.get_request(url) return DatabaseItem.from_response(server_response.content, self.parent_srv.namespace)[0] @@ -50,9 +51,9 @@ def delete(self, database_id): if not database_id: error = "Database ID undefined." raise ValueError(error) - url = "{0}/{1}".format(self.baseurl, database_id) + url = f"{self.baseurl}/{database_id}" self.delete_request(url) - logger.info("Deleted single database (ID: {0})".format(database_id)) + logger.info(f"Deleted single database (ID: {database_id})") @api(version="3.5") def update(self, database_item): @@ -60,10 +61,10 @@ def update(self, database_item): error = "Database item missing ID." raise MissingRequiredFieldError(error) - url = "{0}/{1}".format(self.baseurl, database_item.id) + url = f"{self.baseurl}/{database_item.id}" update_req = RequestFactory.Database.update_req(database_item) server_response = self.put_request(url, update_req) - logger.info("Updated database item (ID: {0})".format(database_item.id)) + logger.info(f"Updated database item (ID: {database_item.id})") updated_database = DatabaseItem.from_response(server_response.content, self.parent_srv.namespace)[0] return updated_database @@ -78,10 +79,10 @@ def column_fetcher(): return self._get_tables_for_database(database_item) database_item._set_tables(column_fetcher) - logger.info("Populated tables for database (ID: {0}".format(database_item.id)) + logger.info(f"Populated tables for database (ID: {database_item.id}") def _get_tables_for_database(self, database_item): - url = "{0}/{1}/tables".format(self.baseurl, database_item.id) + url = f"{self.baseurl}/{database_item.id}/tables" server_response = self.get_request(url) tables = TableItem.from_response(server_response.content, self.parent_srv.namespace) return tables @@ -127,7 +128,7 @@ def delete_dqw(self, item): self._data_quality_warnings.clear(item) @api(version="3.9") - def add_tags(self, item: Union[DatabaseItem, str], tags: Iterable[str]) -> Set[str]: + def add_tags(self, item: Union[DatabaseItem, str], tags: Iterable[str]) -> set[str]: return super().add_tags(item, tags) @api(version="3.9") diff --git a/tableauserverclient/server/endpoint/datasources_endpoint.py b/tableauserverclient/server/endpoint/datasources_endpoint.py index 30fd0b386..6bd809c28 100644 --- a/tableauserverclient/server/endpoint/datasources_endpoint.py +++ b/tableauserverclient/server/endpoint/datasources_endpoint.py @@ -6,7 +6,8 @@ from contextlib import closing from pathlib import Path -from typing import Iterable, List, Mapping, Optional, Sequence, Set, Tuple, TYPE_CHECKING, Union +from typing import Optional, TYPE_CHECKING, Union +from collections.abc import Iterable, Mapping, Sequence from tableauserverclient.helpers.headers import fix_filename from tableauserverclient.server.query import QuerySet @@ -57,7 +58,7 @@ class Datasources(QuerysetEndpoint[DatasourceItem], TaggingMixin[DatasourceItem]): def __init__(self, parent_srv: "Server") -> None: - super(Datasources, self).__init__(parent_srv) + super().__init__(parent_srv) self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl) self._data_quality_warnings = _DataQualityWarningEndpoint(self.parent_srv, "datasource") @@ -65,11 +66,11 @@ def __init__(self, parent_srv: "Server") -> None: @property def baseurl(self) -> str: - return "{0}/sites/{1}/datasources".format(self.parent_srv.baseurl, self.parent_srv.site_id) + return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/datasources" # Get all datasources @api(version="2.0") - def get(self, req_options: Optional[RequestOptions] = None) -> Tuple[List[DatasourceItem], PaginationItem]: + def get(self, req_options: Optional[RequestOptions] = None) -> tuple[list[DatasourceItem], PaginationItem]: logger.info("Querying all datasources on site") url = self.baseurl server_response = self.get_request(url, req_options) @@ -83,8 +84,8 @@ def get_by_id(self, datasource_id: str) -> DatasourceItem: if not datasource_id: error = "Datasource ID undefined." raise ValueError(error) - logger.info("Querying single datasource (ID: {0})".format(datasource_id)) - url = "{0}/{1}".format(self.baseurl, datasource_id) + logger.info(f"Querying single datasource (ID: {datasource_id})") + url = f"{self.baseurl}/{datasource_id}" server_response = self.get_request(url) return DatasourceItem.from_response(server_response.content, self.parent_srv.namespace)[0] @@ -99,10 +100,10 @@ def connections_fetcher(): return self._get_datasource_connections(datasource_item) datasource_item._set_connections(connections_fetcher) - logger.info("Populated connections for datasource (ID: {0})".format(datasource_item.id)) + logger.info(f"Populated connections for datasource (ID: {datasource_item.id})") def _get_datasource_connections(self, datasource_item, req_options=None): - url = "{0}/{1}/connections".format(self.baseurl, datasource_item.id) + url = f"{self.baseurl}/{datasource_item.id}/connections" server_response = self.get_request(url, req_options) connections = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace) return connections @@ -113,9 +114,9 @@ def delete(self, datasource_id: str) -> None: if not datasource_id: error = "Datasource ID undefined." raise ValueError(error) - url = "{0}/{1}".format(self.baseurl, datasource_id) + url = f"{self.baseurl}/{datasource_id}" self.delete_request(url) - logger.info("Deleted single datasource (ID: {0})".format(datasource_id)) + logger.info(f"Deleted single datasource (ID: {datasource_id})") # Download 1 datasource by id @api(version="2.0") @@ -152,11 +153,11 @@ def update(self, datasource_item: DatasourceItem) -> DatasourceItem: self.update_tags(datasource_item) # Update the datasource itself - url = "{0}/{1}".format(self.baseurl, datasource_item.id) + url = f"{self.baseurl}/{datasource_item.id}" update_req = RequestFactory.Datasource.update_req(datasource_item) server_response = self.put_request(url, update_req) - logger.info("Updated datasource item (ID: {0})".format(datasource_item.id)) + logger.info(f"Updated datasource item (ID: {datasource_item.id})") updated_datasource = copy.copy(datasource_item) return updated_datasource._parse_common_elements(server_response.content, self.parent_srv.namespace) @@ -165,7 +166,7 @@ def update(self, datasource_item: DatasourceItem) -> DatasourceItem: 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) + url = f"{self.baseurl}/{datasource_item.id}/connections/{connection_item.id}" update_req = RequestFactory.Connection.update_req(connection_item) server_response = self.put_request(url, update_req) @@ -174,18 +175,16 @@ def update_connection( return None if len(connections) > 1: - logger.debug("Multiple connections returned ({0})".format(len(connections))) + logger.debug(f"Multiple connections returned ({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) - ) + logger.info(f"Updated datasource item (ID: {datasource_item.id} & connection item {connection_item.id}") return connection @api(version="2.8") def refresh(self, datasource_item: DatasourceItem) -> JobItem: id_ = getattr(datasource_item, "id", datasource_item) - url = "{0}/{1}/refresh".format(self.baseurl, id_) + url = f"{self.baseurl}/{id_}/refresh" empty_req = RequestFactory.Empty.empty_req() server_response = self.post_request(url, empty_req) new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0] @@ -194,7 +193,7 @@ def refresh(self, datasource_item: DatasourceItem) -> JobItem: @api(version="3.5") def create_extract(self, datasource_item: DatasourceItem, encrypt: bool = False) -> JobItem: id_ = getattr(datasource_item, "id", datasource_item) - url = "{0}/{1}/createExtract?encrypt={2}".format(self.baseurl, id_, encrypt) + url = f"{self.baseurl}/{id_}/createExtract?encrypt={encrypt}" empty_req = RequestFactory.Empty.empty_req() server_response = self.post_request(url, empty_req) new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0] @@ -203,7 +202,7 @@ def create_extract(self, datasource_item: DatasourceItem, encrypt: bool = False) @api(version="3.5") def delete_extract(self, datasource_item: DatasourceItem) -> None: id_ = getattr(datasource_item, "id", datasource_item) - url = "{0}/{1}/deleteExtract".format(self.baseurl, id_) + url = f"{self.baseurl}/{id_}/deleteExtract" empty_req = RequestFactory.Empty.empty_req() self.post_request(url, empty_req) @@ -223,12 +222,12 @@ def publish( if isinstance(file, (os.PathLike, str)): if not os.path.isfile(file): error = "File path does not lead to an existing file." - raise IOError(error) + raise OSError(error) 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)) + logger.debug(f"Publishing file `{filename}`, size `{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] @@ -247,10 +246,10 @@ def publish( elif file_type == "xml": file_extension = "tds" else: - error = "Unsupported file type {}".format(file_type) + error = f"Unsupported file type {file_type}" raise ValueError(error) - filename = "{}.{}".format(datasource_item.name, file_extension) + filename = f"{datasource_item.name}.{file_extension}" file_size = get_file_object_size(file) else: @@ -261,12 +260,12 @@ def publish( raise ValueError(error) # Construct the url with the defined mode - url = "{0}?datasourceType={1}".format(self.baseurl, file_extension) + url = f"{self.baseurl}?datasourceType={file_extension}" if mode == self.parent_srv.PublishMode.Overwrite or mode == self.parent_srv.PublishMode.Append: - url += "&{0}=true".format(mode.lower()) + url += f"&{mode.lower()}=true" if as_job: - url += "&{0}=true".format("asJob") + url += "&{}=true".format("asJob") # Determine if chunking is required (64MB is the limit for single upload method) if file_size >= config.FILESIZE_LIMIT_MB * BYTES_PER_MB: @@ -276,12 +275,12 @@ def publish( ) ) upload_session_id = self.parent_srv.fileuploads.upload(file) - url = "{0}&uploadSessionId={1}".format(url, upload_session_id) + url = f"{url}&uploadSessionId={upload_session_id}" xml_request, content_type = RequestFactory.Datasource.publish_req_chunked( datasource_item, connection_credentials, connections ) else: - logger.info("Publishing {0} to server".format(filename)) + logger.info(f"Publishing {filename} to server") if isinstance(file, (Path, str)): with open(file, "rb") as f: @@ -309,11 +308,11 @@ def publish( if as_job: new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0] - logger.info("Published {0} (JOB_ID: {1}".format(filename, new_job.id)) + logger.info(f"Published {filename} (JOB_ID: {new_job.id}") return new_job else: new_datasource = DatasourceItem.from_response(server_response.content, self.parent_srv.namespace)[0] - logger.info("Published {0} (ID: {1})".format(filename, new_datasource.id)) + logger.info(f"Published {filename} (ID: {new_datasource.id})") return new_datasource @api(version="3.13") @@ -327,23 +326,23 @@ def update_hyper_data( ) -> JobItem: if isinstance(datasource_or_connection_item, DatasourceItem): datasource_id = datasource_or_connection_item.id - url = "{0}/{1}/data".format(self.baseurl, datasource_id) + url = f"{self.baseurl}/{datasource_id}/data" elif isinstance(datasource_or_connection_item, ConnectionItem): datasource_id = datasource_or_connection_item.datasource_id connection_id = datasource_or_connection_item.id - url = "{0}/{1}/connections/{2}/data".format(self.baseurl, datasource_id, connection_id) + url = f"{self.baseurl}/{datasource_id}/connections/{connection_id}/data" else: assert isinstance(datasource_or_connection_item, str) - url = "{0}/{1}/data".format(self.baseurl, datasource_or_connection_item) + url = f"{self.baseurl}/{datasource_or_connection_item}/data" if payload is not None: if not os.path.isfile(payload): error = "File path does not lead to an existing file." - raise IOError(error) + raise OSError(error) - logger.info("Uploading {0} to server with chunking method for Update job".format(payload)) + logger.info(f"Uploading {payload} to server with chunking method for Update job") upload_session_id = self.parent_srv.fileuploads.upload(payload) - url = "{0}?uploadSessionId={1}".format(url, upload_session_id) + url = f"{url}?uploadSessionId={upload_session_id}" json_request = json.dumps({"actions": actions}) parameters = {"headers": {"requestid": request_id}} @@ -356,7 +355,7 @@ def populate_permissions(self, item: DatasourceItem) -> None: self._permissions.populate(item) @api(version="2.0") - def update_permissions(self, item: DatasourceItem, permission_item: List["PermissionsRule"]) -> None: + def update_permissions(self, item: DatasourceItem, permission_item: list["PermissionsRule"]) -> None: self._permissions.update(item, permission_item) @api(version="2.0") @@ -390,12 +389,12 @@ def revisions_fetcher(): return self._get_datasource_revisions(datasource_item) datasource_item._set_revisions(revisions_fetcher) - logger.info("Populated revisions for datasource (ID: {0})".format(datasource_item.id)) + logger.info(f"Populated revisions for datasource (ID: {datasource_item.id})") def _get_datasource_revisions( self, datasource_item: DatasourceItem, req_options: Optional["RequestOptions"] = None - ) -> List[RevisionItem]: - url = "{0}/{1}/revisions".format(self.baseurl, datasource_item.id) + ) -> list[RevisionItem]: + url = f"{self.baseurl}/{datasource_item.id}/revisions" server_response = self.get_request(url, req_options) revisions = RevisionItem.from_response(server_response.content, self.parent_srv.namespace, datasource_item) return revisions @@ -413,9 +412,9 @@ def download_revision( error = "Datasource ID undefined." raise ValueError(error) if revision_number is None: - url = "{0}/{1}/content".format(self.baseurl, datasource_id) + url = f"{self.baseurl}/{datasource_id}/content" else: - url = "{0}/{1}/revisions/{2}/content".format(self.baseurl, datasource_id, revision_number) + url = f"{self.baseurl}/{datasource_id}/revisions/{revision_number}/content" if not include_extract: url += "?includeExtract=False" @@ -437,9 +436,7 @@ def download_revision( f.write(chunk) return_path = os.path.abspath(download_path) - logger.info( - "Downloaded datasource revision {0} to {1} (ID: {2})".format(revision_number, return_path, datasource_id) - ) + logger.info(f"Downloaded datasource revision {revision_number} to {return_path} (ID: {datasource_id})") return return_path @api(version="2.3") @@ -449,19 +446,17 @@ def delete_revision(self, datasource_id: str, revision_number: str) -> None: url = "/".join([self.baseurl, datasource_id, "revisions", revision_number]) self.delete_request(url) - logger.info( - "Deleted single datasource revision (ID: {0}) (Revision: {1})".format(datasource_id, revision_number) - ) + logger.info(f"Deleted single datasource revision (ID: {datasource_id}) (Revision: {revision_number})") # a convenience method @api(version="2.8") def schedule_extract_refresh( self, schedule_id: str, item: DatasourceItem - ) -> List["AddResponse"]: # actually should return a task + ) -> list["AddResponse"]: # actually should return a task return self.parent_srv.schedules.add_to_schedule(schedule_id, datasource=item) @api(version="1.0") - def add_tags(self, item: Union[DatasourceItem, str], tags: Union[Iterable[str], str]) -> Set[str]: + def add_tags(self, item: Union[DatasourceItem, str], tags: Union[Iterable[str], str]) -> set[str]: return super().add_tags(item, tags) @api(version="1.0") diff --git a/tableauserverclient/server/endpoint/default_permissions_endpoint.py b/tableauserverclient/server/endpoint/default_permissions_endpoint.py index 19112d713..343d8b097 100644 --- a/tableauserverclient/server/endpoint/default_permissions_endpoint.py +++ b/tableauserverclient/server/endpoint/default_permissions_endpoint.py @@ -4,7 +4,8 @@ from .exceptions import MissingRequiredFieldError from tableauserverclient.server import RequestFactory from tableauserverclient.models import DatabaseItem, PermissionsRule, ProjectItem, plural_type, Resource -from typing import TYPE_CHECKING, Callable, List, Optional, Sequence, Union +from typing import TYPE_CHECKING, Callable, Optional, Union +from collections.abc import Sequence if TYPE_CHECKING: from ..server import Server @@ -25,7 +26,7 @@ class _DefaultPermissionsEndpoint(Endpoint): """ def __init__(self, parent_srv: "Server", owner_baseurl: Callable[[], str]) -> None: - super(_DefaultPermissionsEndpoint, self).__init__(parent_srv) + super().__init__(parent_srv) # owner_baseurl is the baseurl of the parent, a project or database. # It MUST be a lambda since we don't know the full site URL until we sign in. @@ -33,18 +34,18 @@ def __init__(self, parent_srv: "Server", owner_baseurl: Callable[[], str]) -> No self.owner_baseurl = owner_baseurl def __str__(self): - return "".format(self.owner_baseurl()) + return f"" __repr__ = __str__ def update_default_permissions( self, resource: BaseItem, permissions: Sequence[PermissionsRule], content_type: Resource - ) -> List[PermissionsRule]: - url = "{0}/{1}/default-permissions/{2}".format(self.owner_baseurl(), resource.id, plural_type(content_type)) + ) -> list[PermissionsRule]: + url = f"{self.owner_baseurl()}/{resource.id}/default-permissions/{plural_type(content_type)}" update_req = RequestFactory.Permission.add_req(permissions) response = self.put_request(url, update_req) permissions = PermissionsRule.from_response(response.content, self.parent_srv.namespace) - logger.info("Updated default {} permissions for resource {}".format(content_type, resource.id)) + logger.info(f"Updated default {content_type} permissions for resource {resource.id}") logger.info(permissions) return permissions @@ -65,29 +66,27 @@ def delete_default_permission(self, resource: BaseItem, rule: PermissionsRule, c ) ) - logger.debug("Removing {0} permission for capability {1}".format(mode, capability)) + logger.debug(f"Removing {mode} permission for capability {capability}") self.delete_request(url) - logger.info( - "Deleted permission for {0} {1} item {2}".format(rule.grantee.tag_name, rule.grantee.id, resource.id) - ) + logger.info(f"Deleted permission for {rule.grantee.tag_name} {rule.grantee.id} item {resource.id}") def populate_default_permissions(self, item: BaseItem, content_type: Resource) -> None: if not item.id: error = "Server item is missing ID. Item must be retrieved from server first." raise MissingRequiredFieldError(error) - def permission_fetcher() -> List[PermissionsRule]: + def permission_fetcher() -> list[PermissionsRule]: return self._get_default_permissions(item, content_type) item._set_default_permissions(permission_fetcher, content_type) - logger.info("Populated default {0} permissions for item (ID: {1})".format(content_type, item.id)) + logger.info(f"Populated default {content_type} permissions for item (ID: {item.id})") def _get_default_permissions( self, item: BaseItem, content_type: Resource, req_options: Optional["RequestOptions"] = None - ) -> List[PermissionsRule]: - url = "{0}/{1}/default-permissions/{2}".format(self.owner_baseurl(), item.id, plural_type(content_type)) + ) -> list[PermissionsRule]: + url = f"{self.owner_baseurl()}/{item.id}/default-permissions/{plural_type(content_type)}" server_response = self.get_request(url, req_options) permissions = PermissionsRule.from_response(server_response.content, self.parent_srv.namespace) logger.info({"content_type": content_type, "permissions": permissions}) diff --git a/tableauserverclient/server/endpoint/dqw_endpoint.py b/tableauserverclient/server/endpoint/dqw_endpoint.py index 5296523ee..90e31483b 100644 --- a/tableauserverclient/server/endpoint/dqw_endpoint.py +++ b/tableauserverclient/server/endpoint/dqw_endpoint.py @@ -10,35 +10,35 @@ class _DataQualityWarningEndpoint(Endpoint): def __init__(self, parent_srv, resource_type): - super(_DataQualityWarningEndpoint, self).__init__(parent_srv) + super().__init__(parent_srv) self.resource_type = resource_type @property def baseurl(self): - return "{0}/sites/{1}/dataQualityWarnings/{2}".format( + return "{}/sites/{}/dataQualityWarnings/{}".format( self.parent_srv.baseurl, self.parent_srv.site_id, self.resource_type ) def add(self, resource, warning): - url = "{baseurl}/{content_luid}".format(baseurl=self.baseurl, content_luid=resource.id) + url = f"{self.baseurl}/{resource.id}" add_req = RequestFactory.DQW.add_req(warning) response = self.post_request(url, add_req) warnings = DQWItem.from_response(response.content, self.parent_srv.namespace) - logger.info("Added dqw for resource {0}".format(resource.id)) + logger.info(f"Added dqw for resource {resource.id}") return warnings def update(self, resource, warning): - url = "{baseurl}/{content_luid}".format(baseurl=self.baseurl, content_luid=resource.id) + url = f"{self.baseurl}/{resource.id}" add_req = RequestFactory.DQW.update_req(warning) response = self.put_request(url, add_req) warnings = DQWItem.from_response(response.content, self.parent_srv.namespace) - logger.info("Added dqw for resource {0}".format(resource.id)) + logger.info(f"Added dqw for resource {resource.id}") return warnings def clear(self, resource): - url = "{baseurl}/{content_luid}".format(baseurl=self.baseurl, content_luid=resource.id) + url = f"{self.baseurl}/{resource.id}" return self.delete_request(url) def populate(self, item): @@ -50,10 +50,10 @@ def dqw_fetcher(): return self._get_data_quality_warnings(item) item._set_data_quality_warnings(dqw_fetcher) - logger.info("Populated permissions for item (ID: {0})".format(item.id)) + logger.info(f"Populated permissions for item (ID: {item.id})") def _get_data_quality_warnings(self, item, req_options=None): - url = "{baseurl}/{content_luid}".format(baseurl=self.baseurl, content_luid=item.id) + url = f"{self.baseurl}/{item.id}" server_response = self.get_request(url, req_options) dqws = DQWItem.from_response(server_response.content, self.parent_srv.namespace) diff --git a/tableauserverclient/server/endpoint/endpoint.py b/tableauserverclient/server/endpoint/endpoint.py index be0602df5..bef96fdee 100644 --- a/tableauserverclient/server/endpoint/endpoint.py +++ b/tableauserverclient/server/endpoint/endpoint.py @@ -8,12 +8,9 @@ from typing import ( Any, Callable, - Dict, Generic, - List, Optional, TYPE_CHECKING, - Tuple, TypeVar, Union, ) @@ -56,7 +53,7 @@ def __init__(self, parent_srv: "Server"): async_response = None @staticmethod - def set_parameters(http_options, auth_token, content, content_type, parameters) -> Dict[str, Any]: + def set_parameters(http_options, auth_token, content, content_type, parameters) -> dict[str, Any]: parameters = parameters or {} parameters.update(http_options) if "headers" not in parameters: @@ -82,7 +79,7 @@ def set_user_agent(parameters): else: # only set the TSC user agent if not already populated _client_version: Optional[str] = get_versions()["version"] - parameters["headers"][USER_AGENT_HEADER] = "Tableau Server Client/{}".format(_client_version) + parameters["headers"][USER_AGENT_HEADER] = f"Tableau Server Client/{_client_version}" # result: parameters["headers"]["User-Agent"] is set # return explicitly for testing only @@ -90,12 +87,12 @@ def set_user_agent(parameters): def _blocking_request(self, method, url, parameters={}) -> Optional[Union["Response", Exception]]: response = None - logger.debug("[{}] Begin blocking request to {}".format(datetime.timestamp(), url)) + logger.debug(f"[{datetime.timestamp()}] Begin blocking request to {url}") try: response = method(url, **parameters) - logger.debug("[{}] Call finished".format(datetime.timestamp())) + logger.debug(f"[{datetime.timestamp()}] Call finished") except Exception as e: - logger.debug("Error making request to server: {}".format(e)) + logger.debug(f"Error making request to server: {e}") raise e return response @@ -111,13 +108,13 @@ def _make_request( content: Optional[bytes] = None, auth_token: Optional[str] = None, content_type: Optional[str] = None, - parameters: Optional[Dict[str, Any]] = None, + parameters: Optional[dict[str, Any]] = None, ) -> "Response": parameters = Endpoint.set_parameters( self.parent_srv.http_options, auth_token, content, content_type, parameters ) - logger.debug("request method {}, url: {}".format(method.__name__, url)) + logger.debug(f"request method {method.__name__}, url: {url}") if content: redacted = helpers.strings.redact_xml(content[:200]) # this needs to be under a trace or something, it's a LOT @@ -129,14 +126,14 @@ def _make_request( server_response: Optional[Union["Response", Exception]] = self.send_request_while_show_progress_threaded( method, url, parameters, request_timeout ) - logger.debug("[{}] Async request returned: received {}".format(datetime.timestamp(), server_response)) + logger.debug(f"[{datetime.timestamp()}] Async request returned: received {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())) + logger.debug(f"[{datetime.timestamp()}] Async request failed: retrying") server_response = self._blocking_request(method, url, parameters) if server_response is None: - logger.debug("[{}] Request failed".format(datetime.timestamp())) + logger.debug(f"[{datetime.timestamp()}] Request failed") raise RuntimeError if isinstance(server_response, Exception): raise server_response @@ -154,9 +151,9 @@ def _make_request( return server_response def _check_status(self, server_response: "Response", url: Optional[str] = None): - logger.debug("Response status: {}".format(server_response)) + logger.debug(f"Response status: {server_response}") if not hasattr(server_response, "status_code"): - raise EnvironmentError("Response is not a http response?") + raise OSError("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: @@ -183,9 +180,9 @@ def log_response_safely(self, server_response: "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 = f"Content type `{content_type}`" if content_type == "application/octet-stream": - loggable_response = "A stream of type {} [Truncated File Contents]".format(content_type) + loggable_response = f"A stream of type {content_type} [Truncated File Contents]" elif server_response.encoding and len(server_response.content) > 0: loggable_response = helpers.strings.redact_xml(server_response.content.decode(server_response.encoding)) return loggable_response @@ -313,7 +310,7 @@ def wrapper(self: E, *args: P.args, **kwargs: P.kwargs) -> R: for p in params_to_check: min_ver = Version(str(params[p])) if server_ver < min_ver: - error = "{!r} not available in {}, it will be ignored. Added in {}".format(p, server_ver, min_ver) + error = f"{p!r} not available in {server_ver}, it will be ignored. Added in {min_ver}" warnings.warn(error) return func(self, *args, **kwargs) @@ -353,5 +350,5 @@ def paginate(self, **kwargs) -> QuerySet[T]: return queryset @abc.abstractmethod - def get(self, request_options: Optional[RequestOptions] = None) -> Tuple[List[T], PaginationItem]: + def get(self, request_options: Optional[RequestOptions] = None) -> tuple[list[T], PaginationItem]: raise NotImplementedError(f".get has not been implemented for {self.__class__.__qualname__}") diff --git a/tableauserverclient/server/endpoint/exceptions.py b/tableauserverclient/server/endpoint/exceptions.py index 9dfd38da6..17d789d01 100644 --- a/tableauserverclient/server/endpoint/exceptions.py +++ b/tableauserverclient/server/endpoint/exceptions.py @@ -12,10 +12,10 @@ def __init__(self, code, summary, detail, url=None): self.summary = summary self.detail = detail self.url = url - super(ServerResponseError, self).__init__(str(self)) + super().__init__(str(self)) def __str__(self): - return "\n\n\t{0}: {1}\n\t\t{2}".format(self.code, self.summary, self.detail) + return f"\n\n\t{self.code}: {self.summary}\n\t\t{self.detail}" @classmethod def from_response(cls, resp, ns, url=None): @@ -40,7 +40,7 @@ def __init__(self, server_response, request_url: Optional[str] = None): self.url = request_url or "server" def __str__(self): - return "\n\nInternal error {0} at {1}\n{2}".format(self.code, self.url, self.content) + return f"\n\nInternal error {self.code} at {self.url}\n{self.content}" class MissingRequiredFieldError(TableauError): diff --git a/tableauserverclient/server/endpoint/favorites_endpoint.py b/tableauserverclient/server/endpoint/favorites_endpoint.py index 5f298f37e..8330e6d2c 100644 --- a/tableauserverclient/server/endpoint/favorites_endpoint.py +++ b/tableauserverclient/server/endpoint/favorites_endpoint.py @@ -20,13 +20,13 @@ class Favorites(Endpoint): @property def baseurl(self) -> str: - return "{0}/sites/{1}/favorites".format(self.parent_srv.baseurl, self.parent_srv.site_id) + return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/favorites" # Gets all favorites @api(version="2.5") def get(self, user_item: UserItem, req_options: Optional[RequestOptions] = None) -> None: - logger.info("Querying all favorites for user {0}".format(user_item.name)) - url = "{0}/{1}".format(self.baseurl, user_item.id) + logger.info(f"Querying all favorites for user {user_item.name}") + url = f"{self.baseurl}/{user_item.id}" server_response = self.get_request(url, req_options) user_item._favorites = FavoriteItem.from_response(server_response.content, self.parent_srv.namespace) @@ -34,53 +34,53 @@ def get(self, user_item: UserItem, req_options: Optional[RequestOptions] = None) @api(version="3.15") def add_favorite(self, user_item: UserItem, content_type: str, item: TableauItem) -> "Response": - url = "{0}/{1}".format(self.baseurl, user_item.id) + url = f"{self.baseurl}/{user_item.id}" add_req = RequestFactory.Favorite.add_request(item.id, content_type, item.name) server_response = self.put_request(url, add_req) - logger.info("Favorited {0} for user (ID: {1})".format(item.name, user_item.id)) + logger.info(f"Favorited {item.name} for user (ID: {user_item.id})") return server_response @api(version="2.0") def add_favorite_workbook(self, user_item: UserItem, workbook_item: WorkbookItem) -> None: - url = "{0}/{1}".format(self.baseurl, user_item.id) + url = f"{self.baseurl}/{user_item.id}" add_req = RequestFactory.Favorite.add_workbook_req(workbook_item.id, workbook_item.name) server_response = self.put_request(url, add_req) - logger.info("Favorited {0} for user (ID: {1})".format(workbook_item.name, user_item.id)) + logger.info(f"Favorited {workbook_item.name} for user (ID: {user_item.id})") @api(version="2.0") def add_favorite_view(self, user_item: UserItem, view_item: ViewItem) -> None: - url = "{0}/{1}".format(self.baseurl, user_item.id) + url = f"{self.baseurl}/{user_item.id}" add_req = RequestFactory.Favorite.add_view_req(view_item.id, view_item.name) server_response = self.put_request(url, add_req) - logger.info("Favorited {0} for user (ID: {1})".format(view_item.name, user_item.id)) + logger.info(f"Favorited {view_item.name} for user (ID: {user_item.id})") @api(version="2.3") def add_favorite_datasource(self, user_item: UserItem, datasource_item: DatasourceItem) -> None: - url = "{0}/{1}".format(self.baseurl, user_item.id) + url = f"{self.baseurl}/{user_item.id}" add_req = RequestFactory.Favorite.add_datasource_req(datasource_item.id, datasource_item.name) server_response = self.put_request(url, add_req) - logger.info("Favorited {0} for user (ID: {1})".format(datasource_item.name, user_item.id)) + logger.info(f"Favorited {datasource_item.name} for user (ID: {user_item.id})") @api(version="3.1") def add_favorite_project(self, user_item: UserItem, project_item: ProjectItem) -> None: - url = "{0}/{1}".format(self.baseurl, user_item.id) + url = f"{self.baseurl}/{user_item.id}" add_req = RequestFactory.Favorite.add_project_req(project_item.id, project_item.name) server_response = self.put_request(url, add_req) - logger.info("Favorited {0} for user (ID: {1})".format(project_item.name, user_item.id)) + logger.info(f"Favorited {project_item.name} for user (ID: {user_item.id})") @api(version="3.3") def add_favorite_flow(self, user_item: UserItem, flow_item: FlowItem) -> None: - url = "{0}/{1}".format(self.baseurl, user_item.id) + url = f"{self.baseurl}/{user_item.id}" add_req = RequestFactory.Favorite.add_flow_req(flow_item.id, flow_item.name) server_response = self.put_request(url, add_req) - logger.info("Favorited {0} for user (ID: {1})".format(flow_item.name, user_item.id)) + logger.info(f"Favorited {flow_item.name} for user (ID: {user_item.id})") @api(version="3.3") def add_favorite_metric(self, user_item: UserItem, metric_item: MetricItem) -> None: - url = "{0}/{1}".format(self.baseurl, user_item.id) + url = f"{self.baseurl}/{user_item.id}" add_req = RequestFactory.Favorite.add_request(metric_item.id, Resource.Metric, metric_item.name) server_response = self.put_request(url, add_req) - logger.info("Favorited metric {0} for user (ID: {1})".format(metric_item.name, user_item.id)) + logger.info(f"Favorited metric {metric_item.name} for user (ID: {user_item.id})") # ------- delete from favorites # Response: @@ -94,42 +94,42 @@ def add_favorite_metric(self, user_item: UserItem, metric_item: MetricItem) -> N @api(version="3.15") def delete_favorite(self, user_item: UserItem, content_type: Resource, item: TableauItem) -> None: - url = "{0}/{1}/{2}/{3}".format(self.baseurl, user_item.id, content_type, item.id) - logger.info("Removing favorite {0}({1}) for user (ID: {2})".format(content_type, item.id, user_item.id)) + url = f"{self.baseurl}/{user_item.id}/{content_type}/{item.id}" + logger.info(f"Removing favorite {content_type}({item.id}) for user (ID: {user_item.id})") self.delete_request(url) @api(version="2.0") def delete_favorite_workbook(self, user_item: UserItem, workbook_item: WorkbookItem) -> None: - url = "{0}/{1}/workbooks/{2}".format(self.baseurl, user_item.id, workbook_item.id) - logger.info("Removing favorite workbook {0} for user (ID: {1})".format(workbook_item.id, user_item.id)) + url = f"{self.baseurl}/{user_item.id}/workbooks/{workbook_item.id}" + logger.info(f"Removing favorite workbook {workbook_item.id} for user (ID: {user_item.id})") self.delete_request(url) @api(version="2.0") def delete_favorite_view(self, user_item: UserItem, view_item: ViewItem) -> None: - url = "{0}/{1}/views/{2}".format(self.baseurl, user_item.id, view_item.id) - logger.info("Removing favorite view {0} for user (ID: {1})".format(view_item.id, user_item.id)) + url = f"{self.baseurl}/{user_item.id}/views/{view_item.id}" + logger.info(f"Removing favorite view {view_item.id} for user (ID: {user_item.id})") self.delete_request(url) @api(version="2.3") def delete_favorite_datasource(self, user_item: UserItem, datasource_item: DatasourceItem) -> None: - url = "{0}/{1}/datasources/{2}".format(self.baseurl, user_item.id, datasource_item.id) - logger.info("Removing favorite {0} for user (ID: {1})".format(datasource_item.id, user_item.id)) + url = f"{self.baseurl}/{user_item.id}/datasources/{datasource_item.id}" + logger.info(f"Removing favorite {datasource_item.id} for user (ID: {user_item.id})") self.delete_request(url) @api(version="3.1") def delete_favorite_project(self, user_item: UserItem, project_item: ProjectItem) -> None: - url = "{0}/{1}/projects/{2}".format(self.baseurl, user_item.id, project_item.id) - logger.info("Removing favorite project {0} for user (ID: {1})".format(project_item.id, user_item.id)) + url = f"{self.baseurl}/{user_item.id}/projects/{project_item.id}" + logger.info(f"Removing favorite project {project_item.id} for user (ID: {user_item.id})") self.delete_request(url) @api(version="3.3") def delete_favorite_flow(self, user_item: UserItem, flow_item: FlowItem) -> None: - url = "{0}/{1}/flows/{2}".format(self.baseurl, user_item.id, flow_item.id) - logger.info("Removing favorite flow {0} for user (ID: {1})".format(flow_item.id, user_item.id)) + url = f"{self.baseurl}/{user_item.id}/flows/{flow_item.id}" + logger.info(f"Removing favorite flow {flow_item.id} for user (ID: {user_item.id})") self.delete_request(url) @api(version="3.15") def delete_favorite_metric(self, user_item: UserItem, metric_item: MetricItem) -> None: - url = "{0}/{1}/metrics/{2}".format(self.baseurl, user_item.id, metric_item.id) - logger.info("Removing favorite metric {0} for user (ID: {1})".format(metric_item.id, user_item.id)) + url = f"{self.baseurl}/{user_item.id}/metrics/{metric_item.id}" + logger.info(f"Removing favorite metric {metric_item.id} for user (ID: {user_item.id})") self.delete_request(url) diff --git a/tableauserverclient/server/endpoint/fileuploads_endpoint.py b/tableauserverclient/server/endpoint/fileuploads_endpoint.py index 0d30797c1..1ae10e72d 100644 --- a/tableauserverclient/server/endpoint/fileuploads_endpoint.py +++ b/tableauserverclient/server/endpoint/fileuploads_endpoint.py @@ -9,11 +9,11 @@ class Fileuploads(Endpoint): def __init__(self, parent_srv): - super(Fileuploads, self).__init__(parent_srv) + super().__init__(parent_srv) @property def baseurl(self): - return "{0}/sites/{1}/fileUploads".format(self.parent_srv.baseurl, self.parent_srv.site_id) + return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/fileUploads" @api(version="2.0") def initiate(self): @@ -21,14 +21,14 @@ def initiate(self): server_response = self.post_request(url, "") fileupload_item = FileuploadItem.from_response(server_response.content, self.parent_srv.namespace) upload_id = fileupload_item.upload_session_id - logger.info("Initiated file upload session (ID: {0})".format(upload_id)) + logger.info(f"Initiated file upload session (ID: {upload_id})") return upload_id @api(version="2.0") def append(self, upload_id, data, content_type): - url = "{0}/{1}".format(self.baseurl, upload_id) + url = f"{self.baseurl}/{upload_id}" server_response = self.put_request(url, data, content_type) - logger.info("Uploading a chunk to session (ID: {0})".format(upload_id)) + logger.info(f"Uploading a chunk to session (ID: {upload_id})") return FileuploadItem.from_response(server_response.content, self.parent_srv.namespace) def _read_chunks(self, file): @@ -52,12 +52,10 @@ 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())) + logger.debug(f"{datetime.timestamp()} processing chunk...") request, content_type = RequestFactory.Fileupload.chunk_req(chunk) - logger.debug("{} created chunk request".format(datetime.timestamp())) + logger.debug(f"{datetime.timestamp()} created chunk request") fileupload_item = self.append(upload_id, request, content_type) - 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)) + logger.info(f"\t{datetime.timestamp()} Published {(fileupload_item.file_size / BYTES_PER_MB)}MB") + logger.info(f"File upload finished (ID: {upload_id})") return upload_id diff --git a/tableauserverclient/server/endpoint/flow_runs_endpoint.py b/tableauserverclient/server/endpoint/flow_runs_endpoint.py index c339a0645..2c3bb84bc 100644 --- a/tableauserverclient/server/endpoint/flow_runs_endpoint.py +++ b/tableauserverclient/server/endpoint/flow_runs_endpoint.py @@ -1,9 +1,9 @@ import logging -from typing import List, Optional, Tuple, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING, Union from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api from tableauserverclient.server.endpoint.exceptions import FlowRunFailedException, FlowRunCancelledException -from tableauserverclient.models import FlowRunItem, PaginationItem +from tableauserverclient.models import FlowRunItem from tableauserverclient.exponential_backoff import ExponentialBackoffTimer from tableauserverclient.helpers.logging import logger @@ -16,22 +16,24 @@ class FlowRuns(QuerysetEndpoint[FlowRunItem]): def __init__(self, parent_srv: "Server") -> None: - super(FlowRuns, self).__init__(parent_srv) + super().__init__(parent_srv) return None @property def baseurl(self) -> str: - return "{0}/sites/{1}/flows/runs".format(self.parent_srv.baseurl, self.parent_srv.site_id) + return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/flows/runs" # Get all flows @api(version="3.10") - def get(self, req_options: Optional["RequestOptions"] = None) -> Tuple[List[FlowRunItem], PaginationItem]: + # QuerysetEndpoint expects a PaginationItem to be returned, but FlowRuns + # does not return a PaginationItem. Suppressing the mypy error because the + # changes to the QuerySet class should permit this to function regardless. + def get(self, req_options: Optional["RequestOptions"] = None) -> list[FlowRunItem]: # type: ignore[override] logger.info("Querying all flow runs on site") url = self.baseurl server_response = self.get_request(url, req_options) - pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace) all_flow_run_items = FlowRunItem.from_response(server_response.content, self.parent_srv.namespace) - return all_flow_run_items, pagination_item + return all_flow_run_items # Get 1 flow by id @api(version="3.10") @@ -39,21 +41,21 @@ def get_by_id(self, flow_run_id: str) -> FlowRunItem: if not flow_run_id: error = "Flow ID undefined." raise ValueError(error) - logger.info("Querying single flow (ID: {0})".format(flow_run_id)) - url = "{0}/{1}".format(self.baseurl, flow_run_id) + logger.info(f"Querying single flow (ID: {flow_run_id})") + url = f"{self.baseurl}/{flow_run_id}" server_response = self.get_request(url) return FlowRunItem.from_response(server_response.content, self.parent_srv.namespace)[0] # Cancel 1 flow run by id @api(version="3.10") - def cancel(self, flow_run_id: str) -> None: + def cancel(self, flow_run_id: Union[str, FlowRunItem]) -> None: if not flow_run_id: error = "Flow ID undefined." raise ValueError(error) id_ = getattr(flow_run_id, "id", flow_run_id) - url = "{0}/{1}".format(self.baseurl, id_) + url = f"{self.baseurl}/{id_}" self.put_request(url) - logger.info("Deleted single flow (ID: {0})".format(id_)) + logger.info(f"Deleted single flow (ID: {id_})") @api(version="3.10") def wait_for_job(self, flow_run_id: str, *, timeout: Optional[int] = None) -> FlowRunItem: @@ -69,7 +71,7 @@ def wait_for_job(self, flow_run_id: str, *, timeout: Optional[int] = None) -> Fl flow_run = self.get_by_id(flow_run_id) logger.debug(f"\tFlowRun {flow_run_id} progress={flow_run.progress}") - logger.info("FlowRun {} Completed: Status: {}".format(flow_run_id, flow_run.status)) + logger.info(f"FlowRun {flow_run_id} Completed: Status: {flow_run.status}") if flow_run.status == "Success": return flow_run diff --git a/tableauserverclient/server/endpoint/flow_task_endpoint.py b/tableauserverclient/server/endpoint/flow_task_endpoint.py index eea3f9710..9e21661e6 100644 --- a/tableauserverclient/server/endpoint/flow_task_endpoint.py +++ b/tableauserverclient/server/endpoint/flow_task_endpoint.py @@ -1,5 +1,5 @@ import logging -from typing import List, Optional, Tuple, TYPE_CHECKING +from typing import TYPE_CHECKING from tableauserverclient.server.endpoint.endpoint import Endpoint, api from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError @@ -15,7 +15,7 @@ class FlowTasks(Endpoint): @property def baseurl(self) -> str: - return "{0}/sites/{1}/tasks/flows".format(self.parent_srv.baseurl, self.parent_srv.site_id) + return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/tasks/flows" @api(version="3.22") def create(self, flow_item: TaskItem) -> TaskItem: diff --git a/tableauserverclient/server/endpoint/flows_endpoint.py b/tableauserverclient/server/endpoint/flows_endpoint.py index 53d072f50..7eb5dc3ba 100644 --- a/tableauserverclient/server/endpoint/flows_endpoint.py +++ b/tableauserverclient/server/endpoint/flows_endpoint.py @@ -5,7 +5,8 @@ import os from contextlib import closing from pathlib import Path -from typing import Iterable, List, Optional, TYPE_CHECKING, Tuple, Union +from typing import Optional, TYPE_CHECKING, Union +from collections.abc import Iterable from tableauserverclient.helpers.headers import fix_filename @@ -53,18 +54,18 @@ class Flows(QuerysetEndpoint[FlowItem], TaggingMixin[FlowItem]): def __init__(self, parent_srv): - super(Flows, self).__init__(parent_srv) + super().__init__(parent_srv) self._resource_tagger = _ResourceTagger(parent_srv) self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl) self._data_quality_warnings = _DataQualityWarningEndpoint(self.parent_srv, "flow") @property def baseurl(self) -> str: - return "{0}/sites/{1}/flows".format(self.parent_srv.baseurl, self.parent_srv.site_id) + return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/flows" # Get all flows @api(version="3.3") - def get(self, req_options: Optional["RequestOptions"] = None) -> Tuple[List[FlowItem], PaginationItem]: + def get(self, req_options: Optional["RequestOptions"] = None) -> tuple[list[FlowItem], PaginationItem]: logger.info("Querying all flows on site") url = self.baseurl server_response = self.get_request(url, req_options) @@ -78,8 +79,8 @@ def get_by_id(self, flow_id: str) -> FlowItem: if not flow_id: error = "Flow ID undefined." raise ValueError(error) - logger.info("Querying single flow (ID: {0})".format(flow_id)) - url = "{0}/{1}".format(self.baseurl, flow_id) + logger.info(f"Querying single flow (ID: {flow_id})") + url = f"{self.baseurl}/{flow_id}" server_response = self.get_request(url) return FlowItem.from_response(server_response.content, self.parent_srv.namespace)[0] @@ -94,10 +95,10 @@ def connections_fetcher(): return self._get_flow_connections(flow_item) flow_item._set_connections(connections_fetcher) - logger.info("Populated connections for flow (ID: {0})".format(flow_item.id)) + logger.info(f"Populated connections for flow (ID: {flow_item.id})") - def _get_flow_connections(self, flow_item, req_options: Optional["RequestOptions"] = None) -> List[ConnectionItem]: - url = "{0}/{1}/connections".format(self.baseurl, flow_item.id) + def _get_flow_connections(self, flow_item, req_options: Optional["RequestOptions"] = None) -> list[ConnectionItem]: + url = f"{self.baseurl}/{flow_item.id}/connections" server_response = self.get_request(url, req_options) connections = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace) return connections @@ -108,9 +109,9 @@ def delete(self, flow_id: str) -> None: if not flow_id: error = "Flow ID undefined." raise ValueError(error) - url = "{0}/{1}".format(self.baseurl, flow_id) + url = f"{self.baseurl}/{flow_id}" self.delete_request(url) - logger.info("Deleted single flow (ID: {0})".format(flow_id)) + logger.info(f"Deleted single flow (ID: {flow_id})") # Download 1 flow by id @api(version="3.3") @@ -118,7 +119,7 @@ def download(self, flow_id: str, filepath: Optional[PathOrFileW] = None) -> Path if not flow_id: error = "Flow ID undefined." raise ValueError(error) - url = "{0}/{1}/content".format(self.baseurl, flow_id) + url = f"{self.baseurl}/{flow_id}/content" with closing(self.get_request(url, parameters={"stream": True})) as server_response: m = Message() @@ -137,7 +138,7 @@ def download(self, flow_id: str, filepath: Optional[PathOrFileW] = None) -> Path f.write(chunk) return_path = os.path.abspath(download_path) - logger.info("Downloaded flow to {0} (ID: {1})".format(return_path, flow_id)) + logger.info(f"Downloaded flow to {return_path} (ID: {flow_id})") return return_path # Update flow @@ -150,28 +151,28 @@ def update(self, flow_item: FlowItem) -> FlowItem: self._resource_tagger.update_tags(self.baseurl, flow_item) # Update the flow itself - url = "{0}/{1}".format(self.baseurl, flow_item.id) + url = f"{self.baseurl}/{flow_item.id}" update_req = RequestFactory.Flow.update_req(flow_item) server_response = self.put_request(url, update_req) - logger.info("Updated flow item (ID: {0})".format(flow_item.id)) + logger.info(f"Updated flow item (ID: {flow_item.id})") updated_flow = copy.copy(flow_item) return updated_flow._parse_common_elements(server_response.content, self.parent_srv.namespace) # Update flow connections @api(version="3.3") def update_connection(self, flow_item: FlowItem, connection_item: ConnectionItem) -> ConnectionItem: - url = "{0}/{1}/connections/{2}".format(self.baseurl, flow_item.id, connection_item.id) + url = f"{self.baseurl}/{flow_item.id}/connections/{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] - logger.info("Updated flow item (ID: {0} & connection item {1}".format(flow_item.id, connection_item.id)) + logger.info(f"Updated flow item (ID: {flow_item.id} & connection item {connection_item.id}") return connection @api(version="3.3") def refresh(self, flow_item: FlowItem) -> JobItem: - url = "{0}/{1}/run".format(self.baseurl, flow_item.id) + url = f"{self.baseurl}/{flow_item.id}/run" empty_req = RequestFactory.Empty.empty_req() server_response = self.post_request(url, empty_req) new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0] @@ -180,7 +181,7 @@ def refresh(self, flow_item: FlowItem) -> JobItem: # Publish flow @api(version="3.3") def publish( - self, flow_item: FlowItem, file: PathOrFileR, mode: str, connections: Optional[List[ConnectionItem]] = None + self, flow_item: FlowItem, file: PathOrFileR, mode: str, connections: Optional[list[ConnectionItem]] = None ) -> FlowItem: if not mode or not hasattr(self.parent_srv.PublishMode, mode): error = "Invalid mode defined." @@ -189,7 +190,7 @@ def publish( if isinstance(file, (str, os.PathLike)): if not os.path.isfile(file): error = "File path does not lead to an existing file." - raise IOError(error) + raise OSError(error) filename = os.path.basename(file) file_extension = os.path.splitext(filename)[1][1:] @@ -213,30 +214,30 @@ def publish( elif file_type == "xml": file_extension = "tfl" else: - error = "Unsupported file type {}!".format(file_type) + error = f"Unsupported file type {file_type}!" raise ValueError(error) # Generate filename for file object. # This is needed when publishing the flow in a single request - filename = "{}.{}".format(flow_item.name, file_extension) + filename = f"{flow_item.name}.{file_extension}" file_size = get_file_object_size(file) else: raise TypeError("file should be a filepath or file object.") # Construct the url with the defined mode - url = "{0}?flowType={1}".format(self.baseurl, file_extension) + url = f"{self.baseurl}?flowType={file_extension}" if mode == self.parent_srv.PublishMode.Overwrite or mode == self.parent_srv.PublishMode.Append: - url += "&{0}=true".format(mode.lower()) + url += f"&{mode.lower()}=true" # 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 (flow over 64MB)".format(filename)) + logger.info(f"Publishing {filename} to server with chunking method (flow over 64MB)") upload_session_id = self.parent_srv.fileuploads.upload(file) - url = "{0}&uploadSessionId={1}".format(url, upload_session_id) + url = f"{url}&uploadSessionId={upload_session_id}" xml_request, content_type = RequestFactory.Flow.publish_req_chunked(flow_item, connections) else: - logger.info("Publishing {0} to server".format(filename)) + logger.info(f"Publishing {filename} to server") if isinstance(file, (str, Path)): with open(file, "rb") as f: @@ -259,7 +260,7 @@ def publish( raise err else: new_flow = FlowItem.from_response(server_response.content, self.parent_srv.namespace)[0] - logger.info("Published {0} (ID: {1})".format(filename, new_flow.id)) + logger.info(f"Published {filename} (ID: {new_flow.id})") return new_flow @api(version="3.3") @@ -294,7 +295,7 @@ def delete_dqw(self, item: FlowItem) -> None: @api(version="3.3") def schedule_flow_run( self, schedule_id: str, item: FlowItem - ) -> List["AddResponse"]: # actually should return a task + ) -> list["AddResponse"]: # actually should return a task return self.parent_srv.schedules.add_to_schedule(schedule_id, flow=item) def filter(self, *invalid, page_size: Optional[int] = None, **kwargs) -> QuerySet[FlowItem]: diff --git a/tableauserverclient/server/endpoint/groups_endpoint.py b/tableauserverclient/server/endpoint/groups_endpoint.py index 8acf31692..c512b011b 100644 --- a/tableauserverclient/server/endpoint/groups_endpoint.py +++ b/tableauserverclient/server/endpoint/groups_endpoint.py @@ -8,7 +8,8 @@ from tableauserverclient.helpers.logging import logger -from typing import Iterable, List, Optional, TYPE_CHECKING, Tuple, Union +from typing import Optional, TYPE_CHECKING, Union +from collections.abc import Iterable from tableauserverclient.server.query import QuerySet @@ -19,10 +20,10 @@ class Groups(QuerysetEndpoint[GroupItem]): @property def baseurl(self) -> str: - return "{0}/sites/{1}/groups".format(self.parent_srv.baseurl, self.parent_srv.site_id) + return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/groups" @api(version="2.0") - def get(self, req_options: Optional["RequestOptions"] = None) -> Tuple[List[GroupItem], PaginationItem]: + def get(self, req_options: Optional["RequestOptions"] = None) -> tuple[list[GroupItem], PaginationItem]: """Gets all groups""" logger.info("Querying all groups on site") url = self.baseurl @@ -50,12 +51,12 @@ def user_pager(): def _get_users_for_group( self, group_item: GroupItem, req_options: Optional["RequestOptions"] = None - ) -> Tuple[List[UserItem], PaginationItem]: - url = "{0}/{1}/users".format(self.baseurl, group_item.id) + ) -> tuple[list[UserItem], PaginationItem]: + url = f"{self.baseurl}/{group_item.id}/users" server_response = self.get_request(url, req_options) user_item = UserItem.from_response(server_response.content, self.parent_srv.namespace) pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace) - logger.info("Populated users for group (ID: {0})".format(group_item.id)) + logger.info(f"Populated users for group (ID: {group_item.id})") return user_item, pagination_item @api(version="2.0") @@ -64,13 +65,13 @@ def delete(self, group_id: str) -> None: if not group_id: error = "Group ID undefined." raise ValueError(error) - url = "{0}/{1}".format(self.baseurl, group_id) + url = f"{self.baseurl}/{group_id}" self.delete_request(url) - logger.info("Deleted single group (ID: {0})".format(group_id)) + logger.info(f"Deleted single group (ID: {group_id})") @api(version="2.0") def update(self, group_item: GroupItem, as_job: bool = False) -> Union[GroupItem, JobItem]: - url = "{0}/{1}".format(self.baseurl, group_item.id) + url = f"{self.baseurl}/{group_item.id}" if not group_item.id: error = "Group item missing ID." @@ -83,7 +84,7 @@ def update(self, group_item: GroupItem, as_job: bool = False) -> Union[GroupItem update_req = RequestFactory.Group.update_req(group_item) server_response = self.put_request(url, update_req) - logger.info("Updated group item (ID: {0})".format(group_item.id)) + logger.info(f"Updated group item (ID: {group_item.id})") if as_job: return JobItem.from_response(server_response.content, self.parent_srv.namespace)[0] else: @@ -118,9 +119,9 @@ def remove_user(self, group_item: GroupItem, user_id: str) -> None: if not user_id: error = "User ID undefined." raise ValueError(error) - url = "{0}/{1}/users/{2}".format(self.baseurl, group_item.id, user_id) + url = f"{self.baseurl}/{group_item.id}/users/{user_id}" self.delete_request(url) - logger.info("Removed user (id: {0}) from group (ID: {1})".format(user_id, group_item.id)) + logger.info(f"Removed user (id: {user_id}) from group (ID: {group_item.id})") @api(version="3.21") def remove_users(self, group_item: GroupItem, users: Iterable[Union[str, UserItem]]) -> None: @@ -132,7 +133,7 @@ def remove_users(self, group_item: GroupItem, users: Iterable[Union[str, UserIte url = f"{self.baseurl}/{group_id}/users/remove" add_req = RequestFactory.Group.remove_users_req(users) _ = self.put_request(url, add_req) - logger.info("Removed users to group (ID: {0})".format(group_item.id)) + logger.info(f"Removed users to group (ID: {group_item.id})") return None @api(version="2.0") @@ -144,15 +145,15 @@ def add_user(self, group_item: GroupItem, user_id: str) -> UserItem: if not user_id: error = "User ID undefined." raise ValueError(error) - url = "{0}/{1}/users".format(self.baseurl, group_item.id) + url = f"{self.baseurl}/{group_item.id}/users" add_req = RequestFactory.Group.add_user_req(user_id) server_response = self.post_request(url, add_req) user = UserItem.from_response(server_response.content, self.parent_srv.namespace).pop() - logger.info("Added user (id: {0}) to group (ID: {1})".format(user_id, group_item.id)) + logger.info(f"Added user (id: {user_id}) to group (ID: {group_item.id})") return user @api(version="3.21") - def add_users(self, group_item: GroupItem, users: Iterable[Union[str, UserItem]]) -> List[UserItem]: + def add_users(self, group_item: GroupItem, users: Iterable[Union[str, UserItem]]) -> list[UserItem]: """Adds multiple users to 1 group""" group_id = group_item.id if hasattr(group_item, "id") else group_item if not isinstance(group_id, str): @@ -162,7 +163,7 @@ def add_users(self, group_item: GroupItem, users: Iterable[Union[str, UserItem]] add_req = RequestFactory.Group.add_users_req(users) server_response = self.post_request(url, add_req) users = UserItem.from_response(server_response.content, self.parent_srv.namespace) - logger.info("Added users to group (ID: {0})".format(group_item.id)) + logger.info(f"Added users to group (ID: {group_item.id})") return users def filter(self, *invalid, page_size: Optional[int] = None, **kwargs) -> QuerySet[GroupItem]: diff --git a/tableauserverclient/server/endpoint/groupsets_endpoint.py b/tableauserverclient/server/endpoint/groupsets_endpoint.py index 06e7cc627..c7f5ed0e5 100644 --- a/tableauserverclient/server/endpoint/groupsets_endpoint.py +++ b/tableauserverclient/server/endpoint/groupsets_endpoint.py @@ -1,4 +1,4 @@ -from typing import List, Literal, Optional, Tuple, TYPE_CHECKING, Union +from typing import Literal, Optional, TYPE_CHECKING, Union from tableauserverclient.helpers.logging import logger from tableauserverclient.models.group_item import GroupItem @@ -27,7 +27,7 @@ def get( self, request_options: Optional[RequestOptions] = None, result_level: Optional[Literal["members", "local"]] = None, - ) -> Tuple[List[GroupSetItem], PaginationItem]: + ) -> tuple[list[GroupSetItem], PaginationItem]: logger.info("Querying all group sets on site") url = self.baseurl if result_level: diff --git a/tableauserverclient/server/endpoint/jobs_endpoint.py b/tableauserverclient/server/endpoint/jobs_endpoint.py index ae8cf2633..723d3dd38 100644 --- a/tableauserverclient/server/endpoint/jobs_endpoint.py +++ b/tableauserverclient/server/endpoint/jobs_endpoint.py @@ -11,24 +11,24 @@ from tableauserverclient.helpers.logging import logger -from typing import List, Optional, Tuple, Union +from typing import Optional, Union class Jobs(QuerysetEndpoint[BackgroundJobItem]): @property def baseurl(self): - return "{0}/sites/{1}/jobs".format(self.parent_srv.baseurl, self.parent_srv.site_id) + return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/jobs" @overload # type: ignore[override] def get(self: Self, job_id: str, req_options: Optional[RequestOptionsBase] = None) -> JobItem: # type: ignore[override] ... @overload # type: ignore[override] - def get(self: Self, job_id: RequestOptionsBase, req_options: None) -> Tuple[List[BackgroundJobItem], PaginationItem]: # type: ignore[override] + def get(self: Self, job_id: RequestOptionsBase, req_options: None) -> tuple[list[BackgroundJobItem], PaginationItem]: # type: ignore[override] ... @overload # type: ignore[override] - def get(self: Self, job_id: None, req_options: Optional[RequestOptionsBase]) -> Tuple[List[BackgroundJobItem], PaginationItem]: # type: ignore[override] + def get(self: Self, job_id: None, req_options: Optional[RequestOptionsBase]) -> tuple[list[BackgroundJobItem], PaginationItem]: # type: ignore[override] ... @api(version="2.6") @@ -53,13 +53,13 @@ def cancel(self, job_id: Union[str, JobItem]): if isinstance(job_id, JobItem): job_id = job_id.id assert isinstance(job_id, str) - url = "{0}/{1}".format(self.baseurl, job_id) + url = f"{self.baseurl}/{job_id}" return self.put_request(url) @api(version="2.6") def get_by_id(self, job_id: str) -> JobItem: logger.info("Query for information about job " + job_id) - url = "{0}/{1}".format(self.baseurl, job_id) + url = f"{self.baseurl}/{job_id}" server_response = self.get_request(url) new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0] return new_job @@ -77,7 +77,7 @@ def wait_for_job(self, job_id: Union[str, JobItem], *, timeout: Optional[float] job = self.get_by_id(job_id) logger.debug(f"\tJob {job_id} progress={job.progress}") - logger.info("Job {} Completed: Finish Code: {} - Notes:{}".format(job_id, job.finish_code, job.notes)) + logger.info(f"Job {job_id} Completed: Finish Code: {job.finish_code} - Notes:{job.notes}") if job.finish_code == JobItem.FinishCode.Success: return job diff --git a/tableauserverclient/server/endpoint/linked_tasks_endpoint.py b/tableauserverclient/server/endpoint/linked_tasks_endpoint.py index 374130509..ede4d38e3 100644 --- a/tableauserverclient/server/endpoint/linked_tasks_endpoint.py +++ b/tableauserverclient/server/endpoint/linked_tasks_endpoint.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Tuple, Union +from typing import Optional, Union from tableauserverclient.helpers.logging import logger from tableauserverclient.models.linked_tasks_item import LinkedTaskItem, LinkedTaskJobItem @@ -18,7 +18,7 @@ def baseurl(self) -> str: return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/tasks/linked" @api(version="3.15") - def get(self, req_options: Optional["RequestOptions"] = None) -> Tuple[List[LinkedTaskItem], PaginationItem]: + def get(self, req_options: Optional["RequestOptions"] = None) -> tuple[list[LinkedTaskItem], PaginationItem]: logger.info("Querying all linked tasks on site") url = self.baseurl server_response = self.get_request(url, req_options) diff --git a/tableauserverclient/server/endpoint/metadata_endpoint.py b/tableauserverclient/server/endpoint/metadata_endpoint.py index 38c3eebb6..e5dbcbcf8 100644 --- a/tableauserverclient/server/endpoint/metadata_endpoint.py +++ b/tableauserverclient/server/endpoint/metadata_endpoint.py @@ -50,11 +50,11 @@ def get_page_info(result): class Metadata(Endpoint): @property def baseurl(self): - return "{0}/api/metadata/graphql".format(self.parent_srv.server_address) + return f"{self.parent_srv.server_address}/api/metadata/graphql" @property def control_baseurl(self): - return "{0}/api/metadata/v1/control".format(self.parent_srv.server_address) + return f"{self.parent_srv.server_address}/api/metadata/v1/control" @api("3.5") def query(self, query, variables=None, abort_on_error=False, parameters=None): diff --git a/tableauserverclient/server/endpoint/metrics_endpoint.py b/tableauserverclient/server/endpoint/metrics_endpoint.py index ab1ec5852..3fea1f5b6 100644 --- a/tableauserverclient/server/endpoint/metrics_endpoint.py +++ b/tableauserverclient/server/endpoint/metrics_endpoint.py @@ -8,7 +8,7 @@ import logging -from typing import List, Optional, TYPE_CHECKING, Tuple +from typing import Optional, TYPE_CHECKING if TYPE_CHECKING: from ..request_options import RequestOptions @@ -20,18 +20,18 @@ class Metrics(QuerysetEndpoint[MetricItem]): def __init__(self, parent_srv: "Server") -> None: - super(Metrics, self).__init__(parent_srv) + super().__init__(parent_srv) self._resource_tagger = _ResourceTagger(parent_srv) self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl) self._data_quality_warnings = _DataQualityWarningEndpoint(self.parent_srv, "metric") @property def baseurl(self) -> str: - return "{0}/sites/{1}/metrics".format(self.parent_srv.baseurl, self.parent_srv.site_id) + return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/metrics" # Get all metrics @api(version="3.9") - def get(self, req_options: Optional["RequestOptions"] = None) -> Tuple[List[MetricItem], PaginationItem]: + def get(self, req_options: Optional["RequestOptions"] = None) -> tuple[list[MetricItem], PaginationItem]: logger.info("Querying all metrics on site") url = self.baseurl server_response = self.get_request(url, req_options) @@ -45,8 +45,8 @@ def get_by_id(self, metric_id: str) -> MetricItem: if not metric_id: error = "Metric ID undefined." raise ValueError(error) - logger.info("Querying single metric (ID: {0})".format(metric_id)) - url = "{0}/{1}".format(self.baseurl, metric_id) + logger.info(f"Querying single metric (ID: {metric_id})") + url = f"{self.baseurl}/{metric_id}" server_response = self.get_request(url) return MetricItem.from_response(server_response.content, self.parent_srv.namespace)[0] @@ -56,9 +56,9 @@ def delete(self, metric_id: str) -> None: if not metric_id: error = "Metric ID undefined." raise ValueError(error) - url = "{0}/{1}".format(self.baseurl, metric_id) + url = f"{self.baseurl}/{metric_id}" self.delete_request(url) - logger.info("Deleted single metric (ID: {0})".format(metric_id)) + logger.info(f"Deleted single metric (ID: {metric_id})") # Update metric @api(version="3.9") @@ -70,8 +70,8 @@ def update(self, metric_item: MetricItem) -> MetricItem: self._resource_tagger.update_tags(self.baseurl, metric_item) # Update the metric itself - url = "{0}/{1}".format(self.baseurl, metric_item.id) + url = f"{self.baseurl}/{metric_item.id}" update_req = RequestFactory.Metric.update_req(metric_item) server_response = self.put_request(url, update_req) - logger.info("Updated metric item (ID: {0})".format(metric_item.id)) + logger.info(f"Updated metric item (ID: {metric_item.id})") return MetricItem.from_response(server_response.content, self.parent_srv.namespace)[0] diff --git a/tableauserverclient/server/endpoint/permissions_endpoint.py b/tableauserverclient/server/endpoint/permissions_endpoint.py index 4433625f2..10d420ff7 100644 --- a/tableauserverclient/server/endpoint/permissions_endpoint.py +++ b/tableauserverclient/server/endpoint/permissions_endpoint.py @@ -6,7 +6,7 @@ from .endpoint import Endpoint from .exceptions import MissingRequiredFieldError -from typing import Callable, TYPE_CHECKING, List, Optional, Union +from typing import Callable, TYPE_CHECKING, Optional, Union from tableauserverclient.helpers.logging import logger @@ -25,7 +25,7 @@ class _PermissionsEndpoint(Endpoint): """ def __init__(self, parent_srv: "Server", owner_baseurl: Callable[[], str]) -> None: - super(_PermissionsEndpoint, self).__init__(parent_srv) + super().__init__(parent_srv) # owner_baseurl is the baseurl of the parent. The MUST be a lambda # since we don't know the full site URL until we sign in. If @@ -33,18 +33,18 @@ def __init__(self, parent_srv: "Server", owner_baseurl: Callable[[], str]) -> No self.owner_baseurl = owner_baseurl def __str__(self): - return "".format(self.owner_baseurl) + return f"" - def update(self, resource: TableauItem, permissions: List[PermissionsRule]) -> List[PermissionsRule]: - url = "{0}/{1}/permissions".format(self.owner_baseurl(), resource.id) + def update(self, resource: TableauItem, permissions: list[PermissionsRule]) -> list[PermissionsRule]: + url = f"{self.owner_baseurl()}/{resource.id}/permissions" update_req = RequestFactory.Permission.add_req(permissions) response = self.put_request(url, update_req) permissions = PermissionsRule.from_response(response.content, self.parent_srv.namespace) - logger.info("Updated permissions for resource {0}: {1}".format(resource.id, permissions)) + logger.info(f"Updated permissions for resource {resource.id}: {permissions}") return permissions - def delete(self, resource: TableauItem, rules: Union[PermissionsRule, List[PermissionsRule]]): + def delete(self, resource: TableauItem, rules: Union[PermissionsRule, list[PermissionsRule]]): # Delete is the only endpoint that doesn't take a list of rules # so let's fake it to keep it consistent # TODO that means we need error handling around the call @@ -54,7 +54,7 @@ def delete(self, resource: TableauItem, rules: Union[PermissionsRule, List[Permi for rule in rules: for capability, mode in rule.capabilities.items(): "/permissions/groups/group-id/capability-name/capability-mode" - url = "{0}/{1}/permissions/{2}/{3}/{4}/{5}".format( + url = "{}/{}/permissions/{}/{}/{}/{}".format( self.owner_baseurl(), resource.id, rule.grantee.tag_name + "s", @@ -63,13 +63,11 @@ def delete(self, resource: TableauItem, rules: Union[PermissionsRule, List[Permi mode, ) - logger.debug("Removing {0} permission for capability {1}".format(mode, capability)) + logger.debug(f"Removing {mode} permission for capability {capability}") self.delete_request(url) - logger.info( - "Deleted permission for {0} {1} item {2}".format(rule.grantee.tag_name, rule.grantee.id, resource.id) - ) + logger.info(f"Deleted permission for {rule.grantee.tag_name} {rule.grantee.id} item {resource.id}") def populate(self, item: TableauItem): if not item.id: @@ -80,12 +78,12 @@ def permission_fetcher(): return self._get_permissions(item) item._set_permissions(permission_fetcher) - logger.info("Populated permissions for item (ID: {0})".format(item.id)) + logger.info(f"Populated permissions for item (ID: {item.id})") def _get_permissions(self, item: TableauItem, req_options: Optional["RequestOptions"] = None): - url = "{0}/{1}/permissions".format(self.owner_baseurl(), item.id) + url = f"{self.owner_baseurl()}/{item.id}/permissions" server_response = self.get_request(url, req_options) permissions = PermissionsRule.from_response(server_response.content, self.parent_srv.namespace) - logger.info("Permissions for resource {0}: {1}".format(item.id, permissions)) + logger.info(f"Permissions for resource {item.id}: {permissions}") return permissions diff --git a/tableauserverclient/server/endpoint/projects_endpoint.py b/tableauserverclient/server/endpoint/projects_endpoint.py index 565817e37..4d139fe66 100644 --- a/tableauserverclient/server/endpoint/projects_endpoint.py +++ b/tableauserverclient/server/endpoint/projects_endpoint.py @@ -7,7 +7,7 @@ from tableauserverclient.server import RequestFactory, RequestOptions from tableauserverclient.models import ProjectItem, PaginationItem, Resource -from typing import List, Optional, Tuple, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING from tableauserverclient.server.query import QuerySet @@ -20,17 +20,17 @@ class Projects(QuerysetEndpoint[ProjectItem]): def __init__(self, parent_srv: "Server") -> None: - super(Projects, self).__init__(parent_srv) + super().__init__(parent_srv) self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl) self._default_permissions = _DefaultPermissionsEndpoint(parent_srv, lambda: self.baseurl) @property def baseurl(self) -> str: - return "{0}/sites/{1}/projects".format(self.parent_srv.baseurl, self.parent_srv.site_id) + return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/projects" @api(version="2.0") - def get(self, req_options: Optional["RequestOptions"] = None) -> Tuple[List[ProjectItem], PaginationItem]: + def get(self, req_options: Optional["RequestOptions"] = None) -> tuple[list[ProjectItem], PaginationItem]: logger.info("Querying all projects on site") url = self.baseurl server_response = self.get_request(url, req_options) @@ -43,9 +43,9 @@ def delete(self, project_id: str) -> None: if not project_id: error = "Project ID undefined." raise ValueError(error) - url = "{0}/{1}".format(self.baseurl, project_id) + url = f"{self.baseurl}/{project_id}" self.delete_request(url) - logger.info("Deleted single project (ID: {0})".format(project_id)) + logger.info(f"Deleted single project (ID: {project_id})") @api(version="2.0") def update(self, project_item: ProjectItem, samples: bool = False) -> ProjectItem: @@ -54,10 +54,10 @@ def update(self, project_item: ProjectItem, samples: bool = False) -> ProjectIte raise MissingRequiredFieldError(error) params = {"params": {RequestOptions.Field.PublishSamples: samples}} - url = "{0}/{1}".format(self.baseurl, project_item.id) + url = f"{self.baseurl}/{project_item.id}" update_req = RequestFactory.Project.update_req(project_item) server_response = self.put_request(url, update_req, XML_CONTENT_TYPE, params) - logger.info("Updated project item (ID: {0})".format(project_item.id)) + logger.info(f"Updated project item (ID: {project_item.id})") updated_project = ProjectItem.from_response(server_response.content, self.parent_srv.namespace)[0] return updated_project @@ -66,11 +66,11 @@ def create(self, project_item: ProjectItem, samples: bool = False) -> ProjectIte params = {"params": {RequestOptions.Field.PublishSamples: samples}} url = self.baseurl if project_item._samples: - url = "{0}?publishSamples={1}".format(self.baseurl, project_item._samples) + url = f"{self.baseurl}?publishSamples={project_item._samples}" create_req = RequestFactory.Project.create_req(project_item) server_response = self.post_request(url, create_req, XML_CONTENT_TYPE, params) new_project = ProjectItem.from_response(server_response.content, self.parent_srv.namespace)[0] - logger.info("Created new project (ID: {0})".format(new_project.id)) + logger.info(f"Created new project (ID: {new_project.id})") return new_project @api(version="2.0") diff --git a/tableauserverclient/server/endpoint/resource_tagger.py b/tableauserverclient/server/endpoint/resource_tagger.py index 1894e3b8a..63c03b3e3 100644 --- a/tableauserverclient/server/endpoint/resource_tagger.py +++ b/tableauserverclient/server/endpoint/resource_tagger.py @@ -1,6 +1,7 @@ import abc import copy -from typing import Generic, Iterable, Optional, Protocol, Set, TypeVar, Union, TYPE_CHECKING, runtime_checkable +from typing import Generic, Optional, Protocol, TypeVar, Union, TYPE_CHECKING, runtime_checkable +from collections.abc import Iterable import urllib.parse from tableauserverclient.server.endpoint.endpoint import Endpoint, api @@ -24,7 +25,7 @@ class _ResourceTagger(Endpoint): # Add new tags to resource def _add_tags(self, baseurl, resource_id, tag_set): - url = "{0}/{1}/tags".format(baseurl, resource_id) + url = f"{baseurl}/{resource_id}/tags" add_req = RequestFactory.Tag.add_req(tag_set) try: @@ -39,7 +40,7 @@ def _add_tags(self, baseurl, resource_id, tag_set): # Delete a resource's tag by name def _delete_tag(self, baseurl, resource_id, tag_name): encoded_tag_name = urllib.parse.quote(tag_name) - url = "{0}/{1}/tags/{2}".format(baseurl, resource_id, encoded_tag_name) + url = f"{baseurl}/{resource_id}/tags/{encoded_tag_name}" try: self.delete_request(url) @@ -59,7 +60,7 @@ def update_tags(self, baseurl, resource_item): if add_set: resource_item.tags = self._add_tags(baseurl, resource_item.id, add_set) resource_item._initial_tags = copy.copy(resource_item.tags) - logger.info("Updated tags to {0}".format(resource_item.tags)) + logger.info(f"Updated tags to {resource_item.tags}") class Response(Protocol): @@ -68,8 +69,8 @@ class Response(Protocol): @runtime_checkable class Taggable(Protocol): - tags: Set[str] - _initial_tags: Set[str] + tags: set[str] + _initial_tags: set[str] @property def id(self) -> Optional[str]: @@ -95,14 +96,14 @@ def put_request(self, url, request) -> Response: def delete_request(self, url) -> None: pass - def add_tags(self, item: Union[T, str], tags: Union[Iterable[str], str]) -> Set[str]: + def add_tags(self, item: Union[T, str], tags: Union[Iterable[str], str]) -> set[str]: item_id = getattr(item, "id", item) if not isinstance(item_id, str): raise ValueError("ID not found.") if isinstance(tags, str): - tag_set = set([tags]) + tag_set = {tags} else: tag_set = set(tags) @@ -118,7 +119,7 @@ def delete_tags(self, item: Union[T, str], tags: Union[Iterable[str], str]) -> N raise ValueError("ID not found.") if isinstance(tags, str): - tag_set = set([tags]) + tag_set = {tags} else: tag_set = set(tags) @@ -158,9 +159,9 @@ def baseurl(self): return f"{self.parent_srv.baseurl}/tags" @api(version="3.9") - def batch_add(self, tags: Union[Iterable[str], str], content: content) -> Set[str]: + def batch_add(self, tags: Union[Iterable[str], str], content: content) -> set[str]: if isinstance(tags, str): - tag_set = set([tags]) + tag_set = {tags} else: tag_set = set(tags) @@ -170,9 +171,9 @@ def batch_add(self, tags: Union[Iterable[str], str], content: content) -> Set[st return TagItem.from_response(server_response.content, self.parent_srv.namespace) @api(version="3.9") - def batch_delete(self, tags: Union[Iterable[str], str], content: content) -> Set[str]: + def batch_delete(self, tags: Union[Iterable[str], str], content: content) -> set[str]: if isinstance(tags, str): - tag_set = set([tags]) + tag_set = {tags} else: tag_set = set(tags) diff --git a/tableauserverclient/server/endpoint/schedules_endpoint.py b/tableauserverclient/server/endpoint/schedules_endpoint.py index cfaee3324..4ed243b25 100644 --- a/tableauserverclient/server/endpoint/schedules_endpoint.py +++ b/tableauserverclient/server/endpoint/schedules_endpoint.py @@ -2,7 +2,7 @@ import logging import warnings from collections import namedtuple -from typing import TYPE_CHECKING, Callable, List, Optional, Tuple, Union +from typing import TYPE_CHECKING, Callable, Optional, Union from .endpoint import Endpoint, api, parameter_added_in from .exceptions import MissingRequiredFieldError @@ -22,14 +22,14 @@ class Schedules(Endpoint): @property def baseurl(self) -> str: - return "{0}/schedules".format(self.parent_srv.baseurl) + return f"{self.parent_srv.baseurl}/schedules" @property def siteurl(self) -> str: - return "{0}/sites/{1}/schedules".format(self.parent_srv.baseurl, self.parent_srv.site_id) + return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/schedules" @api(version="2.3") - def get(self, req_options: Optional["RequestOptions"] = None) -> Tuple[List[ScheduleItem], PaginationItem]: + def get(self, req_options: Optional["RequestOptions"] = None) -> tuple[list[ScheduleItem], PaginationItem]: logger.info("Querying all schedules") url = self.baseurl server_response = self.get_request(url, req_options) @@ -42,8 +42,8 @@ def get_by_id(self, schedule_id): if not schedule_id: error = "No Schedule ID provided" raise ValueError(error) - logger.info("Querying a single schedule by id ({})".format(schedule_id)) - url = "{0}/{1}".format(self.baseurl, schedule_id) + logger.info(f"Querying a single schedule by id ({schedule_id})") + url = f"{self.baseurl}/{schedule_id}" server_response = self.get_request(url) return ScheduleItem.from_response(server_response.content, self.parent_srv.namespace)[0] @@ -52,9 +52,9 @@ def delete(self, schedule_id: str) -> None: if not schedule_id: error = "Schedule ID undefined" raise ValueError(error) - url = "{0}/{1}".format(self.baseurl, schedule_id) + url = f"{self.baseurl}/{schedule_id}" self.delete_request(url) - logger.info("Deleted single schedule (ID: {0})".format(schedule_id)) + logger.info(f"Deleted single schedule (ID: {schedule_id})") @api(version="2.3") def update(self, schedule_item: ScheduleItem) -> ScheduleItem: @@ -62,10 +62,10 @@ def update(self, schedule_item: ScheduleItem) -> ScheduleItem: error = "Schedule item missing ID." raise MissingRequiredFieldError(error) - url = "{0}/{1}".format(self.baseurl, schedule_item.id) + url = f"{self.baseurl}/{schedule_item.id}" update_req = RequestFactory.Schedule.update_req(schedule_item) server_response = self.put_request(url, update_req) - logger.info("Updated schedule item (ID: {})".format(schedule_item.id)) + logger.info(f"Updated schedule item (ID: {schedule_item.id})") updated_schedule = copy.copy(schedule_item) return updated_schedule._parse_common_tags(server_response.content, self.parent_srv.namespace) @@ -79,7 +79,7 @@ def create(self, schedule_item: ScheduleItem) -> ScheduleItem: create_req = RequestFactory.Schedule.create_req(schedule_item) server_response = self.post_request(url, create_req) new_schedule = ScheduleItem.from_response(server_response.content, self.parent_srv.namespace)[0] - logger.info("Created new schedule (ID: {})".format(new_schedule.id)) + logger.info(f"Created new schedule (ID: {new_schedule.id})") return new_schedule @api(version="2.8") @@ -91,12 +91,12 @@ def add_to_schedule( datasource: Optional["DatasourceItem"] = None, flow: Optional["FlowItem"] = None, task_type: Optional[str] = None, - ) -> List[AddResponse]: + ) -> list[AddResponse]: # There doesn't seem to be a good reason to allow one item of each type? if workbook and datasource: warnings.warn("Passing in multiple items for add_to_schedule will be deprecated", PendingDeprecationWarning) - items: List[ - Tuple[str, Union[WorkbookItem, FlowItem, DatasourceItem], str, Callable[[Optional[str], str], bytes], str] + items: list[ + tuple[str, Union[WorkbookItem, FlowItem, DatasourceItem], str, Callable[[Optional[str], str], bytes], str] ] = [] if workbook is not None: @@ -133,13 +133,13 @@ def _add_to( item_task_type, ) -> AddResponse: id_ = resource.id - url = "{0}/{1}/{2}s".format(self.siteurl, schedule_id, type_) + url = f"{self.siteurl}/{schedule_id}/{type_}s" add_req = req_factory(id_, task_type=item_task_type) # type: ignore[call-arg, arg-type] response = self.put_request(url, add_req) error, warnings, task_created = ScheduleItem.parse_add_to_schedule_response(response, self.parent_srv.namespace) if task_created: - logger.info("Added {} to {} to schedule {}".format(type_, id_, schedule_id)) + logger.info(f"Added {type_} to {id_} to schedule {schedule_id}") if error is not None or warnings is not None: return AddResponse( diff --git a/tableauserverclient/server/endpoint/server_info_endpoint.py b/tableauserverclient/server/endpoint/server_info_endpoint.py index 26aaf2910..ab731c11b 100644 --- a/tableauserverclient/server/endpoint/server_info_endpoint.py +++ b/tableauserverclient/server/endpoint/server_info_endpoint.py @@ -21,11 +21,11 @@ def serverInfo(self): return self._info def __repr__(self): - return "".format(self.serverInfo) + return f"" @property def baseurl(self): - return "{0}/serverInfo".format(self.parent_srv.baseurl) + return f"{self.parent_srv.baseurl}/serverInfo" @api(version="2.4") def get(self): diff --git a/tableauserverclient/server/endpoint/sites_endpoint.py b/tableauserverclient/server/endpoint/sites_endpoint.py index dfec49ae1..0f3d25908 100644 --- a/tableauserverclient/server/endpoint/sites_endpoint.py +++ b/tableauserverclient/server/endpoint/sites_endpoint.py @@ -8,7 +8,7 @@ from tableauserverclient.helpers.logging import logger -from typing import TYPE_CHECKING, List, Optional, Tuple +from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: from ..request_options import RequestOptions @@ -17,11 +17,11 @@ class Sites(Endpoint): @property def baseurl(self) -> str: - return "{0}/sites".format(self.parent_srv.baseurl) + return f"{self.parent_srv.baseurl}/sites" # Gets all sites @api(version="2.0") - def get(self, req_options: Optional["RequestOptions"] = None) -> Tuple[List[SiteItem], PaginationItem]: + def get(self, req_options: Optional["RequestOptions"] = None) -> tuple[list[SiteItem], PaginationItem]: logger.info("Querying all sites on site") logger.info("Requires Server Admin permissions") url = self.baseurl @@ -40,8 +40,8 @@ def get_by_id(self, site_id: str) -> SiteItem: error = "You can only retrieve the site for which you are currently authenticated." raise ValueError(error) - logger.info("Querying single site (ID: {0})".format(site_id)) - url = "{0}/{1}".format(self.baseurl, site_id) + logger.info(f"Querying single site (ID: {site_id})") + url = f"{self.baseurl}/{site_id}" server_response = self.get_request(url) return SiteItem.from_response(server_response.content, self.parent_srv.namespace)[0] @@ -52,8 +52,8 @@ def get_by_name(self, site_name: str) -> SiteItem: error = "Site Name undefined." raise ValueError(error) print("Note: You can only work with the site for which you are currently authenticated") - logger.info("Querying single site (Name: {0})".format(site_name)) - url = "{0}/{1}?key=name".format(self.baseurl, site_name) + logger.info(f"Querying single site (Name: {site_name})") + url = f"{self.baseurl}/{site_name}?key=name" print(self.baseurl, url) server_response = self.get_request(url) return SiteItem.from_response(server_response.content, self.parent_srv.namespace)[0] @@ -68,9 +68,9 @@ def get_by_content_url(self, content_url: str) -> SiteItem: error = "You can only work with the site you are currently authenticated for" raise ValueError(error) - logger.info("Querying single site (Content URL: {0})".format(content_url)) + logger.info(f"Querying single site (Content URL: {content_url})") logger.debug("Querying other sites requires Server Admin permissions") - url = "{0}/{1}?key=contentUrl".format(self.baseurl, content_url) + url = f"{self.baseurl}/{content_url}?key=contentUrl" server_response = self.get_request(url) return SiteItem.from_response(server_response.content, self.parent_srv.namespace)[0] @@ -90,10 +90,10 @@ def update(self, site_item: SiteItem) -> SiteItem: error = "You cannot set admin_mode to ContentOnly and also set a user quota" raise ValueError(error) - url = "{0}/{1}".format(self.baseurl, site_item.id) + url = f"{self.baseurl}/{site_item.id}" update_req = RequestFactory.Site.update_req(site_item, self.parent_srv) server_response = self.put_request(url, update_req) - logger.info("Updated site item (ID: {0})".format(site_item.id)) + logger.info(f"Updated site item (ID: {site_item.id})") update_site = copy.copy(site_item) return update_site._parse_common_tags(server_response.content, self.parent_srv.namespace) @@ -103,13 +103,13 @@ def delete(self, site_id: str) -> None: if not site_id: error = "Site ID undefined." raise ValueError(error) - url = "{0}/{1}".format(self.baseurl, site_id) + url = f"{self.baseurl}/{site_id}" if not site_id == self.parent_srv.site_id: error = "You can only delete the site you are currently authenticated for" raise ValueError(error) self.delete_request(url) self.parent_srv._clear_auth() - logger.info("Deleted single site (ID: {0}) and signed out".format(site_id)) + logger.info(f"Deleted single site (ID: {site_id}) and signed out") # Create new site @api(version="2.0") @@ -123,7 +123,7 @@ def create(self, site_item: SiteItem) -> SiteItem: create_req = RequestFactory.Site.create_req(site_item, self.parent_srv) server_response = self.post_request(url, create_req) new_site = SiteItem.from_response(server_response.content, self.parent_srv.namespace)[0] - logger.info("Created new site (ID: {0})".format(new_site.id)) + logger.info(f"Created new site (ID: {new_site.id})") return new_site @api(version="3.5") @@ -131,7 +131,7 @@ def encrypt_extracts(self, site_id: str) -> None: if not site_id: error = "Site ID undefined." raise ValueError(error) - url = "{0}/{1}/encrypt-extracts".format(self.baseurl, site_id) + url = f"{self.baseurl}/{site_id}/encrypt-extracts" empty_req = RequestFactory.Empty.empty_req() self.post_request(url, empty_req) @@ -140,7 +140,7 @@ def decrypt_extracts(self, site_id: str) -> None: if not site_id: error = "Site ID undefined." raise ValueError(error) - url = "{0}/{1}/decrypt-extracts".format(self.baseurl, site_id) + url = f"{self.baseurl}/{site_id}/decrypt-extracts" empty_req = RequestFactory.Empty.empty_req() self.post_request(url, empty_req) @@ -149,7 +149,7 @@ def re_encrypt_extracts(self, site_id: str) -> None: if not site_id: error = "Site ID undefined." raise ValueError(error) - url = "{0}/{1}/reencrypt-extracts".format(self.baseurl, site_id) + url = f"{self.baseurl}/{site_id}/reencrypt-extracts" empty_req = RequestFactory.Empty.empty_req() self.post_request(url, empty_req) diff --git a/tableauserverclient/server/endpoint/subscriptions_endpoint.py b/tableauserverclient/server/endpoint/subscriptions_endpoint.py index a9f2e7bf5..c9abc9b06 100644 --- a/tableauserverclient/server/endpoint/subscriptions_endpoint.py +++ b/tableauserverclient/server/endpoint/subscriptions_endpoint.py @@ -7,7 +7,7 @@ from tableauserverclient.helpers.logging import logger -from typing import List, Optional, TYPE_CHECKING, Tuple +from typing import Optional, TYPE_CHECKING if TYPE_CHECKING: from ..request_options import RequestOptions @@ -16,10 +16,10 @@ class Subscriptions(Endpoint): @property def baseurl(self) -> str: - return "{0}/sites/{1}/subscriptions".format(self.parent_srv.baseurl, self.parent_srv.site_id) + return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/subscriptions" @api(version="2.3") - def get(self, req_options: Optional["RequestOptions"] = None) -> Tuple[List[SubscriptionItem], PaginationItem]: + def get(self, req_options: Optional["RequestOptions"] = None) -> tuple[list[SubscriptionItem], PaginationItem]: logger.info("Querying all subscriptions for the site") url = self.baseurl server_response = self.get_request(url, req_options) @@ -33,8 +33,8 @@ def get_by_id(self, subscription_id: str) -> SubscriptionItem: if not subscription_id: error = "No Subscription ID provided" raise ValueError(error) - logger.info("Querying a single subscription by id ({})".format(subscription_id)) - url = "{}/{}".format(self.baseurl, subscription_id) + logger.info(f"Querying a single subscription by id ({subscription_id})") + url = f"{self.baseurl}/{subscription_id}" server_response = self.get_request(url) return SubscriptionItem.from_response(server_response.content, self.parent_srv.namespace)[0] @@ -43,7 +43,7 @@ def create(self, subscription_item: SubscriptionItem) -> SubscriptionItem: if not subscription_item: error = "No Susbcription provided" raise ValueError(error) - logger.info("Creating a subscription ({})".format(subscription_item)) + logger.info(f"Creating a subscription ({subscription_item})") url = self.baseurl create_req = RequestFactory.Subscription.create_req(subscription_item) server_response = self.post_request(url, create_req) @@ -54,17 +54,17 @@ def delete(self, subscription_id: str) -> None: if not subscription_id: error = "Subscription ID undefined." raise ValueError(error) - url = "{0}/{1}".format(self.baseurl, subscription_id) + url = f"{self.baseurl}/{subscription_id}" self.delete_request(url) - logger.info("Deleted subscription (ID: {0})".format(subscription_id)) + logger.info(f"Deleted subscription (ID: {subscription_id})") @api(version="2.3") def update(self, subscription_item: SubscriptionItem) -> SubscriptionItem: if not subscription_item.id: error = "Subscription item missing ID. Subscription must be retrieved from server first." raise MissingRequiredFieldError(error) - url = "{0}/{1}".format(self.baseurl, subscription_item.id) + url = f"{self.baseurl}/{subscription_item.id}" update_req = RequestFactory.Subscription.update_req(subscription_item) server_response = self.put_request(url, update_req) - logger.info("Updated subscription item (ID: {0})".format(subscription_item.id)) + logger.info(f"Updated subscription item (ID: {subscription_item.id})") return SubscriptionItem.from_response(server_response.content, self.parent_srv.namespace)[0] diff --git a/tableauserverclient/server/endpoint/tables_endpoint.py b/tableauserverclient/server/endpoint/tables_endpoint.py index 36ef78c0a..120d3ba9c 100644 --- a/tableauserverclient/server/endpoint/tables_endpoint.py +++ b/tableauserverclient/server/endpoint/tables_endpoint.py @@ -1,5 +1,6 @@ import logging -from typing import Iterable, Set, Union +from typing import Union +from collections.abc import Iterable from tableauserverclient.server.endpoint.dqw_endpoint import _DataQualityWarningEndpoint from tableauserverclient.server.endpoint.endpoint import api, Endpoint @@ -15,14 +16,14 @@ class Tables(Endpoint, TaggingMixin[TableItem]): def __init__(self, parent_srv): - super(Tables, self).__init__(parent_srv) + super().__init__(parent_srv) self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl) self._data_quality_warnings = _DataQualityWarningEndpoint(self.parent_srv, "table") @property def baseurl(self): - return "{0}/sites/{1}/tables".format(self.parent_srv.baseurl, self.parent_srv.site_id) + return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/tables" @api(version="3.5") def get(self, req_options=None): @@ -39,8 +40,8 @@ def get_by_id(self, table_id): if not table_id: error = "table ID undefined." raise ValueError(error) - logger.info("Querying single table (ID: {0})".format(table_id)) - url = "{0}/{1}".format(self.baseurl, table_id) + logger.info(f"Querying single table (ID: {table_id})") + url = f"{self.baseurl}/{table_id}" server_response = self.get_request(url) return TableItem.from_response(server_response.content, self.parent_srv.namespace)[0] @@ -49,9 +50,9 @@ def delete(self, table_id): if not table_id: error = "Database ID undefined." raise ValueError(error) - url = "{0}/{1}".format(self.baseurl, table_id) + url = f"{self.baseurl}/{table_id}" self.delete_request(url) - logger.info("Deleted single table (ID: {0})".format(table_id)) + logger.info(f"Deleted single table (ID: {table_id})") @api(version="3.5") def update(self, table_item): @@ -59,10 +60,10 @@ def update(self, table_item): error = "table item missing ID." raise MissingRequiredFieldError(error) - url = "{0}/{1}".format(self.baseurl, table_item.id) + url = f"{self.baseurl}/{table_item.id}" update_req = RequestFactory.Table.update_req(table_item) server_response = self.put_request(url, update_req) - logger.info("Updated table item (ID: {0})".format(table_item.id)) + logger.info(f"Updated table item (ID: {table_item.id})") updated_table = TableItem.from_response(server_response.content, self.parent_srv.namespace)[0] return updated_table @@ -80,10 +81,10 @@ def column_fetcher(): ) table_item._set_columns(column_fetcher) - logger.info("Populated columns for table (ID: {0}".format(table_item.id)) + logger.info(f"Populated columns for table (ID: {table_item.id}") def _get_columns_for_table(self, table_item, req_options=None): - url = "{0}/{1}/columns".format(self.baseurl, table_item.id) + url = f"{self.baseurl}/{table_item.id}/columns" server_response = self.get_request(url, req_options) columns = ColumnItem.from_response(server_response.content, self.parent_srv.namespace) pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace) @@ -91,12 +92,12 @@ def _get_columns_for_table(self, table_item, req_options=None): @api(version="3.5") def update_column(self, table_item, column_item): - url = "{0}/{1}/columns/{2}".format(self.baseurl, table_item.id, column_item.id) + url = f"{self.baseurl}/{table_item.id}/columns/{column_item.id}" update_req = RequestFactory.Column.update_req(column_item) server_response = self.put_request(url, update_req) column = ColumnItem.from_response(server_response.content, self.parent_srv.namespace)[0] - logger.info("Updated table item (ID: {0} & column item {1}".format(table_item.id, column_item.id)) + logger.info(f"Updated table item (ID: {table_item.id} & column item {column_item.id}") return column @api(version="3.5") @@ -128,7 +129,7 @@ def delete_dqw(self, item): self._data_quality_warnings.clear(item) @api(version="3.9") - def add_tags(self, item: Union[TableItem, str], tags: Union[Iterable[str], str]) -> Set[str]: + def add_tags(self, item: Union[TableItem, str], tags: Union[Iterable[str], str]) -> set[str]: return super().add_tags(item, tags) @api(version="3.9") diff --git a/tableauserverclient/server/endpoint/tasks_endpoint.py b/tableauserverclient/server/endpoint/tasks_endpoint.py index a727a515f..eb82c43bc 100644 --- a/tableauserverclient/server/endpoint/tasks_endpoint.py +++ b/tableauserverclient/server/endpoint/tasks_endpoint.py @@ -1,5 +1,5 @@ import logging -from typing import List, Optional, Tuple, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING from tableauserverclient.server.endpoint.endpoint import Endpoint, api from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError @@ -15,7 +15,7 @@ class Tasks(Endpoint): @property def baseurl(self) -> str: - return "{0}/sites/{1}/tasks".format(self.parent_srv.baseurl, self.parent_srv.site_id) + return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/tasks" def __normalize_task_type(self, task_type: str) -> str: """ @@ -23,20 +23,20 @@ def __normalize_task_type(self, task_type: str) -> str: It is different than the tag "extractRefresh" used in the request body. """ if task_type == TaskItem.Type.ExtractRefresh: - return "{}es".format(task_type) + return f"{task_type}es" else: return task_type @api(version="2.6") def get( self, req_options: Optional["RequestOptions"] = None, task_type: str = TaskItem.Type.ExtractRefresh - ) -> Tuple[List[TaskItem], PaginationItem]: + ) -> tuple[list[TaskItem], PaginationItem]: if task_type == TaskItem.Type.DataAcceleration: self.parent_srv.assert_at_least_version("3.8", "Data Acceleration Tasks") logger.info("Querying all %s tasks for the site", task_type) - url = "{0}/{1}".format(self.baseurl, self.__normalize_task_type(task_type)) + url = f"{self.baseurl}/{self.__normalize_task_type(task_type)}" server_response = self.get_request(url, req_options) pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace) @@ -63,7 +63,7 @@ def create(self, extract_item: TaskItem) -> TaskItem: error = "No extract refresh provided" raise ValueError(error) logger.info("Creating an extract refresh %s", extract_item) - url = "{0}/{1}".format(self.baseurl, self.__normalize_task_type(TaskItem.Type.ExtractRefresh)) + url = f"{self.baseurl}/{self.__normalize_task_type(TaskItem.Type.ExtractRefresh)}" create_req = RequestFactory.Task.create_extract_req(extract_item) server_response = self.post_request(url, create_req) return server_response.content @@ -74,7 +74,7 @@ def run(self, task_item: TaskItem) -> bytes: error = "Task item missing ID." raise MissingRequiredFieldError(error) - url = "{0}/{1}/{2}/runNow".format( + url = "{}/{}/{}/runNow".format( self.baseurl, self.__normalize_task_type(TaskItem.Type.ExtractRefresh), task_item.id, @@ -92,6 +92,6 @@ def delete(self, task_id: str, task_type: str = TaskItem.Type.ExtractRefresh) -> if not task_id: error = "No Task ID provided" raise ValueError(error) - url = "{0}/{1}/{2}".format(self.baseurl, self.__normalize_task_type(task_type), task_id) + url = f"{self.baseurl}/{self.__normalize_task_type(task_type)}/{task_id}" self.delete_request(url) logger.info("Deleted single task (ID: %s)", task_id) diff --git a/tableauserverclient/server/endpoint/users_endpoint.py b/tableauserverclient/server/endpoint/users_endpoint.py index c4b6418b7..793638396 100644 --- a/tableauserverclient/server/endpoint/users_endpoint.py +++ b/tableauserverclient/server/endpoint/users_endpoint.py @@ -1,6 +1,6 @@ import copy import logging -from typing import List, Optional, Tuple +from typing import Optional from tableauserverclient.server.query import QuerySet @@ -16,11 +16,11 @@ class Users(QuerysetEndpoint[UserItem]): @property def baseurl(self) -> str: - return "{0}/sites/{1}/users".format(self.parent_srv.baseurl, self.parent_srv.site_id) + return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/users" # Gets all users @api(version="2.0") - def get(self, req_options: Optional[RequestOptions] = None) -> Tuple[List[UserItem], PaginationItem]: + def get(self, req_options: Optional[RequestOptions] = None) -> tuple[list[UserItem], PaginationItem]: logger.info("Querying all users on site") if req_options is None: @@ -39,8 +39,8 @@ def get_by_id(self, user_id: str) -> UserItem: if not user_id: error = "User ID undefined." raise ValueError(error) - logger.info("Querying single user (ID: {0})".format(user_id)) - url = "{0}/{1}".format(self.baseurl, user_id) + logger.info(f"Querying single user (ID: {user_id})") + url = f"{self.baseurl}/{user_id}" server_response = self.get_request(url) return UserItem.from_response(server_response.content, self.parent_srv.namespace).pop() @@ -51,10 +51,10 @@ def update(self, user_item: UserItem, password: Optional[str] = None) -> UserIte error = "User item missing ID." raise MissingRequiredFieldError(error) - url = "{0}/{1}".format(self.baseurl, user_item.id) + url = f"{self.baseurl}/{user_item.id}" update_req = RequestFactory.User.update_req(user_item, password) server_response = self.put_request(url, update_req) - logger.info("Updated user item (ID: {0})".format(user_item.id)) + logger.info(f"Updated user item (ID: {user_item.id})") updated_item = copy.copy(user_item) return updated_item._parse_common_tags(server_response.content, self.parent_srv.namespace) @@ -64,27 +64,27 @@ def remove(self, user_id: str, map_assets_to: Optional[str] = None) -> None: if not user_id: error = "User ID undefined." raise ValueError(error) - url = "{0}/{1}".format(self.baseurl, user_id) + url = f"{self.baseurl}/{user_id}" if map_assets_to is not None: url += f"?mapAssetsTo={map_assets_to}" self.delete_request(url) - logger.info("Removed single user (ID: {0})".format(user_id)) + logger.info(f"Removed single user (ID: {user_id})") # Add new user to site @api(version="2.0") def add(self, user_item: UserItem) -> UserItem: url = self.baseurl - logger.info("Add user {}".format(user_item.name)) + logger.info(f"Add user {user_item.name}") add_req = RequestFactory.User.add_req(user_item) server_response = self.post_request(url, add_req) logger.info(server_response) new_user = UserItem.from_response(server_response.content, self.parent_srv.namespace).pop() - logger.info("Added new user (ID: {0})".format(new_user.id)) + logger.info(f"Added new user (ID: {new_user.id})") return new_user # Add new users to site. This does not actually perform a bulk action, it's syntactic sugar @api(version="2.0") - def add_all(self, users: List[UserItem]): + def add_all(self, users: list[UserItem]): created = [] failed = [] for user in users: @@ -98,7 +98,7 @@ def add_all(self, users: List[UserItem]): # helping the user by parsing a file they could have used to add users through the UI # line format: Username [required], password, display name, license, admin, publish @api(version="2.0") - def create_from_file(self, filepath: str) -> Tuple[List[UserItem], List[Tuple[UserItem, ServerResponseError]]]: + def create_from_file(self, filepath: str) -> tuple[list[UserItem], list[tuple[UserItem, ServerResponseError]]]: created = [] failed = [] if not filepath.find("csv"): @@ -133,10 +133,10 @@ def wb_pager(): def _get_wbs_for_user( self, user_item: UserItem, req_options: Optional[RequestOptions] = None - ) -> Tuple[List[WorkbookItem], PaginationItem]: - url = "{0}/{1}/workbooks".format(self.baseurl, user_item.id) + ) -> tuple[list[WorkbookItem], PaginationItem]: + url = f"{self.baseurl}/{user_item.id}/workbooks" server_response = self.get_request(url, req_options) - logger.info("Populated workbooks for user (ID: {0})".format(user_item.id)) + logger.info(f"Populated workbooks for user (ID: {user_item.id})") workbook_item = WorkbookItem.from_response(server_response.content, self.parent_srv.namespace) pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace) return workbook_item, pagination_item @@ -161,10 +161,10 @@ def groups_for_user_pager(): def _get_groups_for_user( self, user_item: UserItem, req_options: Optional[RequestOptions] = None - ) -> Tuple[List[GroupItem], PaginationItem]: - url = "{0}/{1}/groups".format(self.baseurl, user_item.id) + ) -> tuple[list[GroupItem], PaginationItem]: + url = f"{self.baseurl}/{user_item.id}/groups" server_response = self.get_request(url, req_options) - logger.info("Populated groups for user (ID: {0})".format(user_item.id)) + logger.info(f"Populated groups for user (ID: {user_item.id})") group_item = GroupItem.from_response(server_response.content, self.parent_srv.namespace) pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace) return group_item, pagination_item diff --git a/tableauserverclient/server/endpoint/views_endpoint.py b/tableauserverclient/server/endpoint/views_endpoint.py index f2ccf658e..3709fc41d 100644 --- a/tableauserverclient/server/endpoint/views_endpoint.py +++ b/tableauserverclient/server/endpoint/views_endpoint.py @@ -11,7 +11,8 @@ from tableauserverclient.helpers.logging import logger -from typing import Iterable, Iterator, List, Optional, Set, Tuple, TYPE_CHECKING, Union +from typing import Optional, TYPE_CHECKING, Union +from collections.abc import Iterable, Iterator if TYPE_CHECKING: from tableauserverclient.server.request_options import ( @@ -25,22 +26,22 @@ class Views(QuerysetEndpoint[ViewItem], TaggingMixin[ViewItem]): def __init__(self, parent_srv): - super(Views, self).__init__(parent_srv) + super().__init__(parent_srv) self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl) # Used because populate_preview_image functionaliy requires workbook endpoint @property def siteurl(self) -> str: - return "{0}/sites/{1}".format(self.parent_srv.baseurl, self.parent_srv.site_id) + return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}" @property def baseurl(self) -> str: - return "{0}/views".format(self.siteurl) + return f"{self.siteurl}/views" @api(version="2.2") def get( self, req_options: Optional["RequestOptions"] = None, usage: bool = False - ) -> Tuple[List[ViewItem], PaginationItem]: + ) -> tuple[list[ViewItem], PaginationItem]: logger.info("Querying all views on site") url = self.baseurl if usage: @@ -55,8 +56,8 @@ def get_by_id(self, view_id: str, usage: bool = False) -> ViewItem: if not view_id: error = "View item missing ID." raise MissingRequiredFieldError(error) - logger.info("Querying single view (ID: {0})".format(view_id)) - url = "{0}/{1}".format(self.baseurl, view_id) + logger.info(f"Querying single view (ID: {view_id})") + url = f"{self.baseurl}/{view_id}" if usage: url += "?includeUsageStatistics=true" server_response = self.get_request(url) @@ -72,10 +73,10 @@ def image_fetcher(): return self._get_preview_for_view(view_item) view_item._set_preview_image(image_fetcher) - logger.info("Populated preview image for view (ID: {0})".format(view_item.id)) + logger.info(f"Populated preview image for view (ID: {view_item.id})") def _get_preview_for_view(self, view_item: ViewItem) -> bytes: - url = "{0}/workbooks/{1}/views/{2}/previewImage".format(self.siteurl, view_item.workbook_id, view_item.id) + url = f"{self.siteurl}/workbooks/{view_item.workbook_id}/views/{view_item.id}/previewImage" server_response = self.get_request(url) image = server_response.content return image @@ -90,10 +91,10 @@ def image_fetcher(): return self._get_view_image(view_item, req_options) view_item._set_image(image_fetcher) - logger.info("Populated image for view (ID: {0})".format(view_item.id)) + logger.info(f"Populated image for view (ID: {view_item.id})") def _get_view_image(self, view_item: ViewItem, req_options: Optional["ImageRequestOptions"]) -> bytes: - url = "{0}/{1}/image".format(self.baseurl, view_item.id) + url = f"{self.baseurl}/{view_item.id}/image" server_response = self.get_request(url, req_options) image = server_response.content return image @@ -108,10 +109,10 @@ def pdf_fetcher(): return self._get_view_pdf(view_item, req_options) view_item._set_pdf(pdf_fetcher) - logger.info("Populated pdf for view (ID: {0})".format(view_item.id)) + logger.info(f"Populated pdf for view (ID: {view_item.id})") def _get_view_pdf(self, view_item: ViewItem, req_options: Optional["PDFRequestOptions"]) -> bytes: - url = "{0}/{1}/pdf".format(self.baseurl, view_item.id) + url = f"{self.baseurl}/{view_item.id}/pdf" server_response = self.get_request(url, req_options) pdf = server_response.content return pdf @@ -126,10 +127,10 @@ def csv_fetcher(): return self._get_view_csv(view_item, req_options) view_item._set_csv(csv_fetcher) - logger.info("Populated csv for view (ID: {0})".format(view_item.id)) + logger.info(f"Populated csv for view (ID: {view_item.id})") def _get_view_csv(self, view_item: ViewItem, req_options: Optional["CSVRequestOptions"]) -> Iterator[bytes]: - url = "{0}/{1}/data".format(self.baseurl, view_item.id) + url = f"{self.baseurl}/{view_item.id}/data" with closing(self.get_request(url, request_object=req_options, parameters={"stream": True})) as server_response: yield from server_response.iter_content(1024) @@ -144,10 +145,10 @@ def excel_fetcher(): return self._get_view_excel(view_item, req_options) view_item._set_excel(excel_fetcher) - logger.info("Populated excel for view (ID: {0})".format(view_item.id)) + logger.info(f"Populated excel for view (ID: {view_item.id})") def _get_view_excel(self, view_item: ViewItem, req_options: Optional["ExcelRequestOptions"]) -> Iterator[bytes]: - url = "{0}/{1}/crosstab/excel".format(self.baseurl, view_item.id) + url = f"{self.baseurl}/{view_item.id}/crosstab/excel" with closing(self.get_request(url, request_object=req_options, parameters={"stream": True})) as server_response: yield from server_response.iter_content(1024) @@ -176,7 +177,7 @@ def update(self, view_item: ViewItem) -> ViewItem: return view_item @api(version="1.0") - def add_tags(self, item: Union[ViewItem, str], tags: Union[Iterable[str], str]) -> Set[str]: + def add_tags(self, item: Union[ViewItem, str], tags: Union[Iterable[str], str]) -> set[str]: return super().add_tags(item, tags) @api(version="1.0") diff --git a/tableauserverclient/server/endpoint/virtual_connections_endpoint.py b/tableauserverclient/server/endpoint/virtual_connections_endpoint.py index f71db00cc..944b72502 100644 --- a/tableauserverclient/server/endpoint/virtual_connections_endpoint.py +++ b/tableauserverclient/server/endpoint/virtual_connections_endpoint.py @@ -1,7 +1,8 @@ from functools import partial import json from pathlib import Path -from typing import Iterable, List, Optional, Set, TYPE_CHECKING, Tuple, Union +from typing import Optional, TYPE_CHECKING, Union +from collections.abc import Iterable from tableauserverclient.models.connection_item import ConnectionItem from tableauserverclient.models.pagination_item import PaginationItem @@ -28,7 +29,7 @@ def baseurl(self) -> str: return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/virtualConnections" @api(version="3.18") - def get(self, req_options: Optional[RequestOptions] = None) -> Tuple[List[VirtualConnectionItem], PaginationItem]: + def get(self, req_options: Optional[RequestOptions] = None) -> tuple[list[VirtualConnectionItem], PaginationItem]: server_response = self.get_request(self.baseurl, req_options) pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace) virtual_connections = VirtualConnectionItem.from_response(server_response.content, self.parent_srv.namespace) @@ -44,7 +45,7 @@ def _connection_fetcher(): def _get_virtual_database_connections( self, virtual_connection: VirtualConnectionItem, req_options: Optional[RequestOptions] = None - ) -> Tuple[List[ConnectionItem], PaginationItem]: + ) -> tuple[list[ConnectionItem], PaginationItem]: server_response = self.get_request(f"{self.baseurl}/{virtual_connection.id}/connections", req_options) connections = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace) pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace) @@ -83,7 +84,7 @@ def update(self, virtual_connection: VirtualConnectionItem) -> VirtualConnection @api(version="3.23") def get_revisions( self, virtual_connection: VirtualConnectionItem, req_options: Optional[RequestOptions] = None - ) -> Tuple[List[RevisionItem], PaginationItem]: + ) -> tuple[list[RevisionItem], PaginationItem]: server_response = self.get_request(f"{self.baseurl}/{virtual_connection.id}/revisions", req_options) pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace) revisions = RevisionItem.from_response(server_response.content, self.parent_srv.namespace, virtual_connection) @@ -159,7 +160,7 @@ def delete_permission(self, item, capability_item): @api(version="3.23") def add_tags( self, virtual_connection: Union[VirtualConnectionItem, str], tags: Union[Iterable[str], str] - ) -> Set[str]: + ) -> set[str]: return super().add_tags(virtual_connection, tags) @api(version="3.23") diff --git a/tableauserverclient/server/endpoint/webhooks_endpoint.py b/tableauserverclient/server/endpoint/webhooks_endpoint.py index 597f9c425..06643f99d 100644 --- a/tableauserverclient/server/endpoint/webhooks_endpoint.py +++ b/tableauserverclient/server/endpoint/webhooks_endpoint.py @@ -6,7 +6,7 @@ from tableauserverclient.helpers.logging import logger -from typing import List, Optional, TYPE_CHECKING, Tuple +from typing import Optional, TYPE_CHECKING if TYPE_CHECKING: from ..server import Server @@ -15,14 +15,14 @@ class Webhooks(Endpoint): def __init__(self, parent_srv: "Server") -> None: - super(Webhooks, self).__init__(parent_srv) + super().__init__(parent_srv) @property def baseurl(self) -> str: - return "{0}/sites/{1}/webhooks".format(self.parent_srv.baseurl, self.parent_srv.site_id) + return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/webhooks" @api(version="3.6") - def get(self, req_options: Optional["RequestOptions"] = None) -> Tuple[List[WebhookItem], PaginationItem]: + def get(self, req_options: Optional["RequestOptions"] = None) -> tuple[list[WebhookItem], PaginationItem]: logger.info("Querying all Webhooks on site") url = self.baseurl server_response = self.get_request(url, req_options) @@ -35,8 +35,8 @@ def get_by_id(self, webhook_id: str) -> WebhookItem: if not webhook_id: error = "Webhook ID undefined." raise ValueError(error) - logger.info("Querying single webhook (ID: {0})".format(webhook_id)) - url = "{0}/{1}".format(self.baseurl, webhook_id) + logger.info(f"Querying single webhook (ID: {webhook_id})") + url = f"{self.baseurl}/{webhook_id}" server_response = self.get_request(url) return WebhookItem.from_response(server_response.content, self.parent_srv.namespace)[0] @@ -45,9 +45,9 @@ def delete(self, webhook_id: str) -> None: if not webhook_id: error = "Webhook ID undefined." raise ValueError(error) - url = "{0}/{1}".format(self.baseurl, webhook_id) + url = f"{self.baseurl}/{webhook_id}" self.delete_request(url) - logger.info("Deleted single webhook (ID: {0})".format(webhook_id)) + logger.info(f"Deleted single webhook (ID: {webhook_id})") @api(version="3.6") def create(self, webhook_item: WebhookItem) -> WebhookItem: @@ -56,7 +56,7 @@ def create(self, webhook_item: WebhookItem) -> WebhookItem: server_response = self.post_request(url, create_req) new_webhook = WebhookItem.from_response(server_response.content, self.parent_srv.namespace)[0] - logger.info("Created new webhook (ID: {0})".format(new_webhook.id)) + logger.info(f"Created new webhook (ID: {new_webhook.id})") return new_webhook @api(version="3.6") @@ -64,7 +64,7 @@ def test(self, webhook_id: str): if not webhook_id: error = "Webhook ID undefined." raise ValueError(error) - url = "{0}/{1}/test".format(self.baseurl, webhook_id) + url = f"{self.baseurl}/{webhook_id}/test" testOutcome = self.get_request(url) - logger.info("Testing webhook (ID: {0} returned {1})".format(webhook_id, testOutcome)) + logger.info(f"Testing webhook (ID: {webhook_id} returned {testOutcome})") return testOutcome diff --git a/tableauserverclient/server/endpoint/workbooks_endpoint.py b/tableauserverclient/server/endpoint/workbooks_endpoint.py index da6eda3de..5e4442b60 100644 --- a/tableauserverclient/server/endpoint/workbooks_endpoint.py +++ b/tableauserverclient/server/endpoint/workbooks_endpoint.py @@ -25,15 +25,11 @@ from tableauserverclient.server import RequestFactory from typing import ( - Iterable, - List, Optional, - Sequence, - Set, - Tuple, TYPE_CHECKING, Union, ) +from collections.abc import Iterable, Sequence if TYPE_CHECKING: from tableauserverclient.server import Server @@ -61,18 +57,18 @@ class Workbooks(QuerysetEndpoint[WorkbookItem], TaggingMixin[WorkbookItem]): def __init__(self, parent_srv: "Server") -> None: - super(Workbooks, self).__init__(parent_srv) + super().__init__(parent_srv) self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl) return None @property def baseurl(self) -> str: - return "{0}/sites/{1}/workbooks".format(self.parent_srv.baseurl, self.parent_srv.site_id) + return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/workbooks" # Get all workbooks on site @api(version="2.0") - def get(self, req_options: Optional["RequestOptions"] = None) -> Tuple[List[WorkbookItem], PaginationItem]: + def get(self, req_options: Optional["RequestOptions"] = None) -> tuple[list[WorkbookItem], PaginationItem]: logger.info("Querying all workbooks on site") url = self.baseurl server_response = self.get_request(url, req_options) @@ -86,15 +82,15 @@ def get_by_id(self, workbook_id: str) -> WorkbookItem: if not workbook_id: error = "Workbook ID undefined." raise ValueError(error) - logger.info("Querying single workbook (ID: {0})".format(workbook_id)) - url = "{0}/{1}".format(self.baseurl, workbook_id) + logger.info(f"Querying single workbook (ID: {workbook_id})") + url = f"{self.baseurl}/{workbook_id}" server_response = self.get_request(url) return WorkbookItem.from_response(server_response.content, self.parent_srv.namespace)[0] @api(version="2.8") def refresh(self, workbook_item: Union[WorkbookItem, str]) -> JobItem: id_ = getattr(workbook_item, "id", workbook_item) - url = "{0}/{1}/refresh".format(self.baseurl, id_) + url = f"{self.baseurl}/{id_}/refresh" empty_req = RequestFactory.Empty.empty_req() server_response = self.post_request(url, empty_req) new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0] @@ -107,10 +103,10 @@ def create_extract( workbook_item: WorkbookItem, encrypt: bool = False, includeAll: bool = True, - datasources: Optional[List["DatasourceItem"]] = None, + datasources: Optional[list["DatasourceItem"]] = None, ) -> JobItem: id_ = getattr(workbook_item, "id", workbook_item) - url = "{0}/{1}/createExtract?encrypt={2}".format(self.baseurl, id_, encrypt) + url = f"{self.baseurl}/{id_}/createExtract?encrypt={encrypt}" datasource_req = RequestFactory.Workbook.embedded_extract_req(includeAll, datasources) server_response = self.post_request(url, datasource_req) @@ -121,7 +117,7 @@ def create_extract( @api(version="3.3") def delete_extract(self, workbook_item: WorkbookItem, includeAll: bool = True, datasources=None) -> JobItem: id_ = getattr(workbook_item, "id", workbook_item) - url = "{0}/{1}/deleteExtract".format(self.baseurl, id_) + url = f"{self.baseurl}/{id_}/deleteExtract" datasource_req = RequestFactory.Workbook.embedded_extract_req(includeAll, datasources) server_response = self.post_request(url, datasource_req) new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0] @@ -133,9 +129,9 @@ def delete(self, workbook_id: str) -> None: if not workbook_id: error = "Workbook ID undefined." raise ValueError(error) - url = "{0}/{1}".format(self.baseurl, workbook_id) + url = f"{self.baseurl}/{workbook_id}" self.delete_request(url) - logger.info("Deleted single workbook (ID: {0})".format(workbook_id)) + logger.info(f"Deleted single workbook (ID: {workbook_id})") # Update workbook @api(version="2.0") @@ -152,27 +148,25 @@ def update( self.update_tags(workbook_item) # Update the workbook itself - url = "{0}/{1}".format(self.baseurl, workbook_item.id) + url = f"{self.baseurl}/{workbook_item.id}" if include_view_acceleration_status: url += "?includeViewAccelerationStatus=True" update_req = RequestFactory.Workbook.update_req(workbook_item) server_response = self.put_request(url, update_req) - logger.info("Updated workbook item (ID: {0})".format(workbook_item.id)) + logger.info(f"Updated workbook item (ID: {workbook_item.id})") updated_workbook = copy.copy(workbook_item) return updated_workbook._parse_common_tags(server_response.content, self.parent_srv.namespace) # Update workbook_connection @api(version="2.3") def update_connection(self, workbook_item: WorkbookItem, connection_item: ConnectionItem) -> ConnectionItem: - url = "{0}/{1}/connections/{2}".format(self.baseurl, workbook_item.id, connection_item.id) + url = f"{self.baseurl}/{workbook_item.id}/connections/{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] - logger.info( - "Updated workbook item (ID: {0} & connection item {1})".format(workbook_item.id, connection_item.id) - ) + logger.info(f"Updated workbook item (ID: {workbook_item.id} & connection item {connection_item.id})") return connection # Download workbook contents with option of passing in filepath @@ -199,14 +193,14 @@ def populate_views(self, workbook_item: WorkbookItem, usage: bool = False) -> No error = "Workbook item missing ID. Workbook must be retrieved from server first." raise MissingRequiredFieldError(error) - def view_fetcher() -> List[ViewItem]: + def view_fetcher() -> list[ViewItem]: return self._get_views_for_workbook(workbook_item, usage) workbook_item._set_views(view_fetcher) - logger.info("Populated views for workbook (ID: {0})".format(workbook_item.id)) + logger.info(f"Populated views for workbook (ID: {workbook_item.id})") - def _get_views_for_workbook(self, workbook_item: WorkbookItem, usage: bool) -> List[ViewItem]: - url = "{0}/{1}/views".format(self.baseurl, workbook_item.id) + def _get_views_for_workbook(self, workbook_item: WorkbookItem, usage: bool) -> list[ViewItem]: + url = f"{self.baseurl}/{workbook_item.id}/views" if usage: url += "?includeUsageStatistics=true" server_response = self.get_request(url) @@ -228,12 +222,12 @@ def connection_fetcher(): return self._get_workbook_connections(workbook_item) workbook_item._set_connections(connection_fetcher) - logger.info("Populated connections for workbook (ID: {0})".format(workbook_item.id)) + logger.info(f"Populated connections for workbook (ID: {workbook_item.id})") def _get_workbook_connections( self, workbook_item: WorkbookItem, req_options: Optional["RequestOptions"] = None - ) -> List[ConnectionItem]: - url = "{0}/{1}/connections".format(self.baseurl, workbook_item.id) + ) -> list[ConnectionItem]: + url = f"{self.baseurl}/{workbook_item.id}/connections" server_response = self.get_request(url, req_options) connections = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace) return connections @@ -249,10 +243,10 @@ def pdf_fetcher() -> bytes: return self._get_wb_pdf(workbook_item, req_options) workbook_item._set_pdf(pdf_fetcher) - logger.info("Populated pdf for workbook (ID: {0})".format(workbook_item.id)) + logger.info(f"Populated pdf for workbook (ID: {workbook_item.id})") def _get_wb_pdf(self, workbook_item: WorkbookItem, req_options: Optional["RequestOptions"]) -> bytes: - url = "{0}/{1}/pdf".format(self.baseurl, workbook_item.id) + url = f"{self.baseurl}/{workbook_item.id}/pdf" server_response = self.get_request(url, req_options) pdf = server_response.content return pdf @@ -267,10 +261,10 @@ def pptx_fetcher() -> bytes: return self._get_wb_pptx(workbook_item, req_options) workbook_item._set_powerpoint(pptx_fetcher) - logger.info("Populated powerpoint for workbook (ID: {0})".format(workbook_item.id)) + logger.info(f"Populated powerpoint for workbook (ID: {workbook_item.id})") def _get_wb_pptx(self, workbook_item: WorkbookItem, req_options: Optional["RequestOptions"]) -> bytes: - url = "{0}/{1}/powerpoint".format(self.baseurl, workbook_item.id) + url = f"{self.baseurl}/{workbook_item.id}/powerpoint" server_response = self.get_request(url, req_options) pptx = server_response.content return pptx @@ -286,10 +280,10 @@ def image_fetcher() -> bytes: return self._get_wb_preview_image(workbook_item) workbook_item._set_preview_image(image_fetcher) - logger.info("Populated preview image for workbook (ID: {0})".format(workbook_item.id)) + logger.info(f"Populated preview image for workbook (ID: {workbook_item.id})") def _get_wb_preview_image(self, workbook_item: WorkbookItem) -> bytes: - url = "{0}/{1}/previewImage".format(self.baseurl, workbook_item.id) + url = f"{self.baseurl}/{workbook_item.id}/previewImage" server_response = self.get_request(url) preview_image = server_response.content return preview_image @@ -322,7 +316,7 @@ def publish( if isinstance(file, (str, os.PathLike)): if not os.path.isfile(file): error = "File path does not lead to an existing file." - raise IOError(error) + raise OSError(error) filename = os.path.basename(file) file_extension = os.path.splitext(filename)[1][1:] @@ -346,12 +340,12 @@ def publish( elif file_type == "xml": file_extension = "twb" else: - error = "Unsupported file type {}!".format(file_type) + error = f"Unsupported file type {file_type}!" raise ValueError(error) # Generate filename for file object. # This is needed when publishing the workbook in a single request - filename = "{}.{}".format(workbook_item.name, file_extension) + filename = f"{workbook_item.name}.{file_extension}" file_size = get_file_object_size(file) else: @@ -362,30 +356,30 @@ def publish( raise ValueError(error) # Construct the url with the defined mode - url = "{0}?workbookType={1}".format(self.baseurl, file_extension) + url = f"{self.baseurl}?workbookType={file_extension}" if mode == self.parent_srv.PublishMode.Overwrite: - url += "&{0}=true".format(mode.lower()) + url += f"&{mode.lower()}=true" elif mode == self.parent_srv.PublishMode.Append: error = "Workbooks cannot be appended." raise ValueError(error) if as_job: - url += "&{0}=true".format("asJob") + url += "&{}=true".format("asJob") if skip_connection_check: - url += "&{0}=true".format("skipConnectionCheck") + url += "&{}=true".format("skipConnectionCheck") # 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 (workbook over 64MB)".format(workbook_item.name)) + logger.info(f"Publishing {workbook_item.name} to server with chunking method (workbook over 64MB)") upload_session_id = self.parent_srv.fileuploads.upload(file) - url = "{0}&uploadSessionId={1}".format(url, upload_session_id) + url = f"{url}&uploadSessionId={upload_session_id}" xml_request, content_type = RequestFactory.Workbook.publish_req_chunked( workbook_item, connections=connections, ) else: - logger.info("Publishing {0} to server".format(filename)) + logger.info(f"Publishing {filename} to server") if isinstance(file, (str, Path)): with open(file, "rb") as f: @@ -403,7 +397,7 @@ def publish( file_contents, connections=connections, ) - logger.debug("Request xml: {0} ".format(redact_xml(xml_request[:1000]))) + logger.debug(f"Request xml: {redact_xml(xml_request[:1000])} ") # Send the publishing request to server try: @@ -415,11 +409,11 @@ def publish( if as_job: new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0] - logger.info("Published {0} (JOB_ID: {1}".format(workbook_item.name, new_job.id)) + logger.info(f"Published {workbook_item.name} (JOB_ID: {new_job.id}") return new_job else: new_workbook = WorkbookItem.from_response(server_response.content, self.parent_srv.namespace)[0] - logger.info("Published {0} (ID: {1})".format(workbook_item.name, new_workbook.id)) + logger.info(f"Published {workbook_item.name} (ID: {new_workbook.id})") return new_workbook # Populate workbook item's revisions @@ -433,12 +427,12 @@ def revisions_fetcher(): return self._get_workbook_revisions(workbook_item) workbook_item._set_revisions(revisions_fetcher) - logger.info("Populated revisions for workbook (ID: {0})".format(workbook_item.id)) + logger.info(f"Populated revisions for workbook (ID: {workbook_item.id})") def _get_workbook_revisions( self, workbook_item: WorkbookItem, req_options: Optional["RequestOptions"] = None - ) -> List[RevisionItem]: - url = "{0}/{1}/revisions".format(self.baseurl, workbook_item.id) + ) -> list[RevisionItem]: + url = f"{self.baseurl}/{workbook_item.id}/revisions" server_response = self.get_request(url, req_options) revisions = RevisionItem.from_response(server_response.content, self.parent_srv.namespace, workbook_item) return revisions @@ -456,9 +450,9 @@ def download_revision( error = "Workbook ID undefined." raise ValueError(error) if revision_number is None: - url = "{0}/{1}/content".format(self.baseurl, workbook_id) + url = f"{self.baseurl}/{workbook_id}/content" else: - url = "{0}/{1}/revisions/{2}/content".format(self.baseurl, workbook_id, revision_number) + url = f"{self.baseurl}/{workbook_id}/revisions/{revision_number}/content" if not include_extract: url += "?includeExtract=False" @@ -480,9 +474,7 @@ def download_revision( f.write(chunk) return_path = os.path.abspath(download_path) - logger.info( - "Downloaded workbook revision {0} to {1} (ID: {2})".format(revision_number, return_path, workbook_id) - ) + logger.info(f"Downloaded workbook revision {revision_number} to {return_path} (ID: {workbook_id})") return return_path @api(version="2.3") @@ -492,17 +484,17 @@ def delete_revision(self, workbook_id: str, revision_number: str) -> None: url = "/".join([self.baseurl, workbook_id, "revisions", revision_number]) self.delete_request(url) - logger.info("Deleted single workbook revision (ID: {0}) (Revision: {1})".format(workbook_id, revision_number)) + logger.info(f"Deleted single workbook revision (ID: {workbook_id}) (Revision: {revision_number})") # a convenience method @api(version="2.8") def schedule_extract_refresh( self, schedule_id: str, item: WorkbookItem - ) -> List["AddResponse"]: # actually should return a task + ) -> list["AddResponse"]: # actually should return a task return self.parent_srv.schedules.add_to_schedule(schedule_id, workbook=item) @api(version="1.0") - def add_tags(self, item: Union[WorkbookItem, str], tags: Union[Iterable[str], str]) -> Set[str]: + def add_tags(self, item: Union[WorkbookItem, str], tags: Union[Iterable[str], str]) -> set[str]: return super().add_tags(item, tags) @api(version="1.0") diff --git a/tableauserverclient/server/filter.py b/tableauserverclient/server/filter.py index b936ceb92..fd90e281f 100644 --- a/tableauserverclient/server/filter.py +++ b/tableauserverclient/server/filter.py @@ -1,7 +1,7 @@ from .request_options import RequestOptions -class Filter(object): +class Filter: def __init__(self, field, operator, value): self.field = field self.operator = operator @@ -16,7 +16,7 @@ def __str__(self): # to [,] # so effectively, remove any spaces between "," and "'" and then remove all "'" value_string = value_string.replace(", '", ",'").replace("'", "") - return "{0}:{1}:{2}".format(self.field, self.operator, value_string) + return f"{self.field}:{self.operator}:{value_string}" @property def value(self): diff --git a/tableauserverclient/server/pager.py b/tableauserverclient/server/pager.py index ca9d83872..e6d261b61 100644 --- a/tableauserverclient/server/pager.py +++ b/tableauserverclient/server/pager.py @@ -1,6 +1,7 @@ import copy from functools import partial -from typing import Iterable, Iterator, List, Optional, Protocol, Tuple, TypeVar, Union, runtime_checkable +from typing import Optional, Protocol, TypeVar, Union, runtime_checkable +from collections.abc import Iterable, Iterator from tableauserverclient.models.pagination_item import PaginationItem from tableauserverclient.server.request_options import RequestOptions @@ -11,14 +12,12 @@ @runtime_checkable class Endpoint(Protocol[T]): - def get(self, req_options: Optional[RequestOptions]) -> Tuple[List[T], PaginationItem]: - ... + def get(self, req_options: Optional[RequestOptions]) -> tuple[list[T], PaginationItem]: ... @runtime_checkable class CallableEndpoint(Protocol[T]): - def __call__(self, __req_options: Optional[RequestOptions], **kwargs) -> Tuple[List[T], PaginationItem]: - ... + def __call__(self, __req_options: Optional[RequestOptions], **kwargs) -> tuple[list[T], PaginationItem]: ... class Pager(Iterable[T]): @@ -27,7 +26,7 @@ class Pager(Iterable[T]): Supports all `RequestOptions` including starting on any page. Also used by models to load sub-models (users in a group, views in a workbook, etc) by passing a different endpoint. - Will loop over anything that returns (List[ModelItem], PaginationItem). + Will loop over anything that returns (list[ModelItem], PaginationItem). """ def __init__( diff --git a/tableauserverclient/server/query.py b/tableauserverclient/server/query.py index bbca612e9..feebc1a7e 100644 --- a/tableauserverclient/server/query.py +++ b/tableauserverclient/server/query.py @@ -1,8 +1,10 @@ -from collections.abc import Sized +from collections.abc import Iterable, Iterator, Sized from itertools import count -from typing import Iterable, Iterator, List, Optional, Protocol, Tuple, TYPE_CHECKING, TypeVar, overload +from typing import Optional, Protocol, TYPE_CHECKING, TypeVar, overload +import sys from tableauserverclient.config import config from tableauserverclient.models.pagination_item import PaginationItem +from tableauserverclient.server.endpoint.exceptions import ServerResponseError from tableauserverclient.server.filter import Filter from tableauserverclient.server.request_options import RequestOptions from tableauserverclient.server.sort import Sort @@ -34,10 +36,36 @@ def to_camel_case(word: str) -> str: class QuerySet(Iterable[T], Sized): + """ + QuerySet is a class that allows easy filtering, sorting, and iterating over + many endpoints in TableauServerClient. It is designed to be used in a similar + way to Django QuerySets, but with a more limited feature set. + + QuerySet is an iterable, and can be used in for loops, list comprehensions, + and other places where iterables are expected. + + QuerySet is also Sized, and can be used in places where the length of the + QuerySet is needed. The length of the QuerySet is the total number of items + available in the QuerySet, not just the number of items that have been + fetched. If the endpoint does not return a total count of items, the length + of the QuerySet will be sys.maxsize. If there is no total count, the + QuerySet will continue to fetch items until there are no more items to + fetch. + + QuerySet is not re-entrant. It is not designed to be used in multiple places + at the same time. If you need to use a QuerySet in multiple places, you + should create a new QuerySet for each place you need to use it, convert it + to a list, or create a deep copy of the QuerySet. + + QuerySets are also indexable, and can be sliced. If you try to access an + index that has not been fetched, the QuerySet will fetch the page that + contains the item you are looking for. + """ + def __init__(self, model: "QuerysetEndpoint[T]", page_size: Optional[int] = None) -> None: self.model = model self.request_options = RequestOptions(pagesize=page_size or config.PAGE_SIZE) - self._result_cache: List[T] = [] + self._result_cache: list[T] = [] self._pagination_item = PaginationItem() def __iter__(self: Self) -> Iterator[T]: @@ -49,19 +77,27 @@ def __iter__(self: Self) -> Iterator[T]: for page in count(1): self.request_options.pagenumber = page self._result_cache = [] - self._fetch_all() + try: + self._fetch_all() + except ServerResponseError as e: + if e.code == "400006": + # If the endpoint does not support pagination, it will end + # up overrunning the total number of pages. Catch the + # error and break out of the loop. + raise StopIteration yield from self._result_cache - # Set result_cache to empty so the fetch will populate - if (page * self.page_size) >= len(self): + # If the length of the QuerySet is unknown, continue fetching until + # the result cache is empty. + if (size := len(self)) == 0: + continue + if (page * self.page_size) >= size: return @overload - def __getitem__(self: Self, k: Slice) -> List[T]: - ... + def __getitem__(self: Self, k: Slice) -> list[T]: ... @overload - def __getitem__(self: Self, k: int) -> T: - ... + def __getitem__(self: Self, k: int) -> T: ... def __getitem__(self, k): page = self.page_number @@ -115,10 +151,15 @@ def _fetch_all(self: Self) -> None: Retrieve the data and store result and pagination item in cache """ if not self._result_cache: - self._result_cache, self._pagination_item = self.model.get(self.request_options) + response = self.model.get(self.request_options) + if isinstance(response, tuple): + self._result_cache, self._pagination_item = response + else: + self._result_cache = response + self._pagination_item = PaginationItem() def __len__(self: Self) -> int: - return self.total_available + return self.total_available or sys.maxsize @property def total_available(self: Self) -> int: @@ -128,12 +169,16 @@ def total_available(self: Self) -> int: @property def page_number(self: Self) -> int: self._fetch_all() - return self._pagination_item.page_number + # If the PaginationItem is not returned from the endpoint, use the + # pagenumber from the RequestOptions. + return self._pagination_item.page_number or self.request_options.pagenumber @property def page_size(self: Self) -> int: self._fetch_all() - return self._pagination_item.page_size + # If the PaginationItem is not returned from the endpoint, use the + # pagesize from the RequestOptions. + return self._pagination_item.page_size or self.request_options.pagesize def filter(self: Self, *invalid, page_size: Optional[int] = None, **kwargs) -> Self: if invalid: @@ -160,22 +205,22 @@ def paginate(self: Self, **kwargs) -> Self: return self @staticmethod - def _parse_shorthand_filter(key: str) -> Tuple[str, str]: + def _parse_shorthand_filter(key: str) -> tuple[str, str]: tokens = key.split("__", 1) if len(tokens) == 1: operator = RequestOptions.Operator.Equals else: operator = tokens[1] if operator not in RequestOptions.Operator.__dict__.values(): - raise ValueError("Operator `{}` is not valid.".format(operator)) + raise ValueError(f"Operator `{operator}` is not valid.") field = to_camel_case(tokens[0]) if field not in RequestOptions.Field.__dict__.values(): - raise ValueError("Field name `{}` is not valid.".format(field)) + raise ValueError(f"Field name `{field}` is not valid.") return (field, operator) @staticmethod - def _parse_shorthand_sort(key: str) -> Tuple[str, str]: + def _parse_shorthand_sort(key: str) -> tuple[str, str]: direction = RequestOptions.Direction.Asc if key.startswith("-"): direction = RequestOptions.Direction.Desc diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index 96fa14680..f7bd139d7 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -1,5 +1,6 @@ import xml.etree.ElementTree as ET -from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, TypeVar, TYPE_CHECKING, Union +from typing import Any, Callable, Optional, TypeVar, TYPE_CHECKING, Union +from collections.abc import Iterable from typing_extensions import ParamSpec @@ -15,7 +16,7 @@ # this file could be largely replaced if we were willing to import the huge file from generateDS -def _add_multipart(parts: Dict) -> Tuple[Any, str]: +def _add_multipart(parts: dict) -> tuple[Any, str]: mime_multipart_parts = list() for name, (filename, data, content_type) in parts.items(): multipart_part = RequestField(name=name, data=data, filename=filename) @@ -80,7 +81,7 @@ def _add_credentials_element(parent_element, connection_credentials): credentials_element.attrib["oAuth"] = "true" -class AuthRequest(object): +class AuthRequest: def signin_req(self, auth_item): xml_request = ET.Element("tsRequest") @@ -104,7 +105,7 @@ def switch_req(self, site_content_url): return ET.tostring(xml_request) -class ColumnRequest(object): +class ColumnRequest: def update_req(self, column_item): xml_request = ET.Element("tsRequest") column_element = ET.SubElement(xml_request, "column") @@ -115,7 +116,7 @@ def update_req(self, column_item): return ET.tostring(xml_request) -class DataAlertRequest(object): +class DataAlertRequest: def add_user_to_alert(self, alert_item: "DataAlertItem", user_id: str) -> bytes: xml_request = ET.Element("tsRequest") user_element = ET.SubElement(xml_request, "user") @@ -140,7 +141,7 @@ def update_req(self, alert_item: "DataAlertItem") -> bytes: return ET.tostring(xml_request) -class DatabaseRequest(object): +class DatabaseRequest: def update_req(self, database_item): xml_request = ET.Element("tsRequest") database_element = ET.SubElement(xml_request, "database") @@ -159,7 +160,7 @@ def update_req(self, database_item): return ET.tostring(xml_request) -class DatasourceRequest(object): +class DatasourceRequest: def _generate_xml(self, datasource_item: DatasourceItem, connection_credentials=None, connections=None): xml_request = ET.Element("tsRequest") datasource_element = ET.SubElement(xml_request, "datasource") @@ -244,7 +245,7 @@ def publish_req_chunked(self, datasource_item, connection_credentials=None, conn return _add_multipart(parts) -class DQWRequest(object): +class DQWRequest: def add_req(self, dqw_item): xml_request = ET.Element("tsRequest") dqw_element = ET.SubElement(xml_request, "dataQualityWarning") @@ -274,7 +275,7 @@ def update_req(self, dqw_item): return ET.tostring(xml_request) -class FavoriteRequest(object): +class FavoriteRequest: def add_request(self, id_: Optional[str], target_type: str, label: Optional[str]) -> bytes: """ @@ -329,7 +330,7 @@ def add_workbook_req(self, id_: Optional[str], name: Optional[str]) -> bytes: return self.add_request(id_, Resource.Workbook, name) -class FileuploadRequest(object): +class FileuploadRequest: def chunk_req(self, chunk): parts = { "request_payload": ("", "", "text/xml"), @@ -338,8 +339,8 @@ def chunk_req(self, chunk): return _add_multipart(parts) -class FlowRequest(object): - def _generate_xml(self, flow_item: "FlowItem", connections: Optional[List["ConnectionItem"]] = None) -> bytes: +class FlowRequest: + def _generate_xml(self, flow_item: "FlowItem", connections: Optional[list["ConnectionItem"]] = None) -> bytes: xml_request = ET.Element("tsRequest") flow_element = ET.SubElement(xml_request, "flow") if flow_item.name is not None: @@ -370,8 +371,8 @@ def publish_req( flow_item: "FlowItem", filename: str, file_contents: bytes, - connections: Optional[List["ConnectionItem"]] = None, - ) -> Tuple[Any, str]: + connections: Optional[list["ConnectionItem"]] = None, + ) -> tuple[Any, str]: xml_request = self._generate_xml(flow_item, connections) parts = { @@ -380,14 +381,14 @@ def publish_req( } return _add_multipart(parts) - def publish_req_chunked(self, flow_item, connections=None) -> Tuple[Any, str]: + def publish_req_chunked(self, flow_item, connections=None) -> tuple[Any, str]: xml_request = self._generate_xml(flow_item, connections) parts = {"request_payload": ("", xml_request, "text/xml")} return _add_multipart(parts) -class GroupRequest(object): +class GroupRequest: def add_user_req(self, user_id: str) -> bytes: xml_request = ET.Element("tsRequest") user_element = ET.SubElement(xml_request, "user") @@ -477,7 +478,7 @@ def update_req( return ET.tostring(xml_request) -class PermissionRequest(object): +class PermissionRequest: def add_req(self, rules: Iterable[PermissionsRule]) -> bytes: xml_request = ET.Element("tsRequest") permissions_element = ET.SubElement(xml_request, "permissions") @@ -499,7 +500,7 @@ def _add_all_capabilities(self, capabilities_element, capabilities_map): capability_element.attrib["mode"] = mode -class ProjectRequest(object): +class ProjectRequest: def update_req(self, project_item: "ProjectItem") -> bytes: xml_request = ET.Element("tsRequest") project_element = ET.SubElement(xml_request, "project") @@ -530,7 +531,7 @@ def create_req(self, project_item: "ProjectItem") -> bytes: return ET.tostring(xml_request) -class ScheduleRequest(object): +class ScheduleRequest: def create_req(self, schedule_item): xml_request = ET.Element("tsRequest") schedule_element = ET.SubElement(xml_request, "schedule") @@ -609,7 +610,7 @@ def add_flow_req(self, id_: Optional[str], task_type: str = TaskItem.Type.RunFlo return self._add_to_req(id_, "flow", task_type) -class SiteRequest(object): +class SiteRequest: def update_req(self, site_item: "SiteItem", parent_srv: Optional["Server"] = None): xml_request = ET.Element("tsRequest") site_element = ET.SubElement(xml_request, "site") @@ -848,7 +849,7 @@ def set_versioned_flow_attributes(self, flows_all, flows_edit, flows_schedule, p warnings.warn("In version 3.10 and earlier there is only one option: FlowsEnabled") -class TableRequest(object): +class TableRequest: def update_req(self, table_item): xml_request = ET.Element("tsRequest") table_element = ET.SubElement(xml_request, "table") @@ -871,7 +872,7 @@ def update_req(self, table_item): content_types = Iterable[Union["ColumnItem", "DatabaseItem", "DatasourceItem", "FlowItem", "TableItem", "WorkbookItem"]] -class TagRequest(object): +class TagRequest: def add_req(self, tag_set): xml_request = ET.Element("tsRequest") tags_element = ET.SubElement(xml_request, "tags") @@ -881,7 +882,7 @@ def add_req(self, tag_set): return ET.tostring(xml_request) @_tsrequest_wrapped - def batch_create(self, element: ET.Element, tags: Set[str], content: content_types) -> bytes: + def batch_create(self, element: ET.Element, tags: set[str], content: content_types) -> bytes: tag_batch = ET.SubElement(element, "tagBatch") tags_element = ET.SubElement(tag_batch, "tags") for tag in tags: @@ -897,7 +898,7 @@ def batch_create(self, element: ET.Element, tags: Set[str], content: content_typ return ET.tostring(element) -class UserRequest(object): +class UserRequest: def update_req(self, user_item: UserItem, password: Optional[str]) -> bytes: xml_request = ET.Element("tsRequest") user_element = ET.SubElement(xml_request, "user") @@ -931,7 +932,7 @@ def add_req(self, user_item: UserItem) -> bytes: return ET.tostring(xml_request) -class WorkbookRequest(object): +class WorkbookRequest: def _generate_xml( self, workbook_item, @@ -995,9 +996,9 @@ def update_req(self, workbook_item): if data_freshness_policy_config.option == "FreshEvery": if data_freshness_policy_config.fresh_every_schedule is not None: fresh_every_element = ET.SubElement(data_freshness_policy_element, "freshEverySchedule") - fresh_every_element.attrib[ - "frequency" - ] = data_freshness_policy_config.fresh_every_schedule.frequency + fresh_every_element.attrib["frequency"] = ( + data_freshness_policy_config.fresh_every_schedule.frequency + ) fresh_every_element.attrib["value"] = str(data_freshness_policy_config.fresh_every_schedule.value) else: raise ValueError(f"data_freshness_policy_config.fresh_every_schedule must be populated.") @@ -1075,7 +1076,7 @@ def embedded_extract_req( datasource_element.attrib["id"] = id_ -class Connection(object): +class Connection: @_tsrequest_wrapped def update_req(self, xml_request: ET.Element, connection_item: "ConnectionItem") -> None: connection_element = ET.SubElement(xml_request, "connection") @@ -1098,7 +1099,7 @@ def update_req(self, xml_request: ET.Element, connection_item: "ConnectionItem") connection_element.attrib["queryTaggingEnabled"] = str(connection_item.query_tagging).lower() -class TaskRequest(object): +class TaskRequest: @_tsrequest_wrapped def run_req(self, xml_request: ET.Element, task_item: Any) -> None: # Send an empty tsRequest @@ -1137,7 +1138,7 @@ def create_extract_req(self, xml_request: ET.Element, extract_item: "TaskItem") return ET.tostring(xml_request) -class FlowTaskRequest(object): +class FlowTaskRequest: @_tsrequest_wrapped def create_flow_task_req(self, xml_request: ET.Element, flow_item: "TaskItem") -> bytes: flow_element = ET.SubElement(xml_request, "runFlow") @@ -1171,7 +1172,7 @@ def create_flow_task_req(self, xml_request: ET.Element, flow_item: "TaskItem") - return ET.tostring(xml_request) -class SubscriptionRequest(object): +class SubscriptionRequest: @_tsrequest_wrapped def create_req(self, xml_request: ET.Element, subscription_item: "SubscriptionItem") -> bytes: subscription_element = ET.SubElement(xml_request, "subscription") @@ -1235,13 +1236,13 @@ def update_req(self, xml_request: ET.Element, subscription_item: "SubscriptionIt return ET.tostring(xml_request) -class EmptyRequest(object): +class EmptyRequest: @_tsrequest_wrapped def empty_req(self, xml_request: ET.Element) -> None: pass -class WebhookRequest(object): +class WebhookRequest: @_tsrequest_wrapped def create_req(self, xml_request: ET.Element, webhook_item: "WebhookItem") -> bytes: webhook = ET.SubElement(xml_request, "webhook") @@ -1287,7 +1288,7 @@ def update_req(self, xml_request: ET.Element, metric_item: MetricItem) -> bytes: return ET.tostring(xml_request) -class CustomViewRequest(object): +class CustomViewRequest: @_tsrequest_wrapped def update_req(self, xml_request: ET.Element, custom_view_item: CustomViewItem): updating_element = ET.SubElement(xml_request, "customView") @@ -1415,7 +1416,7 @@ def publish(self, xml_request: ET.Element, virtual_connection: VirtualConnection return ET.tostring(xml_request) -class RequestFactory(object): +class RequestFactory: Auth = AuthRequest() Connection = Connection() Column = ColumnRequest() diff --git a/tableauserverclient/server/request_options.py b/tableauserverclient/server/request_options.py index ddb45834d..a3ad0c498 100644 --- a/tableauserverclient/server/request_options.py +++ b/tableauserverclient/server/request_options.py @@ -9,12 +9,12 @@ from tableauserverclient.helpers.logging import logger -class RequestOptionsBase(object): +class RequestOptionsBase: # This method is used if server api version is below 3.7 (2020.1) def apply_query_params(self, url): try: params = self.get_query_params() - params_list = ["{}={}".format(k, v) for (k, v) in params.items()] + params_list = [f"{k}={v}" for (k, v) in params.items()] logger.debug("Applying options to request: <%s(%s)>", self.__class__.__name__, ",".join(params_list)) @@ -22,7 +22,7 @@ def apply_query_params(self, url): url, existing_params = url.split("?") params_list.append(existing_params) - return "{0}?{1}".format(url, "&".join(params_list)) + return "{}?{}".format(url, "&".join(params_list)) except NotImplementedError: raise @@ -164,13 +164,14 @@ def get_query_params(self): raise NotImplementedError() def vf(self, name: str, value: str) -> Self: - """Apply a filter to the view for a filter that is a normal column - within the view.""" + """Apply a filter based on a column within the view. + Note that when filtering on a boolean type field, the only valid values are 'true' and 'false'""" self.view_filters.append((name, value)) return self def parameter(self, name: str, value: str) -> Self: - """Apply a filter based on a parameter within the workbook.""" + """Apply a filter based on a parameter within the workbook. + Note that when filtering on a boolean type field, the only valid values are 'true' and 'false'""" self.view_parameters.append((name, value)) return self @@ -183,7 +184,7 @@ def _append_view_filters(self, params) -> None: class CSVRequestOptions(_FilterOptionsBase): def __init__(self, maxage=-1): - super(CSVRequestOptions, self).__init__() + super().__init__() self.max_age = maxage @property @@ -233,7 +234,7 @@ class Resolution: High = "high" def __init__(self, imageresolution=None, maxage=-1): - super(ImageRequestOptions, self).__init__() + super().__init__() self.image_resolution = imageresolution self.max_age = maxage @@ -278,7 +279,7 @@ class Orientation: Landscape = "landscape" def __init__(self, page_type=None, orientation=None, maxage=-1, viz_height=None, viz_width=None): - super(PDFRequestOptions, self).__init__() + super().__init__() self.page_type = page_type self.orientation = orientation self.max_age = maxage diff --git a/tableauserverclient/server/server.py b/tableauserverclient/server/server.py index e563a7138..dab5911db 100644 --- a/tableauserverclient/server/server.py +++ b/tableauserverclient/server/server.py @@ -58,7 +58,7 @@ default_server_version = "2.4" # first version that dropped the legacy auth endpoint -class Server(object): +class Server: class PublishMode: Append = "Append" Overwrite = "Overwrite" @@ -130,7 +130,7 @@ def validate_connection_settings(self): raise ValueError("Server connection settings not valid", req_ex) def __repr__(self): - return "".format(self.baseurl, self.server_info.serverInfo) + return f"" def add_http_options(self, options_dict: dict): try: @@ -142,7 +142,7 @@ def add_http_options(self, options_dict: dict): # expected errors on invalid input: # 'set' object has no attribute 'keys', 'list' object has no attribute 'keys' # TypeError: cannot convert dictionary update sequence element #0 to a sequence (input is a tuple) - raise ValueError("Invalid http options given: {}".format(options_dict)) + raise ValueError(f"Invalid http options given: {options_dict}") def clear_http_options(self): self._http_options = dict() @@ -176,15 +176,15 @@ def _determine_highest_version(self): old_version = self.version version = self.server_info.get().rest_api_version except ServerInfoEndpointNotFoundError as e: - logger.info("Could not get version info from server: {}{}".format(e.__class__, e)) + logger.info(f"Could not get version info from server: {e.__class__}{e}") version = self._get_legacy_version() except EndpointUnavailableError as e: - logger.info("Could not get version info from server: {}{}".format(e.__class__, e)) + logger.info(f"Could not get version info from server: {e.__class__}{e}") version = self._get_legacy_version() except Exception as e: - logger.info("Could not get version info from server: {}{}".format(e.__class__, e)) + logger.info(f"Could not get version info from server: {e.__class__}{e}") version = None - logger.info("versions: {}, {}".format(version, old_version)) + logger.info(f"versions: {version}, {old_version}") return version or old_version def use_server_version(self): @@ -201,12 +201,12 @@ def check_at_least_version(self, target: str): def assert_at_least_version(self, comparison: str, reason: str): if not self.check_at_least_version(comparison): - error = "{} is not available in API version {}. Requires {}".format(reason, self.version, comparison) + error = f"{reason} is not available in API version {self.version}. Requires {comparison}" raise EndpointUnavailableError(error) @property def baseurl(self): - return "{0}/api/{1}".format(self._server_address, str(self.version)) + return f"{self._server_address}/api/{str(self.version)}" @property def namespace(self): diff --git a/tableauserverclient/server/sort.py b/tableauserverclient/server/sort.py index 2d6bc030a..839a8c8db 100644 --- a/tableauserverclient/server/sort.py +++ b/tableauserverclient/server/sort.py @@ -1,7 +1,7 @@ -class Sort(object): +class Sort: def __init__(self, field, direction): self.field = field self.direction = direction def __str__(self): - return "{0}:{1}".format(self.field, self.direction) + return f"{self.field}:{self.direction}" diff --git a/test/_utils.py b/test/_utils.py index 8527aaf8c..b4ee93bc3 100644 --- a/test/_utils.py +++ b/test/_utils.py @@ -1,5 +1,6 @@ import os.path import unittest +from xml.etree import ElementTree as ET from contextlib import contextmanager TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), "assets") @@ -18,6 +19,19 @@ def read_xml_assets(*args): return map(read_xml_asset, args) +def server_response_error_factory(code: str, summary: str, detail: str) -> str: + root = ET.Element("tsResponse") + error = ET.SubElement(root, "error") + error.attrib["code"] = code + + summary_element = ET.SubElement(error, "summary") + summary_element.text = summary + + detail_element = ET.SubElement(error, "detail") + detail_element.text = detail + return ET.tostring(root, encoding="utf-8").decode("utf-8") + + @contextmanager def mocked_time(): mock_time = 0 diff --git a/test/assets/flow_runs_get.xml b/test/assets/flow_runs_get.xml index bdce4cdfb..489e8ac63 100644 --- a/test/assets/flow_runs_get.xml +++ b/test/assets/flow_runs_get.xml @@ -1,5 +1,4 @@ - - \ No newline at end of file + diff --git a/test/test_dataalert.py b/test/test_dataalert.py index d9e00a9db..6f6f1683c 100644 --- a/test/test_dataalert.py +++ b/test/test_dataalert.py @@ -108,5 +108,5 @@ def test_delete_user_from_alert(self) -> None: alert_id = "5ea59b45-e497-5673-8809-bfe213236f75" user_id = "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" with requests_mock.mock() as m: - m.delete(self.baseurl + "/{0}/users/{1}".format(alert_id, user_id), status_code=204) + m.delete(self.baseurl + f"/{alert_id}/users/{user_id}", status_code=204) self.server.data_alerts.delete_user_from_alert(alert_id, user_id) diff --git a/test/test_datasource.py b/test/test_datasource.py index 624eb93e1..45d9ba9c9 100644 --- a/test/test_datasource.py +++ b/test/test_datasource.py @@ -75,7 +75,7 @@ def test_get(self) -> None: self.assertEqual("Sample datasource", all_datasources[1].name) self.assertEqual("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", all_datasources[1].project_id) self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", all_datasources[1].owner_id) - self.assertEqual(set(["world", "indicators", "sample"]), all_datasources[1].tags) + self.assertEqual({"world", "indicators", "sample"}, all_datasources[1].tags) self.assertEqual("https://page.com", all_datasources[1].webpage_url) self.assertTrue(all_datasources[1].encrypt_extracts) self.assertFalse(all_datasources[1].has_extracts) @@ -110,7 +110,7 @@ def test_get_by_id(self) -> None: self.assertEqual("Sample datasource", single_datasource.name) self.assertEqual("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", single_datasource.project_id) self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", single_datasource.owner_id) - self.assertEqual(set(["world", "indicators", "sample"]), single_datasource.tags) + self.assertEqual({"world", "indicators", "sample"}, single_datasource.tags) self.assertEqual(TSC.DatasourceItem.AskDataEnablement.SiteDefault, single_datasource.ask_data_enablement) def test_update(self) -> None: @@ -488,7 +488,7 @@ def test_download_object(self) -> None: def test_download_sanitizes_name(self) -> None: filename = "Name,With,Commas.tds" - disposition = 'name="tableau_workbook"; filename="{}"'.format(filename) + disposition = f'name="tableau_workbook"; filename="{filename}"' with requests_mock.mock() as m: m.get( self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/content", @@ -659,7 +659,7 @@ def test_revisions(self) -> None: response_xml = read_xml_asset(REVISION_XML) with requests_mock.mock() as m: - m.get("{0}/{1}/revisions".format(self.baseurl, datasource.id), text=response_xml) + m.get(f"{self.baseurl}/{datasource.id}/revisions", text=response_xml) self.server.datasources.populate_revisions(datasource) revisions = datasource.revisions @@ -687,7 +687,7 @@ def test_delete_revision(self) -> None: datasource._id = "06b944d2-959d-4604-9305-12323c95e70e" with requests_mock.mock() as m: - m.delete("{0}/{1}/revisions/3".format(self.baseurl, datasource.id)) + m.delete(f"{self.baseurl}/{datasource.id}/revisions/3") self.server.datasources.delete_revision(datasource.id, "3") def test_download_revision(self) -> None: diff --git a/test/test_endpoint.py b/test/test_endpoint.py index 8635af978..ff1ef0f72 100644 --- a/test/test_endpoint.py +++ b/test/test_endpoint.py @@ -54,7 +54,7 @@ def test_get_request_stream(self) -> None: self.assertFalse(response._content_consumed) def test_binary_log_truncated(self): - class FakeResponse(object): + class FakeResponse: headers = {"Content-Type": "application/octet-stream"} content = b"\x1337" * 1000 status_code = 200 diff --git a/test/test_favorites.py b/test/test_favorites.py index 6f0be3b3c..87332d70f 100644 --- a/test/test_favorites.py +++ b/test/test_favorites.py @@ -28,7 +28,7 @@ def setUp(self): def test_get(self) -> None: response_xml = read_xml_asset(GET_FAVORITES_XML) with requests_mock.mock() as m: - m.get("{0}/{1}".format(self.baseurl, self.user.id), text=response_xml) + m.get(f"{self.baseurl}/{self.user.id}", text=response_xml) self.server.favorites.get(self.user) self.assertIsNotNone(self.user._favorites) self.assertEqual(len(self.user.favorites["workbooks"]), 1) @@ -54,7 +54,7 @@ def test_add_favorite_workbook(self) -> None: workbook._id = "6d13b0ca-043d-4d42-8c9d-3f3313ea3a00" workbook.name = "Superstore" with requests_mock.mock() as m: - m.put("{0}/{1}".format(self.baseurl, self.user.id), text=response_xml) + m.put(f"{self.baseurl}/{self.user.id}", text=response_xml) self.server.favorites.add_favorite_workbook(self.user, workbook) def test_add_favorite_view(self) -> None: @@ -63,7 +63,7 @@ def test_add_favorite_view(self) -> None: view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" view._name = "ENDANGERED SAFARI" with requests_mock.mock() as m: - m.put("{0}/{1}".format(self.baseurl, self.user.id), text=response_xml) + m.put(f"{self.baseurl}/{self.user.id}", text=response_xml) self.server.favorites.add_favorite_view(self.user, view) def test_add_favorite_datasource(self) -> None: @@ -72,7 +72,7 @@ def test_add_favorite_datasource(self) -> None: datasource._id = "e76a1461-3b1d-4588-bf1b-17551a879ad9" datasource.name = "SampleDS" with requests_mock.mock() as m: - m.put("{0}/{1}".format(self.baseurl, self.user.id), text=response_xml) + m.put(f"{self.baseurl}/{self.user.id}", text=response_xml) self.server.favorites.add_favorite_datasource(self.user, datasource) def test_add_favorite_project(self) -> None: @@ -82,7 +82,7 @@ def test_add_favorite_project(self) -> None: project = TSC.ProjectItem("Tableau") project._id = "1d0304cd-3796-429f-b815-7258370b9b74" with requests_mock.mock() as m: - m.put("{0}/{1}".format(baseurl, self.user.id), text=response_xml) + m.put(f"{baseurl}/{self.user.id}", text=response_xml) self.server.favorites.add_favorite_project(self.user, project) def test_delete_favorite_workbook(self) -> None: @@ -90,7 +90,7 @@ def test_delete_favorite_workbook(self) -> None: workbook._id = "6d13b0ca-043d-4d42-8c9d-3f3313ea3a00" workbook.name = "Superstore" with requests_mock.mock() as m: - m.delete("{0}/{1}/workbooks/{2}".format(self.baseurl, self.user.id, workbook.id)) + m.delete(f"{self.baseurl}/{self.user.id}/workbooks/{workbook.id}") self.server.favorites.delete_favorite_workbook(self.user, workbook) def test_delete_favorite_view(self) -> None: @@ -98,7 +98,7 @@ def test_delete_favorite_view(self) -> None: view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" view._name = "ENDANGERED SAFARI" with requests_mock.mock() as m: - m.delete("{0}/{1}/views/{2}".format(self.baseurl, self.user.id, view.id)) + m.delete(f"{self.baseurl}/{self.user.id}/views/{view.id}") self.server.favorites.delete_favorite_view(self.user, view) def test_delete_favorite_datasource(self) -> None: @@ -106,7 +106,7 @@ def test_delete_favorite_datasource(self) -> None: datasource._id = "e76a1461-3b1d-4588-bf1b-17551a879ad9" datasource.name = "SampleDS" with requests_mock.mock() as m: - m.delete("{0}/{1}/datasources/{2}".format(self.baseurl, self.user.id, datasource.id)) + m.delete(f"{self.baseurl}/{self.user.id}/datasources/{datasource.id}") self.server.favorites.delete_favorite_datasource(self.user, datasource) def test_delete_favorite_project(self) -> None: @@ -115,5 +115,5 @@ def test_delete_favorite_project(self) -> None: project = TSC.ProjectItem("Tableau") project._id = "1d0304cd-3796-429f-b815-7258370b9b74" with requests_mock.mock() as m: - m.delete("{0}/{1}/projects/{2}".format(baseurl, self.user.id, project.id)) + m.delete(f"{baseurl}/{self.user.id}/projects/{project.id}") self.server.favorites.delete_favorite_project(self.user, project) diff --git a/test/test_filesys_helpers.py b/test/test_filesys_helpers.py index 4c8fb0f9f..0f3234d5d 100644 --- a/test/test_filesys_helpers.py +++ b/test/test_filesys_helpers.py @@ -37,7 +37,7 @@ def test_get_file_type_identifies_a_zip_file(self): with BytesIO() as file_object: with ZipFile(file_object, "w") as zf: with BytesIO() as stream: - stream.write("This is a zip file".encode()) + stream.write(b"This is a zip file") zf.writestr("dummy_file", stream.getbuffer()) file_object.seek(0) file_type = get_file_type(file_object) diff --git a/test/test_fileuploads.py b/test/test_fileuploads.py index 50a5ef48b..9567bc3ad 100644 --- a/test/test_fileuploads.py +++ b/test/test_fileuploads.py @@ -33,7 +33,7 @@ def setUp(self): self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - self.baseurl = "{}/sites/{}/fileUploads".format(self.server.baseurl, self.server.site_id) + self.baseurl = f"{self.server.baseurl}/sites/{self.server.site_id}/fileUploads" def test_read_chunks_file_path(self): file_path = asset("SampleWB.twbx") @@ -57,7 +57,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("{}/{}".format(self.baseurl, upload_id), text=append_response_xml) + m.put(f"{self.baseurl}/{upload_id}", text=append_response_xml) actual = self.server.fileuploads.upload(file_path) self.assertEqual(upload_id, actual) @@ -72,7 +72,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("{}/{}".format(self.baseurl, upload_id), text=append_response_xml) + m.put(f"{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_flowruns.py b/test/test_flowruns.py index 864c0d3cd..8af2540dc 100644 --- a/test/test_flowruns.py +++ b/test/test_flowruns.py @@ -1,3 +1,4 @@ +import sys import unittest import requests_mock @@ -5,7 +6,7 @@ import tableauserverclient as TSC from tableauserverclient.datetime_helpers import format_datetime from tableauserverclient.server.endpoint.exceptions import FlowRunFailedException -from ._utils import read_xml_asset, mocked_time +from ._utils import read_xml_asset, mocked_time, server_response_error_factory GET_XML = "flow_runs_get.xml" GET_BY_ID_XML = "flow_runs_get_by_id.xml" @@ -28,9 +29,8 @@ def test_get(self) -> None: response_xml = read_xml_asset(GET_XML) with requests_mock.mock() as m: m.get(self.baseurl, text=response_xml) - all_flow_runs, pagination_item = self.server.flow_runs.get() + all_flow_runs = self.server.flow_runs.get() - self.assertEqual(2, pagination_item.total_available) self.assertEqual("cc2e652d-4a9b-4476-8c93-b238c45db968", all_flow_runs[0].id) self.assertEqual("2021-02-11T01:42:55Z", format_datetime(all_flow_runs[0].started_at)) self.assertEqual("2021-02-11T01:57:38Z", format_datetime(all_flow_runs[0].completed_at)) @@ -75,7 +75,7 @@ def test_wait_for_job_finished(self) -> None: response_xml = read_xml_asset(GET_BY_ID_XML) flow_run_id = "cc2e652d-4a9b-4476-8c93-b238c45db968" with mocked_time(), requests_mock.mock() as m: - m.get("{0}/{1}".format(self.baseurl, flow_run_id), text=response_xml) + m.get(f"{self.baseurl}/{flow_run_id}", text=response_xml) flow_run = self.server.flow_runs.wait_for_job(flow_run_id) self.assertEqual(flow_run_id, flow_run.id) @@ -86,7 +86,7 @@ def test_wait_for_job_failed(self) -> None: response_xml = read_xml_asset(GET_BY_ID_FAILED_XML) flow_run_id = "c2b35d5a-e130-471a-aec8-7bc5435fe0e7" with mocked_time(), requests_mock.mock() as m: - m.get("{0}/{1}".format(self.baseurl, flow_run_id), text=response_xml) + m.get(f"{self.baseurl}/{flow_run_id}", text=response_xml) with self.assertRaises(FlowRunFailedException): self.server.flow_runs.wait_for_job(flow_run_id) @@ -95,6 +95,17 @@ def test_wait_for_job_timeout(self) -> None: response_xml = read_xml_asset(GET_BY_ID_INPROGRESS_XML) flow_run_id = "71afc22c-9c06-40be-8d0f-4c4166d29e6c" with mocked_time(), requests_mock.mock() as m: - m.get("{0}/{1}".format(self.baseurl, flow_run_id), text=response_xml) + m.get(f"{self.baseurl}/{flow_run_id}", text=response_xml) with self.assertRaises(TimeoutError): self.server.flow_runs.wait_for_job(flow_run_id, timeout=30) + + def test_queryset(self) -> None: + response_xml = read_xml_asset(GET_XML) + error_response = server_response_error_factory( + "400006", "Bad Request", "0xB4EAB088 : The start index '9900' is greater than or equal to the total count.)" + ) + with requests_mock.mock() as m: + m.get(f"{self.baseurl}?pageNumber=1", text=response_xml) + m.get(f"{self.baseurl}?pageNumber=2", text=error_response) + queryset = self.server.flow_runs.all() + assert len(queryset) == sys.maxsize diff --git a/test/test_flowtask.py b/test/test_flowtask.py index 034066e64..2d9f7c7bd 100644 --- a/test/test_flowtask.py +++ b/test/test_flowtask.py @@ -40,7 +40,7 @@ def test_create_flow_task(self): with open(GET_XML_CREATE_FLOW_TASK_RESPONSE, "rb") as f: response_xml = f.read().decode("utf-8") with requests_mock.mock() as m: - m.post("{}".format(self.baseurl), text=response_xml) + m.post(f"{self.baseurl}", text=response_xml) create_response_content = self.server.flow_tasks.create(task).decode("utf-8") self.assertTrue("schedule_id" in create_response_content) diff --git a/test/test_group.py b/test/test_group.py index fc9c75a6d..41b5992be 100644 --- a/test/test_group.py +++ b/test/test_group.py @@ -1,4 +1,3 @@ -# encoding=utf-8 from pathlib import Path import unittest import os diff --git a/test/test_job.py b/test/test_job.py index d86397086..20b238764 100644 --- a/test/test_job.py +++ b/test/test_job.py @@ -51,7 +51,7 @@ def test_get_by_id(self) -> None: response_xml = read_xml_asset(GET_BY_ID_XML) job_id = "2eef4225-aa0c-41c4-8662-a76d89ed7336" with requests_mock.mock() as m: - m.get("{0}/{1}".format(self.baseurl, job_id), text=response_xml) + m.get(f"{self.baseurl}/{job_id}", text=response_xml) job = self.server.jobs.get_by_id(job_id) updated_at = datetime(2020, 5, 13, 20, 25, 18, tzinfo=utc) @@ -81,7 +81,7 @@ def test_wait_for_job_finished(self) -> None: response_xml = read_xml_asset(GET_BY_ID_XML) job_id = "2eef4225-aa0c-41c4-8662-a76d89ed7336" with mocked_time(), requests_mock.mock() as m: - m.get("{0}/{1}".format(self.baseurl, job_id), text=response_xml) + m.get(f"{self.baseurl}/{job_id}", text=response_xml) job = self.server.jobs.wait_for_job(job_id) self.assertEqual(job_id, job.id) @@ -92,7 +92,7 @@ def test_wait_for_job_failed(self) -> None: response_xml = read_xml_asset(GET_BY_ID_FAILED_XML) job_id = "77d5e57a-2517-479f-9a3c-a32025f2b64d" with mocked_time(), requests_mock.mock() as m: - m.get("{0}/{1}".format(self.baseurl, job_id), text=response_xml) + m.get(f"{self.baseurl}/{job_id}", text=response_xml) with self.assertRaises(JobFailedException): self.server.jobs.wait_for_job(job_id) @@ -101,7 +101,7 @@ def test_wait_for_job_timeout(self) -> None: response_xml = read_xml_asset(GET_BY_ID_INPROGRESS_XML) job_id = "77d5e57a-2517-479f-9a3c-a32025f2b64d" with mocked_time(), requests_mock.mock() as m: - m.get("{0}/{1}".format(self.baseurl, job_id), text=response_xml) + m.get(f"{self.baseurl}/{job_id}", text=response_xml) with self.assertRaises(TimeoutError): self.server.jobs.wait_for_job(job_id, timeout=30) diff --git a/test/test_project.py b/test/test_project.py index e05785f86..430db84b2 100644 --- a/test/test_project.py +++ b/test/test_project.py @@ -241,9 +241,9 @@ def test_delete_permission(self) -> None: rules = TSC.PermissionsRule(grantee=GroupItem.as_reference(single_group._id), capabilities=capabilities) - endpoint = "{}/permissions/groups/{}".format(single_project._id, single_group._id) - m.delete("{}/{}/Read/Allow".format(self.baseurl, endpoint), status_code=204) - m.delete("{}/{}/Write/Allow".format(self.baseurl, endpoint), status_code=204) + endpoint = f"{single_project._id}/permissions/groups/{single_group._id}" + m.delete(f"{self.baseurl}/{endpoint}/Read/Allow", status_code=204) + m.delete(f"{self.baseurl}/{endpoint}/Write/Allow", status_code=204) self.server.projects.delete_permission(item=single_project, rules=rules) def test_delete_workbook_default_permission(self) -> None: @@ -287,19 +287,19 @@ def test_delete_workbook_default_permission(self) -> None: rules = TSC.PermissionsRule(grantee=GroupItem.as_reference(single_group._id), capabilities=capabilities) - endpoint = "{}/default-permissions/workbooks/groups/{}".format(single_project._id, single_group._id) - m.delete("{}/{}/Read/Allow".format(self.baseurl, endpoint), status_code=204) - m.delete("{}/{}/ExportImage/Allow".format(self.baseurl, endpoint), status_code=204) - m.delete("{}/{}/ExportData/Allow".format(self.baseurl, endpoint), status_code=204) - m.delete("{}/{}/ViewComments/Allow".format(self.baseurl, endpoint), status_code=204) - m.delete("{}/{}/AddComment/Allow".format(self.baseurl, endpoint), status_code=204) - m.delete("{}/{}/Filter/Allow".format(self.baseurl, endpoint), status_code=204) - m.delete("{}/{}/ViewUnderlyingData/Deny".format(self.baseurl, endpoint), status_code=204) - m.delete("{}/{}/ShareView/Allow".format(self.baseurl, endpoint), status_code=204) - m.delete("{}/{}/WebAuthoring/Allow".format(self.baseurl, endpoint), status_code=204) - m.delete("{}/{}/Write/Allow".format(self.baseurl, endpoint), status_code=204) - m.delete("{}/{}/ExportXml/Allow".format(self.baseurl, endpoint), status_code=204) - m.delete("{}/{}/ChangeHierarchy/Allow".format(self.baseurl, endpoint), status_code=204) - m.delete("{}/{}/Delete/Deny".format(self.baseurl, endpoint), status_code=204) - m.delete("{}/{}/ChangePermissions/Allow".format(self.baseurl, endpoint), status_code=204) + endpoint = f"{single_project._id}/default-permissions/workbooks/groups/{single_group._id}" + m.delete(f"{self.baseurl}/{endpoint}/Read/Allow", status_code=204) + m.delete(f"{self.baseurl}/{endpoint}/ExportImage/Allow", status_code=204) + m.delete(f"{self.baseurl}/{endpoint}/ExportData/Allow", status_code=204) + m.delete(f"{self.baseurl}/{endpoint}/ViewComments/Allow", status_code=204) + m.delete(f"{self.baseurl}/{endpoint}/AddComment/Allow", status_code=204) + m.delete(f"{self.baseurl}/{endpoint}/Filter/Allow", status_code=204) + m.delete(f"{self.baseurl}/{endpoint}/ViewUnderlyingData/Deny", status_code=204) + m.delete(f"{self.baseurl}/{endpoint}/ShareView/Allow", status_code=204) + m.delete(f"{self.baseurl}/{endpoint}/WebAuthoring/Allow", status_code=204) + m.delete(f"{self.baseurl}/{endpoint}/Write/Allow", status_code=204) + m.delete(f"{self.baseurl}/{endpoint}/ExportXml/Allow", status_code=204) + m.delete(f"{self.baseurl}/{endpoint}/ChangeHierarchy/Allow", status_code=204) + m.delete(f"{self.baseurl}/{endpoint}/Delete/Deny", status_code=204) + m.delete(f"{self.baseurl}/{endpoint}/ChangePermissions/Allow", status_code=204) self.server.projects.delete_workbook_default_permissions(item=single_project, rule=rules) diff --git a/test/test_regression_tests.py b/test/test_regression_tests.py index 772704f69..62e301591 100644 --- a/test/test_regression_tests.py +++ b/test/test_regression_tests.py @@ -1,9 +1,5 @@ import unittest - -try: - from unittest import mock -except ImportError: - import mock # type: ignore[no-redef] +from unittest import mock import tableauserverclient.server.request_factory as factory from tableauserverclient.helpers.strings import redact_xml diff --git a/test/test_request_option.py b/test/test_request_option.py index e48f8510a..9ca9779ad 100644 --- a/test/test_request_option.py +++ b/test/test_request_option.py @@ -31,7 +31,7 @@ def setUp(self) -> None: self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - self.baseurl = "{0}/{1}".format(self.server.sites.baseurl, self.server._site_id) + self.baseurl = f"{self.server.sites.baseurl}/{self.server._site_id}" def test_pagination(self) -> None: with open(PAGINATION_XML, "rb") as f: @@ -112,9 +112,9 @@ def test_filter_tags_in(self) -> None: matching_workbooks, pagination_item = self.server.workbooks.get(req_option) self.assertEqual(3, pagination_item.total_available) - self.assertEqual(set(["weather"]), matching_workbooks[0].tags) - self.assertEqual(set(["safari"]), matching_workbooks[1].tags) - self.assertEqual(set(["sample"]), matching_workbooks[2].tags) + self.assertEqual({"weather"}, matching_workbooks[0].tags) + self.assertEqual({"safari"}, matching_workbooks[1].tags) + self.assertEqual({"sample"}, matching_workbooks[2].tags) # check if filtered projects with spaces & special characters # get correctly returned @@ -148,9 +148,9 @@ def test_filter_tags_in_shorthand(self) -> None: matching_workbooks = self.server.workbooks.filter(tags__in=["sample", "safari", "weather"]) self.assertEqual(3, matching_workbooks.total_available) - self.assertEqual(set(["weather"]), matching_workbooks[0].tags) - self.assertEqual(set(["safari"]), matching_workbooks[1].tags) - self.assertEqual(set(["sample"]), matching_workbooks[2].tags) + self.assertEqual({"weather"}, matching_workbooks[0].tags) + self.assertEqual({"safari"}, matching_workbooks[1].tags) + self.assertEqual({"sample"}, matching_workbooks[2].tags) def test_invalid_shorthand_option(self) -> None: with self.assertRaises(ValueError): diff --git a/test/test_schedule.py b/test/test_schedule.py index 0377295d7..1d329f86e 100644 --- a/test/test_schedule.py +++ b/test/test_schedule.py @@ -106,7 +106,7 @@ def test_get_by_id(self) -> None: response_xml = f.read().decode("utf-8") with requests_mock.mock() as m: schedule_id = "c9cff7f9-309c-4361-99ff-d4ba8c9f5467" - baseurl = "{}/schedules/{}".format(self.server.baseurl, schedule_id) + baseurl = f"{self.server.baseurl}/schedules/{schedule_id}" m.get(baseurl, text=response_xml) schedule = self.server.schedules.get_by_id(schedule_id) self.assertIsNotNone(schedule) @@ -120,7 +120,7 @@ def test_get_hourly_by_id(self) -> None: response_xml = f.read().decode("utf-8") with requests_mock.mock() as m: schedule_id = "c9cff7f9-309c-4361-99ff-d4ba8c9f5467" - baseurl = "{}/schedules/{}".format(self.server.baseurl, schedule_id) + baseurl = f"{self.server.baseurl}/schedules/{schedule_id}" m.get(baseurl, text=response_xml) schedule = self.server.schedules.get_by_id(schedule_id) self.assertIsNotNone(schedule) @@ -135,7 +135,7 @@ def test_get_daily_by_id(self) -> None: response_xml = f.read().decode("utf-8") with requests_mock.mock() as m: schedule_id = "c9cff7f9-309c-4361-99ff-d4ba8c9f5467" - baseurl = "{}/schedules/{}".format(self.server.baseurl, schedule_id) + baseurl = f"{self.server.baseurl}/schedules/{schedule_id}" m.get(baseurl, text=response_xml) schedule = self.server.schedules.get_by_id(schedule_id) self.assertIsNotNone(schedule) @@ -150,7 +150,7 @@ def test_get_monthly_by_id(self) -> None: response_xml = f.read().decode("utf-8") with requests_mock.mock() as m: schedule_id = "c9cff7f9-309c-4361-99ff-d4ba8c9f5467" - baseurl = "{}/schedules/{}".format(self.server.baseurl, schedule_id) + baseurl = f"{self.server.baseurl}/schedules/{schedule_id}" m.get(baseurl, text=response_xml) schedule = self.server.schedules.get_by_id(schedule_id) self.assertIsNotNone(schedule) @@ -347,7 +347,7 @@ def test_update_after_get(self) -> None: def test_add_workbook(self) -> None: self.server.version = "2.8" - baseurl = "{}/sites/{}/schedules".format(self.server.baseurl, self.server.site_id) + baseurl = f"{self.server.baseurl}/sites/{self.server.site_id}/schedules" with open(WORKBOOK_GET_BY_ID_XML, "rb") as f: workbook_response = f.read().decode("utf-8") @@ -362,7 +362,7 @@ def test_add_workbook(self) -> None: def test_add_workbook_with_warnings(self) -> None: self.server.version = "2.8" - baseurl = "{}/sites/{}/schedules".format(self.server.baseurl, self.server.site_id) + baseurl = f"{self.server.baseurl}/sites/{self.server.site_id}/schedules" with open(WORKBOOK_GET_BY_ID_XML, "rb") as f: workbook_response = f.read().decode("utf-8") @@ -378,7 +378,7 @@ def test_add_workbook_with_warnings(self) -> None: def test_add_datasource(self) -> None: self.server.version = "2.8" - baseurl = "{}/sites/{}/schedules".format(self.server.baseurl, self.server.site_id) + baseurl = f"{self.server.baseurl}/sites/{self.server.site_id}/schedules" with open(DATASOURCE_GET_BY_ID_XML, "rb") as f: datasource_response = f.read().decode("utf-8") @@ -393,7 +393,7 @@ def test_add_datasource(self) -> None: def test_add_flow(self) -> None: self.server.version = "3.3" - baseurl = "{}/sites/{}/schedules".format(self.server.baseurl, self.server.site_id) + baseurl = f"{self.server.baseurl}/sites/{self.server.site_id}/schedules" with open(FLOW_GET_BY_ID_XML, "rb") as f: flow_response = f.read().decode("utf-8") diff --git a/test/test_site_model.py b/test/test_site_model.py index f62eb66f0..60ad9c5e5 100644 --- a/test/test_site_model.py +++ b/test/test_site_model.py @@ -1,5 +1,3 @@ -# coding=utf-8 - import unittest import tableauserverclient as TSC diff --git a/test/test_tagging.py b/test/test_tagging.py index 0184af415..23dffebfb 100644 --- a/test/test_tagging.py +++ b/test/test_tagging.py @@ -1,6 +1,6 @@ from contextlib import ExitStack import re -from typing import Iterable +from collections.abc import Iterable import uuid from xml.etree import ElementTree as ET @@ -172,7 +172,7 @@ def test_update_tags(get_server, endpoint_type, item, tags) -> None: if isinstance(item, str): stack.enter_context(pytest.raises((ValueError, NotImplementedError))) elif hasattr(item, "_initial_tags"): - initial_tags = set(["x", "y", "z"]) + initial_tags = {"x", "y", "z"} item._initial_tags = initial_tags add_tags_xml = add_tag_xml_response_factory(tags - initial_tags) delete_tags_xml = add_tag_xml_response_factory(initial_tags - tags) diff --git a/test/test_task.py b/test/test_task.py index 53da7c160..2d724b879 100644 --- a/test/test_task.py +++ b/test/test_task.py @@ -119,7 +119,7 @@ def test_get_materializeviews_tasks(self): with open(GET_XML_DATAACCELERATION_TASK, "rb") as f: response_xml = f.read().decode("utf-8") with requests_mock.mock() as m: - m.get("{}/{}".format(self.server.tasks.baseurl, TaskItem.Type.DataAcceleration), text=response_xml) + m.get(f"{self.server.tasks.baseurl}/{TaskItem.Type.DataAcceleration}", text=response_xml) all_tasks, pagination_item = self.server.tasks.get(task_type=TaskItem.Type.DataAcceleration) task = all_tasks[0] @@ -145,7 +145,7 @@ def test_get_by_id(self): response_xml = f.read().decode("utf-8") task_id = "f84901ac-72ad-4f9b-a87e-7a3500402ad6" with requests_mock.mock() as m: - m.get("{}/{}".format(self.baseurl, task_id), text=response_xml) + m.get(f"{self.baseurl}/{task_id}", text=response_xml) task = self.server.tasks.get_by_id(task_id) self.assertEqual("c7a9327e-1cda-4504-b026-ddb43b976d1d", task.target.id) @@ -159,7 +159,7 @@ def test_run_now(self): with open(GET_XML_RUN_NOW_RESPONSE, "rb") as f: response_xml = f.read().decode("utf-8") with requests_mock.mock() as m: - m.post("{}/{}/runNow".format(self.baseurl, task_id), text=response_xml) + m.post(f"{self.baseurl}/{task_id}/runNow", text=response_xml) job_response_content = self.server.tasks.run(task).decode("utf-8") self.assertTrue("7b6b59a8-ac3c-4d1d-2e9e-0b5b4ba8a7b6" in job_response_content) @@ -181,7 +181,7 @@ def test_create_extract_task(self): with open(GET_XML_CREATE_TASK_RESPONSE, "rb") as f: response_xml = f.read().decode("utf-8") with requests_mock.mock() as m: - m.post("{}".format(self.baseurl), text=response_xml) + m.post(f"{self.baseurl}", text=response_xml) create_response_content = self.server.tasks.create(task).decode("utf-8") self.assertTrue("task_id" in create_response_content) diff --git a/test/test_user.py b/test/test_user.py index 1f5eba57f..a46624845 100644 --- a/test/test_user.py +++ b/test/test_user.py @@ -1,8 +1,5 @@ -import io import os import unittest -from typing import List -from unittest.mock import MagicMock import requests_mock @@ -163,7 +160,7 @@ def test_populate_workbooks(self) -> None: self.assertEqual("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", workbook_list[0].project_id) self.assertEqual("default", workbook_list[0].project_name) self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", workbook_list[0].owner_id) - self.assertEqual(set(["Safari", "Sample"]), workbook_list[0].tags) + self.assertEqual({"Safari", "Sample"}, workbook_list[0].tags) def test_populate_workbooks_missing_id(self) -> None: single_user = TSC.UserItem("test", "Interactor") @@ -176,7 +173,7 @@ def test_populate_favorites(self) -> None: with open(GET_FAVORITES_XML, "rb") as f: response_xml = f.read().decode("utf-8") with requests_mock.mock() as m: - m.get("{0}/{1}".format(baseurl, single_user.id), text=response_xml) + m.get(f"{baseurl}/{single_user.id}", text=response_xml) self.server.users.populate_favorites(single_user) self.assertIsNotNone(single_user._favorites) self.assertEqual(len(single_user.favorites["workbooks"]), 1) diff --git a/test/test_user_model.py b/test/test_user_model.py index d0997b9ff..a8a2c51cb 100644 --- a/test/test_user_model.py +++ b/test/test_user_model.py @@ -1,7 +1,6 @@ import logging import unittest from unittest.mock import * -from typing import List import io import pytest @@ -107,7 +106,7 @@ def test_validate_user_detail_standard(self): TSC.UserItem.CSVImport.create_user_from_line(test_line) # for file handling - def _mock_file_content(self, content: List[str]) -> io.TextIOWrapper: + def _mock_file_content(self, content: list[str]) -> io.TextIOWrapper: # the empty string represents EOF # the tests run through the file twice, first to validate then to fetch mock = MagicMock(io.TextIOWrapper) @@ -119,10 +118,10 @@ def _mock_file_content(self, content: List[str]) -> io.TextIOWrapper: def test_validate_import_file(self): test_data = self._mock_file_content(UserDataTest.valid_import_content) valid, invalid = TSC.UserItem.CSVImport.validate_file_for_import(test_data, UserDataTest.logger) - assert valid == 2, "Expected two lines to be parsed, got {}".format(valid) - assert invalid == [], "Expected no failures, got {}".format(invalid) + assert valid == 2, f"Expected two lines to be parsed, got {valid}" + assert invalid == [], f"Expected no failures, got {invalid}" def test_validate_usernames_file(self): test_data = self._mock_file_content(UserDataTest.usernames) valid, invalid = TSC.UserItem.CSVImport.validate_file_for_import(test_data, UserDataTest.logger) - assert valid == 5, "Exactly 5 of the lines were valid, counted {}".format(valid + invalid) + assert valid == 5, f"Exactly 5 of the lines were valid, counted {valid + invalid}" diff --git a/test/test_view.py b/test/test_view.py index 1c667a4c3..a89a6d235 100644 --- a/test/test_view.py +++ b/test/test_view.py @@ -49,7 +49,7 @@ def test_get(self) -> None: self.assertEqual("3cc6cd06-89ce-4fdc-b935-5294135d6d42", all_views[0].workbook_id) self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", all_views[0].owner_id) self.assertEqual("5241e88d-d384-4fd7-9c2f-648b5247efc5", all_views[0].project_id) - self.assertEqual(set(["tag1", "tag2"]), all_views[0].tags) + self.assertEqual({"tag1", "tag2"}, all_views[0].tags) self.assertIsNone(all_views[0].created_at) self.assertIsNone(all_views[0].updated_at) self.assertIsNone(all_views[0].sheet_type) @@ -77,7 +77,7 @@ def test_get_by_id(self) -> None: self.assertEqual("3cc6cd06-89ce-4fdc-b935-5294135d6d42", view.workbook_id) self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", view.owner_id) self.assertEqual("5241e88d-d384-4fd7-9c2f-648b5247efc5", view.project_id) - self.assertEqual(set(["tag1", "tag2"]), view.tags) + self.assertEqual({"tag1", "tag2"}, view.tags) self.assertEqual("2002-05-30T09:00:00Z", format_datetime(view.created_at)) self.assertEqual("2002-06-05T08:00:59Z", format_datetime(view.updated_at)) self.assertEqual("story", view.sheet_type) @@ -95,7 +95,7 @@ def test_get_by_id_usage(self) -> None: self.assertEqual("3cc6cd06-89ce-4fdc-b935-5294135d6d42", view.workbook_id) self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", view.owner_id) self.assertEqual("5241e88d-d384-4fd7-9c2f-648b5247efc5", view.project_id) - self.assertEqual(set(["tag1", "tag2"]), view.tags) + self.assertEqual({"tag1", "tag2"}, view.tags) self.assertEqual("2002-05-30T09:00:00Z", format_datetime(view.created_at)) self.assertEqual("2002-06-05T08:00:59Z", format_datetime(view.updated_at)) self.assertEqual("story", view.sheet_type) diff --git a/test/test_view_acceleration.py b/test/test_view_acceleration.py index 6f94f0c10..766831b0a 100644 --- a/test/test_view_acceleration.py +++ b/test/test_view_acceleration.py @@ -42,7 +42,7 @@ def test_get_by_id(self) -> None: self.assertEqual("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", single_workbook.project_id) self.assertEqual("default", single_workbook.project_name) self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", single_workbook.owner_id) - self.assertEqual(set(["Safari", "Sample"]), single_workbook.tags) + self.assertEqual({"Safari", "Sample"}, single_workbook.tags) self.assertEqual("d79634e1-6063-4ec9-95ff-50acbf609ff5", single_workbook.views[0].id) self.assertEqual("ENDANGERED SAFARI", single_workbook.views[0].name) self.assertEqual("SafariSample/sheets/ENDANGEREDSAFARI", single_workbook.views[0].content_url) diff --git a/test/test_workbook.py b/test/test_workbook.py index 950118dc0..1a6b3192f 100644 --- a/test/test_workbook.py +++ b/test/test_workbook.py @@ -83,7 +83,7 @@ def test_get(self) -> None: self.assertEqual("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", all_workbooks[1].project_id) self.assertEqual("default", all_workbooks[1].project_name) self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", all_workbooks[1].owner_id) - self.assertEqual(set(["Safari", "Sample"]), all_workbooks[1].tags) + self.assertEqual({"Safari", "Sample"}, all_workbooks[1].tags) def test_get_ignore_invalid_date(self) -> None: with open(GET_INVALID_DATE_XML, "rb") as f: @@ -127,7 +127,7 @@ def test_get_by_id(self) -> None: self.assertEqual("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", single_workbook.project_id) self.assertEqual("default", single_workbook.project_name) self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", single_workbook.owner_id) - self.assertEqual(set(["Safari", "Sample"]), single_workbook.tags) + self.assertEqual({"Safari", "Sample"}, single_workbook.tags) self.assertEqual("d79634e1-6063-4ec9-95ff-50acbf609ff5", single_workbook.views[0].id) self.assertEqual("ENDANGERED SAFARI", single_workbook.views[0].name) self.assertEqual("SafariSample/sheets/ENDANGEREDSAFARI", single_workbook.views[0].content_url) @@ -152,7 +152,7 @@ def test_get_by_id_personal(self) -> None: self.assertTrue(single_workbook.project_id) self.assertIsNone(single_workbook.project_name) self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", single_workbook.owner_id) - self.assertEqual(set(["Safari", "Sample"]), single_workbook.tags) + self.assertEqual({"Safari", "Sample"}, single_workbook.tags) self.assertEqual("d79634e1-6063-4ec9-95ff-50acbf609ff5", single_workbook.views[0].id) self.assertEqual("ENDANGERED SAFARI", single_workbook.views[0].name) self.assertEqual("SafariSample/sheets/ENDANGEREDSAFARI", single_workbook.views[0].content_url) @@ -277,7 +277,7 @@ def test_download_object(self) -> None: def test_download_sanitizes_name(self) -> None: filename = "Name,With,Commas.twbx" - disposition = 'name="tableau_workbook"; filename="{}"'.format(filename) + disposition = f'name="tableau_workbook"; filename="{filename}"' with requests_mock.mock() as m: m.get( self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/content", @@ -817,7 +817,7 @@ def test_revisions(self) -> None: with open(REVISION_XML, "rb") as f: response_xml = f.read().decode("utf-8") with requests_mock.mock() as m: - m.get("{0}/{1}/revisions".format(self.baseurl, workbook.id), text=response_xml) + m.get(f"{self.baseurl}/{workbook.id}/revisions", text=response_xml) self.server.workbooks.populate_revisions(workbook) revisions = workbook.revisions @@ -846,7 +846,7 @@ def test_delete_revision(self) -> None: workbook._id = "06b944d2-959d-4604-9305-12323c95e70e" with requests_mock.mock() as m: - m.delete("{0}/{1}/revisions/3".format(self.baseurl, workbook.id)) + m.delete(f"{self.baseurl}/{workbook.id}/revisions/3") self.server.workbooks.delete_revision(workbook.id, "3") def test_download_revision(self) -> None: diff --git a/versioneer.py b/versioneer.py index 86c240e13..cce899f58 100644 --- a/versioneer.py +++ b/versioneer.py @@ -276,7 +276,6 @@ """ -from __future__ import print_function try: import configparser @@ -328,7 +327,7 @@ def get_root(): me_dir = os.path.normcase(os.path.splitext(me)[0]) vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) if me_dir != vsr_dir: - print("Warning: build in %s is using versioneer.py from %s" % (os.path.dirname(me), versioneer_py)) + print(f"Warning: build in {os.path.dirname(me)} is using versioneer.py from {versioneer_py}") except NameError: pass return root @@ -342,7 +341,7 @@ def get_config_from_root(root): # the top of versioneer.py for instructions on writing your setup.cfg . setup_cfg = os.path.join(root, "setup.cfg") parser = configparser.SafeConfigParser() - with open(setup_cfg, "r") as f: + with open(setup_cfg) as f: parser.readfp(f) VCS = parser.get("versioneer", "VCS") # mandatory @@ -398,7 +397,7 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env= [c] + args, cwd=cwd, env=env, stdout=subprocess.PIPE, stderr=(subprocess.PIPE if hide_stderr else None) ) break - except EnvironmentError: + except OSError: e = sys.exc_info()[1] if e.errno == errno.ENOENT: continue @@ -408,7 +407,7 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env= return None, None else: if verbose: - print("unable to find command, tried %s" % (commands,)) + print(f"unable to find command, tried {commands}" return None, None stdout = p.communicate()[0].strip() if sys.version_info[0] >= 3: @@ -423,7 +422,7 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env= LONG_VERSION_PY[ "git" -] = ''' +] = r''' # This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag # feature). Distribution tarballs (built by setup.py sdist) and build @@ -955,7 +954,7 @@ def git_get_keywords(versionfile_abs): # _version.py. keywords = {} try: - f = open(versionfile_abs, "r") + f = open(versionfile_abs) for line in f.readlines(): if line.strip().startswith("git_refnames ="): mo = re.search(r'=\s*"(.*)"', line) @@ -970,7 +969,7 @@ def git_get_keywords(versionfile_abs): if mo: keywords["date"] = mo.group(1) f.close() - except EnvironmentError: + except OSError: pass return keywords @@ -994,11 +993,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): if verbose: print("keywords are unexpanded, not using") raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) + refs = {r.strip() for r in refnames.strip("()").split(",")} # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " - tags = set([r[len(TAG) :] for r in refs if r.startswith(TAG)]) + tags = {r[len(TAG) :] for r in refs if r.startswith(TAG)} if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %d @@ -1007,7 +1006,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r"\d", r)]) + tags = {r for r in refs if re.search(r"\d", r)} if verbose: print("discarding '%s', no digits" % ",".join(refs - tags)) if verbose: @@ -1100,7 +1099,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): if verbose: fmt = "tag '%s' doesn't start with prefix '%s'" print(fmt % (full_tag, tag_prefix)) - pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % (full_tag, tag_prefix) + pieces["error"] = f"tag '{full_tag}' doesn't start with prefix '{tag_prefix}'" return pieces pieces["closest-tag"] = full_tag[len(tag_prefix) :] @@ -1145,13 +1144,13 @@ def do_vcs_install(manifest_in, versionfile_source, ipy): files.append(versioneer_file) present = False try: - f = open(".gitattributes", "r") + f = open(".gitattributes") for line in f.readlines(): if line.strip().startswith(versionfile_source): if "export-subst" in line.strip().split()[1:]: present = True f.close() - except EnvironmentError: + except OSError: pass if not present: f = open(".gitattributes", "a+") @@ -1185,7 +1184,7 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): root = os.path.dirname(root) # up a level if verbose: - print("Tried directories %s but none started with prefix %s" % (str(rootdirs), parentdir_prefix)) + print(f"Tried directories {rootdirs!s} but none started with prefix {parentdir_prefix}") raise NotThisMethod("rootdir doesn't start with parentdir_prefix") @@ -1212,7 +1211,7 @@ def versions_from_file(filename): try: with open(filename) as f: contents = f.read() - except EnvironmentError: + except OSError: raise NotThisMethod("unable to read _version.py") mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", contents, re.M | re.S) if not mo: @@ -1229,7 +1228,7 @@ def write_to_version_file(filename, versions): with open(filename, "w") as f: f.write(SHORT_VERSION_PY % contents) - print("set %s to '%s'" % (filename, versions["version"])) + print(f"set {filename} to '{versions['version']}'") def plus_or_dot(pieces): @@ -1452,7 +1451,7 @@ def get_versions(verbose=False): try: ver = versions_from_file(versionfile_abs) if verbose: - print("got version from file %s %s" % (versionfile_abs, ver)) + print(f"got version from file {versionfile_abs} {ver}") return ver except NotThisMethod: pass @@ -1723,7 +1722,7 @@ def do_setup(): root = get_root() try: cfg = get_config_from_root(root) - except (EnvironmentError, configparser.NoSectionError, configparser.NoOptionError) as e: + except (OSError, configparser.NoSectionError, configparser.NoOptionError) as e: if isinstance(e, (EnvironmentError, configparser.NoSectionError)): print("Adding sample versioneer config to setup.cfg", file=sys.stderr) with open(os.path.join(root, "setup.cfg"), "a") as f: @@ -1748,9 +1747,9 @@ def do_setup(): ipy = os.path.join(os.path.dirname(cfg.versionfile_source), "__init__.py") if os.path.exists(ipy): try: - with open(ipy, "r") as f: + with open(ipy) as f: old = f.read() - except EnvironmentError: + except OSError: old = "" if INIT_PY_SNIPPET not in old: print(" appending to %s" % ipy) @@ -1769,12 +1768,12 @@ def do_setup(): manifest_in = os.path.join(root, "MANIFEST.in") simple_includes = set() try: - with open(manifest_in, "r") as f: + with open(manifest_in) as f: for line in f: if line.startswith("include "): for include in line.split()[1:]: simple_includes.add(include) - except EnvironmentError: + except OSError: pass # That doesn't cover everything MANIFEST.in can do # (http://docs.python.org/2/distutils/sourcedist.html#commands), so @@ -1805,7 +1804,7 @@ def scan_setup_py(): found = set() setters = False errors = 0 - with open("setup.py", "r") as f: + with open("setup.py") as f: for line in f.readlines(): if "import versioneer" in line: found.add("import")