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