diff --git a/.github/workflows/meta-checks.yml b/.github/workflows/meta-checks.yml
index 0e2b425ee..41a944e63 100644
--- a/.github/workflows/meta-checks.yml
+++ b/.github/workflows/meta-checks.yml
@@ -13,20 +13,6 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- - name: Get pip cache dir
- id: pip-cache
- shell: bash
- run: |
- echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
-
- - name: cache
- uses: actions/cache@v4
- with:
- path: ${{ steps.pip-cache.outputs.dir }}
- key: ${{ runner.os }}-${{ matrix.python-version }}-pip-${{ hashFiles('pyproject.toml') }}
- restore-keys: |
- ${{ runner.os }}-${{ matrix.python-version }}-pip-
-
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }}
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index 2e197cf20..d70539582 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -13,25 +13,11 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
- python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
+ python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
runs-on: ${{ matrix.os }}
steps:
- - name: Get pip cache dir
- id: pip-cache
- shell: bash
- run: |
- echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
-
- - name: cache
- uses: actions/cache@v4
- with:
- path: ${{ steps.pip-cache.outputs.dir }}
- key: ${{ runner.os }}-${{ matrix.python-version }}-pip-${{ hashFiles('pyproject.toml') }}
- restore-keys: |
- ${{ runner.os }}-${{ matrix.python-version }}-pip-
-
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }}
diff --git a/pyproject.toml b/pyproject.toml
index 08f90c49c..3bf47ea23 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -14,42 +14,42 @@ readme = "README.md"
dependencies = [
'defusedxml>=0.7.1', # latest as at 7/31/23
'packaging>=23.1', # latest as at 7/31/23
- 'requests>=2.32', # latest as at 7/31/23
- 'urllib3>=2.2.2,<3',
+ 'requests>=2.31', # latest as at 7/31/23
+ 'urllib3==2.2.2', # dependabot
'typing_extensions>=4.0.1',
]
-requires-python = ">=3.9"
+requires-python = ">=3.7"
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.13"
+ "Programming Language :: Python :: 3.12"
]
[project.urls]
repository = "https://github.com/tableau/server-client-python"
[project.optional-dependencies]
-test = ["black==24.8", "build", "mypy==1.4", "pytest>=7.0", "pytest-cov", "pytest-subtests",
+test = ["black==23.7", "build", "mypy==1.4", "pytest>=7.0", "pytest-cov", "pytest-subtests",
"requests-mock>=1.0,<2.0"]
[tool.black]
line-length = 120
-target-version = ['py39', 'py310', 'py311', 'py312', 'py313']
+target-version = ['py37', 'py38', 'py39', 'py310', 'py311', 'py312']
[tool.mypy]
check_untyped_defs = false
disable_error_code = [
'misc',
+ # 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", "samples"]
+files = ["tableauserverclient", "test"]
show_error_codes = true
ignore_missing_imports = true # defusedxml library has no types
no_implicit_reexport = true
-implicit_optional = true
[tool.pytest.ini_options]
testpaths = ["test"]
diff --git a/samples/add_default_permission.py b/samples/add_default_permission.py
index d26d009e2..5a450e8ab 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(f"\nCapabilities for {grantee.tag_name} {grantee.id}:")
+ print("\nCapabilities for {0} {1}:".format(grantee.tag_name, grantee.id))
for capability in capabilities:
- print(f"\t{capability} - {capabilities[capability]}")
+ print("\t{0} - {1}".format(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 aca3e895b..f4c6a9ca9 100644
--- a/samples/create_group.py
+++ b/samples/create_group.py
@@ -11,6 +11,7 @@
import os
from datetime import time
+from typing import List
import tableauserverclient as TSC
from tableauserverclient import ServerResponseError
@@ -62,23 +63,23 @@ def main():
if args.file:
filepath = os.path.abspath(args.file)
- print(f"Add users to site from file {filepath}:")
- added: list[TSC.UserItem]
- failed: list[TSC.UserItem, TSC.ServerResponseError]
+ print("Add users to site from file {}:".format(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(f"Adding users to group:{added}")
+ print("Adding users to group:{}".format(added))
for user in added:
- print(f"Adding user {user}")
+ print("Adding user {}".format(user))
try:
server.groups.add_user(group, user.id)
except ServerResponseError as serverError:
if serverError.code == "409011":
- print(f"user {user.name} is already a member of group {group.name}")
+ print("user {} is already a member of group {}".format(user.name, group.name))
else:
raise rError
diff --git a/samples/create_project.py b/samples/create_project.py
index d775902aa..1fc649f8c 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(f"Permissions from project {changed_project.id}:")
+ print("Permissions from project {}:".format(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 c23a2eced..dee088571 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(f"Hourly schedule created (ID: {hourly_schedule.id}).")
+ print("Hourly schedule created (ID: {}).".format(hourly_schedule.id))
except Exception as e:
print(e)
@@ -71,7 +71,7 @@ def main():
)
try:
daily_schedule = server.schedules.create(daily_schedule)
- print(f"Daily schedule created (ID: {daily_schedule.id}).")
+ print("Daily schedule created (ID: {}).".format(daily_schedule.id))
except Exception as e:
print(e)
@@ -89,7 +89,7 @@ def main():
)
try:
weekly_schedule = server.schedules.create(weekly_schedule)
- print(f"Weekly schedule created (ID: {weekly_schedule.id}).")
+ print("Weekly schedule created (ID: {}).".format(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(f"Monthly schedule created (ID: {monthly_schedule.id}).")
+ print("Monthly schedule created (ID: {}).".format(monthly_schedule.id))
except Exception as e:
print(e)
diff --git a/samples/explore_datasource.py b/samples/explore_datasource.py
index c9f35d5be..fb45cb45e 100644
--- a/samples/explore_datasource.py
+++ b/samples/explore_datasource.py
@@ -51,17 +51,16 @@ def main():
if args.publish:
if default_project is not None:
new_datasource = TSC.DatasourceItem(default_project.id)
- new_datasource.description = "Published with a description"
new_datasource = server.datasources.publish(
new_datasource, args.publish, TSC.Server.PublishMode.Overwrite
)
- print(f"Datasource published. ID: {new_datasource.id}")
+ print("Datasource published. ID: {}".format(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(f"\nThere are {pagination_item.total_available} datasources on site: ")
+ print("\nThere are {} datasources on site: ".format(pagination_item.total_available))
print([datasource.name for datasource in all_datasources])
if all_datasources:
@@ -70,19 +69,20 @@ def main():
# Populate connections
server.datasources.populate_connections(sample_datasource)
- print(f"\nConnections for {sample_datasource.name}: ")
- print([f"{connection.id}({connection.datasource_name})" for connection in sample_datasource.connections])
-
- # Demonstrate that description is editable
- sample_datasource.description = "Description updated by TSC"
- server.datasources.update(sample_datasource)
+ print("\nConnections for {}: ".format(sample_datasource.name))
+ print(
+ [
+ "{0}({1})".format(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(f"\nOld tag set: {original_tag_set}")
- print(f"New tag set: {sample_datasource.tags}")
+ print("\nOld tag set: {}".format(original_tag_set))
+ print("New tag set: {}".format(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 f199522ed..243e91954 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.models import Resource
+from tableauserverclient 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(f"Favorites for user: {user.id}")
+ print("Favorites for user: {}".format(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 = all_workbook_items[0]
- server.favorites.add_favorite(user, Resource.Workbook, 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])
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(f"View added to favorites. View Name: {my_view.name}, View ID: {my_view.id}")
+ print("View added to favorites. View Name: {}, View ID: {}".format(my_view.name, my_view.id))
all_datasource_items, pagination_item = server.datasources.get()
if all_datasource_items:
@@ -70,10 +70,12 @@ def main():
)
server.favorites.delete_favorite_workbook(user, my_workbook)
- print(f"Workbook deleted from favorites. Workbook Name: {my_workbook.name}, Workbook ID: {my_workbook.id}")
+ print(
+ "Workbook deleted from favorites. Workbook Name: {}, Workbook ID: {}".format(my_workbook.name, my_workbook.id)
+ )
server.favorites.delete_favorite_view(user, my_view)
- print(f"View deleted from favorites. View Name: {my_view.name}, View ID: {my_view.id}")
+ print("View deleted from favorites. View Name: {}, View ID: {}".format(my_view.name, 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 eb9eba0de..a2274f1a7 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(f"Delete site `{current_site.name}`?")
+ print("Delete site `{}`?".format(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 f25c41849..77802b1db 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(f"Webhook created. ID: {new_webhook.id}")
+ print("Webhook created. ID: {}".format(new_webhook.id))
# Gets all webhook items
all_webhooks, pagination_item = server.webhooks.get()
- print(f"\nThere are {pagination_item.total_available} webhooks on site: ")
+ print("\nThere are {} webhooks on site: ".format(pagination_item.total_available))
print([webhook.name for webhook in all_webhooks])
if all_webhooks:
diff --git a/samples/explore_workbook.py b/samples/explore_workbook.py
index f51639ab3..57f88aa07 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(f"Workbook published. ID: {new_workbook.id}")
+ print("Workbook published. ID: {}".format(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(f"\nThere are {pagination_item.total_available} workbooks on site: ")
+ print("\nThere are {} workbooks on site: ".format(pagination_item.total_available))
print([workbook.name for workbook in all_workbooks])
if all_workbooks:
@@ -78,22 +78,27 @@ def main():
# Populate views
server.workbooks.populate_views(sample_workbook)
- print(f"\nName of views in {sample_workbook.name}: ")
+ print("\nName of views in {}: ".format(sample_workbook.name))
print([view.name for view in sample_workbook.views])
# Populate connections
server.workbooks.populate_connections(sample_workbook)
- print(f"\nConnections for {sample_workbook.name}: ")
- print([f"{connection.id}({connection.datasource_name})" for connection in sample_workbook.connections])
+ print("\nConnections for {}: ".format(sample_workbook.name))
+ print(
+ [
+ "{0}({1})".format(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(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}")
+ 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))
# Delete all tags that were added by setting tags to original
sample_workbook.tags = original_tag_set
@@ -104,8 +109,8 @@ def main():
original_tag_set = set(sample_view.tags)
sample_view.tags.add("view_tag")
server.views.update(sample_view)
- print(f"\nView's old tag set: {original_tag_set}")
- print(f"View's new tag set: {sample_view.tags}")
+ print("\nView's old tag set: {}".format(original_tag_set))
+ print("View's new tag set: {}".format(sample_view.tags))
# Delete tag from just one view
sample_view.tags = original_tag_set
@@ -114,14 +119,14 @@ def main():
if args.download:
# Download
path = server.workbooks.download(sample_workbook.id, args.download)
- print(f"\nDownloaded workbook to {path}")
+ print("\nDownloaded workbook to {}".format(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(f"\nDownloaded preview image of workbook to {os.path.abspath(args.preview_image)}")
+ print("\nDownloaded preview image of workbook to {}".format(os.path.abspath(args.preview_image)))
# get custom views
cvs, _ = server.custom_views.get()
@@ -148,10 +153,10 @@ def main():
server.workbooks.populate_powerpoint(sample_workbook)
with open(args.powerpoint, "wb") as f:
f.write(sample_workbook.powerpoint)
- print(f"\nDownloaded powerpoint of workbook to {os.path.abspath(args.powerpoint)}")
+ print("\nDownloaded powerpoint of workbook to {}".format(os.path.abspath(args.powerpoint)))
if args.delete:
- print(f"deleting {c.id}")
+ print("deleting {}".format(c.id))
unlucky = TSC.CustomViewItem(c.id)
server.custom_views.delete(unlucky.id)
diff --git a/samples/export.py b/samples/export.py
index b2506cf46..f2783fa6e 100644
--- a/samples/export.py
+++ b/samples/export.py
@@ -37,11 +37,8 @@ def main():
"--csv", dest="type", action="store_const", const=("populate_csv", "CSVRequestOptions", "csv", "csv")
)
# other options shown in explore_workbooks: workbook.download, workbook.preview_image
- parser.add_argument(
- "--language", help="Text such as 'Average' will appear in this language. Use values like fr, de, es, en"
- )
+
parser.add_argument("--workbook", action="store_true")
- parser.add_argument("--custom_view", action="store_true")
parser.add_argument("--file", "-f", help="filename to store the exported data")
parser.add_argument("--filter", "-vf", metavar="COLUMN:VALUE", help="View filter to apply to the view")
@@ -59,16 +56,14 @@ def main():
print("Connected")
if args.workbook:
item = server.workbooks.get_by_id(args.resource_id)
- elif args.custom_view:
- item = server.custom_views.get_by_id(args.resource_id)
else:
item = server.views.get_by_id(args.resource_id)
if not item:
- print(f"No item found for id {args.resource_id}")
+ print("No item found for id {}".format(args.resource_id))
exit(1)
- print(f"Item found: {item.name}")
+ print("Item found: {}".format(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.
@@ -77,22 +72,18 @@ def main():
populate = getattr(server.views, populate_func_name)
if args.workbook:
populate = getattr(server.workbooks, populate_func_name)
- elif args.custom_view:
- populate = getattr(server.custom_views, populate_func_name)
option_factory = getattr(TSC, option_factory_name)
- options: TSC.PDFRequestOptions = option_factory()
if args.filter:
- options = options.vf(*args.filter.split(":"))
-
- if args.language:
- options.language = args.language
+ options = option_factory().vf(*args.filter.split(":"))
+ else:
+ options = None
if args.file:
filename = args.file
else:
- filename = f"out-{options.language}.{extension}"
+ filename = "out.{}".format(extension)
populate(item, options)
with open(filename, "wb") as f:
diff --git a/samples/extracts.py b/samples/extracts.py
index c0dd885bc..9bd87a473 100644
--- a/samples/extracts.py
+++ b/samples/extracts.py
@@ -1,7 +1,13 @@
####
-# This script demonstrates how to use the Tableau Server Client to interact with extracts.
-# It explores the different functions that the REST API supports on extracts.
-#####
+# This script demonstrates how to use the Tableau Server Client
+# to interact with workbooks. It explores the different
+# functions that the Server API supports on workbooks.
+#
+# With no flags set, this sample will query all workbooks,
+# pick one workbook and populate its connections/views, and update
+# the workbook. Adding flags will demonstrate the specific feature
+# on top of the general operations.
+####
import argparse
import logging
@@ -41,7 +47,7 @@ def main():
with server.auth.sign_in(tableau_auth):
# Gets all workbook items
all_workbooks, pagination_item = server.workbooks.get()
- print(f"\nThere are {pagination_item.total_available} workbooks on site: ")
+ print("\nThere are {} workbooks on site: ".format(pagination_item.total_available))
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 1694bf0f5..042af32e2 100644
--- a/samples/filter_sort_groups.py
+++ b/samples/filter_sort_groups.py
@@ -47,7 +47,7 @@ def main():
logging.basicConfig(level=logging_level)
tableau_auth = TSC.PersonalAccessTokenAuth(args.token_name, args.token_value, site_id=args.site)
- server = TSC.Server(args.server, use_server_version=True, http_options={"verify": False})
+ server = TSC.Server(args.server, use_server_version=True)
with server.auth.sign_in(tableau_auth):
group_name = "SALES NORTHWEST"
# Try to create a group named "SALES NORTHWEST"
@@ -57,36 +57,37 @@ def main():
# Try to create a group named "SALES ROMANIA"
create_example_group(group_name, server)
- # we no longer need to encode the space
+ # URL Encode the name of the group that we want to filter on
+ # i.e. turn spaces into plus signs
+ filter_group_name = urllib.parse.quote_plus(group_name)
options = TSC.RequestOptions()
- options.filter.add(TSC.Filter(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Operator.Equals, group_name))
+ options.filter.add(
+ TSC.Filter(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Operator.Equals, filter_group_name)
+ )
filtered_groups, _ = server.groups.get(req_options=options)
# Result can either be a matching group or an empty list
if filtered_groups:
- group = filtered_groups.pop()
- print(group)
+ group_name = filtered_groups.pop().name
+ print(group_name)
else:
- error = f"No group named '{group_name}' found"
+ error = "No project named '{}' found".format(filter_group_name)
print(error)
- print("---")
-
# Or, try the above with the django style filtering
try:
- group = server.groups.filter(name=group_name)[0]
- print(group)
+ group = server.groups.filter(name=filter_group_name)[0]
except IndexError:
- print(f"No group named '{group_name}' found")
-
- print("====")
+ print(f"No project named '{filter_group_name}' found")
+ else:
+ print(group.name)
options = TSC.RequestOptions()
options.filter.add(
TSC.Filter(
TSC.RequestOptions.Field.Name,
TSC.RequestOptions.Operator.In,
- ["SALES NORTHWEST", "SALES ROMANIA", "this_group"],
+ ["SALES+NORTHWEST", "SALES+ROMANIA", "this_group"],
)
)
@@ -97,19 +98,12 @@ def main():
for group in matching_groups:
print(group.name)
- print("----")
# or, try the above with the django style filtering.
- all_g = server.groups.all()
- print(f"Searching locally among {all_g.total_available} groups")
- for a in all_g:
- print(a)
- groups = [urllib.parse.quote_plus(group) for group in ["SALES NORTHWEST", "SALES ROMANIA", "this_group"]]
- print(groups)
-
- for group in server.groups.filter(name__in=groups).order_by("-name"):
- print(group.name)
- print("done")
+ groups = ["SALES NORTHWEST", "SALES ROMANIA", "this_group"]
+ groups = [urllib.parse.quote_plus(group) for group in groups]
+ for group in server.groups.filter(name__in=groups).sort("-name"):
+ print(group.name)
if __name__ == "__main__":
diff --git a/samples/filter_sort_projects.py b/samples/filter_sort_projects.py
index 6c3a85dcd..7aa62a5c1 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 = f"No project named '{filter_project_name}' found"
+ error = "No project named '{}' found".format(filter_project_name)
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 5f8cfa238..454b225de 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(f"Connected to {server.server_info.baseurl}")
- print(f"Server information: {server.server_info}")
+ print("Connected to {}".format(server.server_info.baseurl))
+ print("Server information: {}".format(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 8635947a8..d62896059 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(f"Connected to {server.server_info.baseurl}")
+ print("Connected to {}".format(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(f"{pagination.total_available} projects")
+ print("{} projects".format(pagination.total_available))
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 a2c4301d0..21de97831 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(f"Connected to {server.server_info.baseurl}")
+ print("Connected to {}".format(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(f"{pagination.total_available} projects")
+ print("{} projects".format(pagination.total_available))
for project in projects:
print(project.name)
workbooks, pagination = server.datasources.get()
if workbooks:
- print(f"{pagination.total_available} workbooks")
+ print("{} workbooks".format(pagination.total_available))
print(workbooks[0])
views, pagination = server.views.get()
if views:
- print(f"{pagination.total_available} views")
+ print("{} views".format(pagination.total_available))
print(views[0])
datasources, pagination = server.datasources.get()
if datasources:
- print(f"{pagination.total_available} datasources")
+ print("{} datasources".format(pagination.total_available))
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(f"{pagination.total_available} jobs")
+ print("{} jobs".format(pagination.total_available))
print(jobs[0])
schedules, pagination = server.schedules.get()
if schedules:
- print(f"{pagination.total_available} schedules")
+ print("{} schedules".format(pagination.total_available))
print(schedules[0])
tasks, pagination = server.tasks.get()
if tasks:
- print(f"{pagination.total_available} tasks")
+ print("{} tasks".format(pagination.total_available))
print(tasks[0])
webhooks, pagination = server.webhooks.get()
if webhooks:
- print(f"{pagination.total_available} webhooks")
+ print("{} webhooks".format(pagination.total_available))
print(webhooks[0])
users, pagination = server.users.get()
if users:
- print(f"{pagination.total_available} users")
+ print("{} users".format(pagination.total_available))
print(users[0])
groups, pagination = server.groups.get()
if groups:
- print(f"{pagination.total_available} groups")
+ print("{} groups".format(pagination.total_available))
print(groups[0])
diff --git a/samples/initialize_server.py b/samples/initialize_server.py
index cdfaf27a8..cb3d9e1d0 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(f"Site not found: {args.site_id} Creating it...")
+ print("Site not found: {0} Creating it...".format(args.site_id))
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(f"Site {args.site_id} exists. Moving on...")
+ print("Site {0} exists. Moving on...".format(args.site_id))
################################################################################
# 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(f"Project not found: {args.project} Creating it...")
+ print("Project not found: {0} Creating it...".format(args.project))
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(f"Datasource published. ID: {new_ds.id}")
+ print("Datasource published. ID: {0}".format(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(f"Workbook published. ID: {new_workbook.id}")
+ print("Workbook published. ID: {0}".format(new_workbook.id))
if __name__ == "__main__":
diff --git a/samples/list.py b/samples/list.py
index 2675a2954..8d72fb620 100644
--- a/samples/list.py
+++ b/samples/list.py
@@ -48,9 +48,6 @@ 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))
@@ -62,7 +59,7 @@ def main():
print(resource.name[:18], " ") # , resource._connections())
if count > 100:
break
- print(f"Total: {count}")
+ print("Total: {}".format(count))
if __name__ == "__main__":
diff --git a/samples/login.py b/samples/login.py
index bc99385b3..6a3e9e8b3 100644
--- a/samples/login.py
+++ b/samples/login.py
@@ -7,15 +7,9 @@
import argparse
import getpass
import logging
-import os
import tableauserverclient as TSC
-
-
-def get_env(key):
- if key in os.environ:
- return os.environ[key]
- return None
+import env
# If a sample has additional arguments, then it should copy this code and insert them after the call to
@@ -26,13 +20,13 @@ def set_up_and_log_in():
sample_define_common_options(parser)
args = parser.parse_args()
if not args.server:
- args.server = get_env("SERVER")
+ args.server = env.server
if not args.site:
- args.site = get_env("SITE")
+ args.site = env.site
if not args.token_name:
- args.token_name = get_env("TOKEN_NAME")
+ args.token_name = env.token_name
if not args.token_value:
- args.token_value = get_env("TOKEN_VALUE")
+ args.token_value = env.token_value
args.logging_level = "debug"
server = sample_connect_to_server(args)
@@ -65,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(f"\nSigning in...\nServer: {args.server}\nSite: {args.site}\nUsername: {args.username}")
+ print("\nSigning in...\nServer: {}\nSite: {}\nUsername: {}".format(args.server, args.site, args.username))
else:
# Trying to authenticate using personal access tokens.
@@ -74,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(f"\nSigning in...\nServer: {args.server}\nSite: {args.site}\nToken name: {args.token_name}")
+ print("\nSigning in...\nServer: {}\nSite: {}\nToken name: {}".format(args.server, args.site, args.token_name))
if not tableau_auth:
raise TabError("Did not create authentication object. Check arguments.")
@@ -85,7 +79,10 @@ def sample_connect_to_server(args):
# Make sure we use an updated version of the rest apis, and pass in our cert handling choice
server = TSC.Server(args.server, use_server_version=True, http_options={"verify": check_ssl_certificate})
server.auth.sign_in(tableau_auth)
- server.version = "3.19"
+ server.version = "2.6"
+ new_site: TSC.SiteItem = TSC.SiteItem("cdnear", content_url=env.site)
+ server.auth.switch_site(new_site)
+ print("Logged in successfully")
return server
diff --git a/samples/move_workbook_sites.py b/samples/move_workbook_sites.py
index e82c75cf9..47af1f2f9 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(f"No workbook named {args.workbook_name} found.")
+ print("No workbook named {} found.".format(args.workbook_name))
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 = f"No site named {args.destination_site} found."
+ error = "No site named {} found.".format(args.destination_site)
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(f"Successfully moved {new_workbook.name} ({new_workbook.id})")
+ print("Successfully moved {0} ({1})".format(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 a68eed4b3..a7ae6dc89 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(f"Total: {count}\n")
+ print("Total: {}\n".format(count))
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(f"Truncated Total: {count}\n")
+ print("Truncated Total: {}\n".format(count))
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(f"Filtered Total: {count}\n")
+ print("Filtered Total: {}\n".format(count))
# 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(f"QuerySet Total: {count}")
+ print("QuerySet Total: {}".format(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 c674e6882..5ac768674 100644
--- a/samples/publish_datasource.py
+++ b/samples/publish_datasource.py
@@ -21,15 +21,10 @@
import argparse
import logging
-import os
import tableauserverclient as TSC
-import tableauserverclient.datetime_helpers
-
-def get_env(key):
- if key in os.environ:
- return os.environ[key]
- return None
+import env
+import tableauserverclient.datetime_helpers
def main():
@@ -57,13 +52,13 @@ def main():
args = parser.parse_args()
if not args.server:
- args.server = get_env("SERVER")
+ args.server = env.server
if not args.site:
- args.site = get_env("SITE")
+ args.site = env.site
if not args.token_name:
- args.token_name = get_env("TOKEN_NAME")
+ args.token_name = env.token_name
if not args.token_value:
- args.token_value = get_env("TOKEN_VALUE")
+ args.token_value = env.token_value
args.logging = "debug"
args.file = "C:/dev/tab-samples/5M.tdsx"
args.async_ = True
@@ -116,17 +111,15 @@ def main():
new_job = server.datasources.publish(
new_datasource, args.file, publish_mode, connection_credentials=new_conn_creds, as_job=True
)
- print(f"Datasource published asynchronously. Job ID: {new_job.id}")
+ print("Datasource published asynchronously. Job ID: {0}".format(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(
- (
- "{}Datasource published. Datasource ID: {}".format(
- new_datasource.id, tableauserverclient.datetime_helpers.timestamp()
- )
+ "{0}Datasource published. Datasource ID: {1}".format(
+ new_datasource.id, tableauserverclient.datetime_helpers.timestamp()
)
)
print("\t\tClosing connection")
diff --git a/samples/publish_workbook.py b/samples/publish_workbook.py
index d31978c0f..8a9f45279 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(f"Workbook published. JOB ID: {new_job.id}")
+ print("Workbook published. JOB ID: {0}".format(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(f"Workbook published. ID: {new_workbook.id}")
+ print("Workbook published. ID: {0}".format(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 3309acd90..4e509cd97 100644
--- a/samples/query_permissions.py
+++ b/samples/query_permissions.py
@@ -57,15 +57,17 @@ def main():
permissions = resource.permissions
# Print result
- print(f"\n{len(permissions)} permission rule(s) found for {args.resource_type} {args.resource_id}.")
+ print(
+ "\n{0} permission rule(s) found for {1} {2}.".format(len(permissions), args.resource_type, args.resource_id)
+ )
for permission in permissions:
grantee = permission.grantee
capabilities = permission.capabilities
- print(f"\nCapabilities for {grantee.tag_name} {grantee.id}:")
+ print("\nCapabilities for {0} {1}:".format(grantee.tag_name, grantee.id))
for capability in capabilities:
- print(f"\t{capability} - {capabilities[capability]}")
+ print("\t{0} - {1}".format(capability, capabilities[capability]))
if __name__ == "__main__":
diff --git a/samples/refresh_tasks.py b/samples/refresh_tasks.py
index c95000898..03daedf16 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(f"{task}")
+ print("{}".format(task))
def handle_info(server, args):
task = server.tasks.get_by_id(args.id)
- print(f"{task}")
+ print("{}".format(task))
def main():
diff --git a/samples/set_refresh_schedule.py b/samples/set_refresh_schedule.py
index 153bb0ee5..56fd12e62 100644
--- a/samples/set_refresh_schedule.py
+++ b/samples/set_refresh_schedule.py
@@ -38,7 +38,7 @@ def usage(args):
def make_filter(**kwargs):
options = TSC.RequestOptions()
- for item, value in list(kwargs.items()):
+ for item, value in kwargs.items():
name = getattr(TSC.RequestOptions.Field, item)
options.filter.add(TSC.Filter(name, TSC.RequestOptions.Operator.Equals, value))
return options
diff --git a/samples/update_connection.py b/samples/update_connection.py
index 0fe2f342c..4af6592bc 100644
--- a/samples/update_connection.py
+++ b/samples/update_connection.py
@@ -45,7 +45,7 @@ def main():
update_function = endpoint.update_connection
resource = endpoint.get_by_id(args.resource_id)
endpoint.populate_connections(resource)
- connections = list([x for x in resource.connections if x.id == args.connection_id])
+ connections = list(filter(lambda x: x.id == args.connection_id, resource.connections))
assert len(connections) == 1
connection = connections[0]
connection.username = args.datasource_username
diff --git a/samples/update_workbook_data_acceleration.py b/samples/update_workbook_data_acceleration.py
new file mode 100644
index 000000000..75f12262f
--- /dev/null
+++ b/samples/update_workbook_data_acceleration.py
@@ -0,0 +1,109 @@
+####
+# This script demonstrates how to update workbook data acceleration using the Tableau
+# Server Client.
+#
+# To run the script, you must have installed Python 3.7 or later.
+####
+
+
+import argparse
+import logging
+
+import tableauserverclient as TSC
+from tableauserverclient import IntervalItem
+
+
+def main():
+ parser = argparse.ArgumentParser(description="Creates sample schedules for each type of frequency.")
+ # Common options; please keep those in sync across all samples
+ parser.add_argument("--server", "-s", help="server address")
+ parser.add_argument("--site", "-S", help="site name")
+ parser.add_argument("--token-name", "-p", help="name of the personal access token used to sign into the server")
+ parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server")
+ parser.add_argument(
+ "--logging-level",
+ "-l",
+ choices=["debug", "info", "error"],
+ default="error",
+ help="desired logging level (set to error by default)",
+ )
+ # Options specific to this sample:
+ # This sample has no additional options, yet. If you add some, please add them here
+
+ args = parser.parse_args()
+
+ # Set logging level based on user input, or error by default
+ logging_level = getattr(logging, args.logging_level.upper())
+ logging.basicConfig(level=logging_level)
+
+ tableau_auth = TSC.PersonalAccessTokenAuth(args.token_name, args.token_value, site_id=args.site)
+ server = TSC.Server(args.server, use_server_version=False)
+ server.add_http_options({"verify": False})
+ server.use_server_version()
+ 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([workbook.name for workbook in all_workbooks])
+
+ if all_workbooks:
+ # Pick 1 workbook to try data acceleration.
+ # Note that data acceleration has a couple of requirements, please check the Tableau help page
+ # to verify your workbook/view is eligible for data acceleration.
+
+ # Assuming 1st workbook is eligible for sample purposes
+ sample_workbook = all_workbooks[2]
+
+ # Enable acceleration for all the views in the workbook
+ enable_config = dict()
+ enable_config["acceleration_enabled"] = True
+ enable_config["accelerate_now"] = True
+
+ sample_workbook.data_acceleration_config = enable_config
+ updated: TSC.WorkbookItem = server.workbooks.update(sample_workbook)
+ # Since we did not set any specific view, we will enable all views in the workbook
+ print("Enable acceleration for all the views in the workbook " + updated.name + ".")
+
+ # Disable acceleration on one of the view in the workbook
+ # You have to populate_views first, then set the views of the workbook
+ # to the ones you want to update.
+ server.workbooks.populate_views(sample_workbook)
+ view_to_disable = sample_workbook.views[0]
+ sample_workbook.views = [view_to_disable]
+
+ disable_config = dict()
+ disable_config["acceleration_enabled"] = False
+ disable_config["accelerate_now"] = True
+
+ sample_workbook.data_acceleration_config = disable_config
+ # To get the acceleration status on the response, set includeViewAccelerationStatus=true
+ # Note that you have to populate_views first to get the acceleration status, since
+ # acceleration status is per view basis (not per workbook)
+ updated: TSC.WorkbookItem = server.workbooks.update(sample_workbook, True)
+ view1 = updated.views[0]
+ print('Disabled acceleration for 1 view "' + view1.name + '" in the workbook ' + updated.name + ".")
+
+ # Get acceleration status of the views in workbook using workbooks.get_by_id
+ # This won't need to do populate_views beforehand
+ my_workbook = server.workbooks.get_by_id(sample_workbook.id)
+ view1 = my_workbook.views[0]
+ view2 = my_workbook.views[1]
+ print(
+ "Fetching acceleration status for views in the workbook "
+ + updated.name
+ + ".\n"
+ + 'View "'
+ + view1.name
+ + '" has acceleration_status = '
+ + view1.data_acceleration_config["acceleration_status"]
+ + ".\n"
+ + 'View "'
+ + view2.name
+ + '" has acceleration_status = '
+ + view2.data_acceleration_config["acceleration_status"]
+ + "."
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/samples/update_workbook_data_freshness_policy.py b/samples/update_workbook_data_freshness_policy.py
index c23e3717f..9e4d63dc1 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(f"\nThere are {pagination_item.total_available} workbooks on site: ")
+ print("\nThere are {} workbooks on site: ".format(pagination_item.total_available))
print([workbook.name for workbook in all_workbooks])
if all_workbooks:
diff --git a/tableauserverclient/__init__.py b/tableauserverclient/__init__.py
index e0a7abb64..bab2cf05f 100644
--- a/tableauserverclient/__init__.py
+++ b/tableauserverclient/__init__.py
@@ -32,13 +32,11 @@
PermissionsRule,
PersonalAccessTokenAuth,
ProjectItem,
- Resource,
RevisionItem,
ScheduleItem,
SiteItem,
ServerInfoItem,
SubscriptionItem,
- TableauItem,
TableItem,
TableauAuth,
Target,
@@ -58,7 +56,6 @@
PDFRequestOptions,
RequestOptions,
MissingRequiredFieldError,
- FailedSignInError,
NotSignedInError,
ServerResponseError,
Filter,
@@ -68,68 +65,65 @@
)
__all__ = [
+ "get_versions",
+ "DEFAULT_NAMESPACE",
"BackgroundJobItem",
"BackgroundJobItem",
"ColumnItem",
"ConnectionCredentials",
"ConnectionItem",
- "CSVRequestOptions",
"CustomViewItem",
+ "DQWItem",
"DailyInterval",
"DataAlertItem",
"DatabaseItem",
"DataFreshnessPolicyItem",
"DatasourceItem",
- "DEFAULT_NAMESPACE",
- "DQWItem",
- "ExcelRequestOptions",
- "FailedSignInError",
"FavoriteItem",
- "FileuploadItem",
- "Filter",
"FlowItem",
"FlowRunItem",
- "get_versions",
+ "FileuploadItem",
"GroupItem",
"GroupSetItem",
"HourlyInterval",
- "ImageRequestOptions",
"IntervalItem",
"JobItem",
"JWTAuth",
- "LinkedTaskFlowRunItem",
- "LinkedTaskItem",
- "LinkedTaskStepItem",
"MetricItem",
- "MissingRequiredFieldError",
"MonthlyInterval",
- "NotSignedInError",
- "Pager",
"PaginationItem",
- "PDFRequestOptions",
"Permission",
"PermissionsRule",
"PersonalAccessTokenAuth",
"ProjectItem",
- "RequestOptions",
- "Resource",
"RevisionItem",
"ScheduleItem",
- "Server",
- "ServerInfoItem",
- "ServerResponseError",
"SiteItem",
- "Sort",
+ "ServerInfoItem",
"SubscriptionItem",
- "TableauAuth",
- "TableauItem",
"TableItem",
+ "TableauAuth",
"Target",
"TaskItem",
"UserItem",
"ViewItem",
- "VirtualConnectionItem",
"WebhookItem",
"WeeklyInterval",
"WorkbookItem",
+ "CSVRequestOptions",
+ "ExcelRequestOptions",
+ "ImageRequestOptions",
+ "PDFRequestOptions",
+ "RequestOptions",
+ "MissingRequiredFieldError",
+ "NotSignedInError",
+ "ServerResponseError",
+ "Filter",
+ "Pager",
+ "Server",
+ "Sort",
+ "LinkedTaskItem",
+ "LinkedTaskStepItem",
+ "LinkedTaskFlowRunItem",
+ "VirtualConnectionItem",
]
diff --git a/tableauserverclient/_version.py b/tableauserverclient/_version.py
index 79dbed1d8..d47374097 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 OSError:
+ except EnvironmentError:
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(f"unable to find command, tried {commands}")
+ print("unable to find command, tried %s" % (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(f"Tried directories {str(rootdirs)} but none started with prefix {parentdir_prefix}")
+ print("Tried directories %s but none started with prefix %s" % (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)
+ f = open(versionfile_abs, "r")
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 OSError:
+ except EnvironmentError:
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 = {r.strip() for r in refnames.strip("()").split(",")}
+ refs = set([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 = {r[len(TAG) :] for r in refs if r.startswith(TAG)}
+ tags = set([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 = {r for r in refs if re.search(r"\d", r)}
+ tags = set([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 '{}' doesn't start with prefix '{}'".format(
+ pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % (
full_tag,
tag_prefix,
)
diff --git a/tableauserverclient/config.py b/tableauserverclient/config.py
index a75112754..63872398f 100644
--- a/tableauserverclient/config.py
+++ b/tableauserverclient/config.py
@@ -6,13 +6,11 @@
DELAY_SLEEP_SECONDS = 0.1
+# The maximum size of a file that can be published in a single request is 64MB
+FILESIZE_LIMIT_MB = 64
-class Config:
- # The maximum size of a file that can be published in a single request is 64MB
- @property
- def FILESIZE_LIMIT_MB(self):
- return min(int(os.getenv("TSC_FILESIZE_LIMIT_MB", 64)), 64)
+class Config:
# For when a datasource is over 64MB, break it into 5MB(standard chunk size) chunks
@property
def CHUNK_SIZE_MB(self):
diff --git a/tableauserverclient/models/column_item.py b/tableauserverclient/models/column_item.py
index 3a7416e28..df936e315 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:
+class ColumnItem(object):
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 bb2cbbba9..d61bbb751 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:
+class ConnectionCredentials(object):
"""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 937e43481..62ff530c9 100644
--- a/tableauserverclient/models/connection_item.py
+++ b/tableauserverclient/models/connection_item.py
@@ -1,5 +1,5 @@
import logging
-from typing import Optional
+from typing import List, Optional
from defusedxml.ElementTree import fromstring
@@ -8,7 +8,7 @@
from tableauserverclient.helpers.logging import logger
-class ConnectionItem:
+class ConnectionItem(object):
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(
- f"Cannot update value: Query tagging is always enabled for {self._connection_type} connections"
+ "Cannot update value: Query tagging is always enabled for {} connections".format(self._connection_type)
)
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 a0c0a9844..246a19e7f 100644
--- a/tableauserverclient/models/custom_view_item.py
+++ b/tableauserverclient/models/custom_view_item.py
@@ -2,8 +2,7 @@
from defusedxml import ElementTree
from defusedxml.ElementTree import fromstring, tostring
-from typing import Callable, Optional
-from collections.abc import Iterator
+from typing import Callable, List, Optional
from .exceptions import UnpopulatedPropertyError
from .user_item import UserItem
@@ -12,14 +11,12 @@
from ..datetime_helpers import parse_datetime
-class CustomViewItem:
+class CustomViewItem(object):
def __init__(self, id: Optional[str] = None, name: Optional[str] = None) -> None:
self._content_url: Optional[str] = None # ?
self._created_at: Optional["datetime"] = None
self._id: Optional[str] = id
self._image: Optional[Callable[[], bytes]] = None
- self._pdf: Optional[Callable[[], bytes]] = None
- self._csv: Optional[Callable[[], Iterator[bytes]]] = None
self._name: Optional[str] = name
self._shared: Optional[bool] = False
self._updated_at: Optional["datetime"] = None
@@ -38,17 +35,11 @@ def __repr__(self: "CustomViewItem"):
owner_info = ""
if self._owner:
owner_info = " owner='{}'".format(self._owner.name or self._owner.id or "unknown")
- return f""
+ return "".format(self.id, self.name, view_info, wb_info, owner_info)
def _set_image(self, image):
self._image = image
- def _set_pdf(self, pdf):
- self._pdf = pdf
-
- def _set_csv(self, csv):
- self._csv = csv
-
@property
def content_url(self) -> Optional[str]:
return self._content_url
@@ -64,24 +55,10 @@ def id(self) -> Optional[str]:
@property
def image(self) -> bytes:
if self._image is None:
- error = "Custom View item must be populated with its png image first."
+ error = "View item must be populated with its png image first."
raise UnpopulatedPropertyError(error)
return self._image()
- @property
- def pdf(self) -> bytes:
- if self._pdf is None:
- error = "Custom View item must be populated with its pdf first."
- raise UnpopulatedPropertyError(error)
- return self._pdf()
-
- @property
- def csv(self) -> Iterator[bytes]:
- if self._csv is None:
- error = "Custom View item must be populated with its csv first."
- raise UnpopulatedPropertyError(error)
- return self._csv()
-
@property
def name(self) -> Optional[str]:
return self._name
@@ -127,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)
"""
@@ -144,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 3a8883bed..7424e6b95 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:
- class ComparisonRecord:
+class DataAccelerationReportItem(object):
+ class ComparisonRecord(object):
def __init__(
self,
site,
diff --git a/tableauserverclient/models/data_alert_item.py b/tableauserverclient/models/data_alert_item.py
index 7285ee609..65be233e3 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 Optional
+from typing import List, Optional
from defusedxml.ElementTree import fromstring
@@ -10,7 +10,7 @@
)
-class DataAlertItem:
+class DataAlertItem(object):
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 6e0cb9001..f567c501c 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
+from typing import Optional, Union, List
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 = f"Invalid interval value for a monthly frequency: {interval_values}."
+ error = "Invalid interval value for a monthly frequency: {}.".format(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 4d4604461..dfc58e1bb 100644
--- a/tableauserverclient/models/database_item.py
+++ b/tableauserverclient/models/database_item.py
@@ -10,7 +10,7 @@
)
-class DatabaseItem:
+class DatabaseItem(object):
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 f""
+ return "".format(self._id, self.name)
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 = f"_default_{content_type}_permissions"
+ attr = "_default_{content}_permissions".format(content=content_type)
setattr(
self,
attr,
diff --git a/tableauserverclient/models/datasource_item.py b/tableauserverclient/models/datasource_item.py
index 1b082c157..e4e71c4a2 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 Optional
+from typing import Dict, List, Optional, Set, Tuple
from defusedxml.ElementTree import fromstring
@@ -18,14 +18,14 @@
from tableauserverclient.models.tag_item import TagItem
-class DatasourceItem:
+class DatasourceItem(object):
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 fbda9d9f2..ada041481 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:
+class DQWItem(object):
class WarningType:
WARNING = "WARNING"
DEPRECATED = "DEPRECATED"
diff --git a/tableauserverclient/models/favorites_item.py b/tableauserverclient/models/favorites_item.py
index 4fea280f7..caff755e3 100644
--- a/tableauserverclient/models/favorites_item.py
+++ b/tableauserverclient/models/favorites_item.py
@@ -1,27 +1,28 @@
import logging
-from typing import Union
from defusedxml.ElementTree import fromstring
-
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: Union[str, bytes], namespace: dict) -> FavoriteType:
+ def from_response(cls, xml: str, namespace: Dict) -> FavoriteType:
favorites: FavoriteType = {
"datasources": [],
"flows": [],
diff --git a/tableauserverclient/models/fileupload_item.py b/tableauserverclient/models/fileupload_item.py
index aea4dfe1f..e9bdd25b2 100644
--- a/tableauserverclient/models/fileupload_item.py
+++ b/tableauserverclient/models/fileupload_item.py
@@ -1,7 +1,7 @@
from defusedxml.ElementTree import fromstring
-class FileuploadItem:
+class FileuploadItem(object):
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 9bcad5e89..edce2ec97 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 Optional
+from typing import List, Optional, Set
from defusedxml.ElementTree import fromstring
@@ -14,9 +14,9 @@
from tableauserverclient.models.tag_item import TagItem
-class FlowItem:
+class FlowItem(object):
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 f2f1d561f..12281f4f8 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 Optional
+from typing import Dict, List, Optional, Type
from defusedxml.ElementTree import fromstring
from tableauserverclient.datetime_helpers import parse_datetime
-class FlowRunItem:
+class FlowRunItem(object):
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 6871f8b16..6c8f7eb01 100644
--- a/tableauserverclient/models/group_item.py
+++ b/tableauserverclient/models/group_item.py
@@ -1,4 +1,4 @@
-from typing import Callable, Optional, TYPE_CHECKING
+from typing import Callable, List, Optional, TYPE_CHECKING
from defusedxml.ElementTree import fromstring
@@ -11,7 +11,7 @@
from tableauserverclient.server import Pager
-class GroupItem:
+class GroupItem(object):
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 f"{self.__class__.__name__}({self.__dict__!r})"
+ return "{}({!r})".format(self.__class__.__name__, self.__dict__)
@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 aa653a79e..ffb57adf5 100644
--- a/tableauserverclient/models/groupset_item.py
+++ b/tableauserverclient/models/groupset_item.py
@@ -1,4 +1,4 @@
-from typing import Optional
+from typing import Dict, List, 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 d7cf891cc..444674e19 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:
+class IntervalItem(object):
class Frequency:
Hourly = "Hourly"
Daily = "Daily"
@@ -25,7 +25,7 @@ class Day:
LastDay = "LastDay"
-class HourlyInterval:
+class HourlyInterval(object):
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 = f"Invalid weekDay interval {interval}"
+ error = "Invalid weekDay interval {}".format(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 = f"Invalid interval {interval} not in {str(VALID_INTERVALS)}"
+ error = "Invalid interval {} not in {}".format(interval, str(VALID_INTERVALS))
raise ValueError(error)
self._interval = intervals
@@ -108,7 +108,7 @@ def _interval_type_pairs(self):
return interval_type_pairs
-class DailyInterval:
+class DailyInterval(object):
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 = f"Invalid weekDay interval {interval}"
+ error = "Invalid weekDay interval {}".format(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 = f"Invalid interval {interval} not in {str(VALID_INTERVALS)}"
+ error = "Invalid interval {} not in {}".format(interval, str(VALID_INTERVALS))
raise ValueError(error)
self._interval = intervals
@@ -176,7 +176,7 @@ def _interval_type_pairs(self):
return interval_type_pairs
-class WeeklyInterval:
+class WeeklyInterval(object):
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:
+class MonthlyInterval(object):
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 cc7cd5811..155ce668b 100644
--- a/tableauserverclient/models/job_item.py
+++ b/tableauserverclient/models/job_item.py
@@ -1,5 +1,5 @@
import datetime
-from typing import Optional
+from typing import List, Optional
from defusedxml.ElementTree import fromstring
@@ -7,7 +7,7 @@
from tableauserverclient.models.flow_run_item import FlowRunItem
-class JobItem:
+class JobItem(object):
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:
+class BackgroundJobItem(object):
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 14a0e4978..ae9b60425 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 Optional
+from typing import List, 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 432fd861a..d8ba8e825 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 Optional
+from typing import List, Optional, Set
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:
+class MetricItem(object):
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 f30519be5..8cebd1c86 100644
--- a/tableauserverclient/models/pagination_item.py
+++ b/tableauserverclient/models/pagination_item.py
@@ -1,7 +1,7 @@
from defusedxml.ElementTree import fromstring
-class PaginationItem:
+class PaginationItem(object):
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 bb3487279..26f4ee7e8 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 Optional
+from typing import Dict, List, Optional
from defusedxml.ElementTree import fromstring
@@ -36,25 +36,23 @@ class Capability:
ShareView = "ShareView"
ViewComments = "ViewComments"
ViewUnderlyingData = "ViewUnderlyingData"
- VizqlDataApiAccess = "VizqlDataApiAccess"
WebAuthoring = "WebAuthoring"
Write = "Write"
RunExplainData = "RunExplainData"
CreateRefreshMetrics = "CreateRefreshMetrics"
SaveAs = "SaveAs"
- PulseMetricDefine = "PulseMetricDefine"
def __repr__(self):
return ""
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 f""
+ return "".format(self.grantee, self.capabilities)
def __eq__(self, other: object) -> bool:
if not hasattr(other, "grantee") or not hasattr(other, "capabilities"):
@@ -68,7 +66,7 @@ def __and__(self, other: "PermissionsRule") -> "PermissionsRule":
if self.capabilities == other.capabilities:
return self
- capabilities = {*self.capabilities.keys(), *other.capabilities.keys()}
+ capabilities = set((*self.capabilities.keys(), *other.capabilities.keys()))
new_capabilities = {}
for capability in capabilities:
if (self.capabilities.get(capability), other.capabilities.get(capability)) == (
@@ -88,7 +86,7 @@ def __or__(self, other: "PermissionsRule") -> "PermissionsRule":
if self.capabilities == other.capabilities:
return self
- capabilities = {*self.capabilities.keys(), *other.capabilities.keys()}
+ capabilities = set((*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)):
@@ -102,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)
@@ -118,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(f"Capability was not valid: {capability_xml}")
+ logger.error("Capability was not valid: {}".format(capability_xml))
raise UnpopulatedPropertyError()
else:
capability_dict[name] = mode
@@ -129,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
@@ -148,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(f"No support for grantee type of {grantee_type}")
+ raise UnknownGranteeTypeError("No support for grantee type of {}".format(grantee_type))
return grantee
diff --git a/tableauserverclient/models/project_item.py b/tableauserverclient/models/project_item.py
index 48f27c60c..9fb382885 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 Optional
+from typing import List, Optional
from defusedxml.ElementTree import fromstring
@@ -8,16 +8,14 @@
from tableauserverclient.models.property_decorators import property_is_enum, property_not_empty
-class ProjectItem:
- ERROR_MSG = "Project item must be populated with permissions first."
-
+class ProjectItem(object):
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"
)
@@ -45,9 +43,6 @@ def __init__(
self._default_lens_permissions = None
self._default_datarole_permissions = None
self._default_metric_permissions = None
- self._default_virtualconnection_permissions = None
- self._default_database_permissions = None
- self._default_table_permissions = None
@property
def content_permissions(self):
@@ -61,63 +56,52 @@ def content_permissions(self, value: Optional[str]) -> None:
@property
def permissions(self):
if self._permissions is None:
- raise UnpopulatedPropertyError(self.ERROR_MSG)
+ error = "Project item must be populated with permissions first."
+ raise UnpopulatedPropertyError(error)
return self._permissions()
@property
def default_datasource_permissions(self):
if self._default_datasource_permissions is None:
- raise UnpopulatedPropertyError(self.ERROR_MSG)
+ error = "Project item must be populated with permissions first."
+ raise UnpopulatedPropertyError(error)
return self._default_datasource_permissions()
@property
def default_workbook_permissions(self):
if self._default_workbook_permissions is None:
- raise UnpopulatedPropertyError(self.ERROR_MSG)
+ error = "Project item must be populated with permissions first."
+ raise UnpopulatedPropertyError(error)
return self._default_workbook_permissions()
@property
def default_flow_permissions(self):
if self._default_flow_permissions is None:
- raise UnpopulatedPropertyError(self.ERROR_MSG)
+ error = "Project item must be populated with permissions first."
+ raise UnpopulatedPropertyError(error)
return self._default_flow_permissions()
@property
def default_lens_permissions(self):
if self._default_lens_permissions is None:
- raise UnpopulatedPropertyError(self.ERROR_MSG)
+ error = "Project item must be populated with permissions first."
+ raise UnpopulatedPropertyError(error)
return self._default_lens_permissions()
@property
def default_datarole_permissions(self):
if self._default_datarole_permissions is None:
- raise UnpopulatedPropertyError(self.ERROR_MSG)
+ error = "Project item must be populated with permissions first."
+ raise UnpopulatedPropertyError(error)
return self._default_datarole_permissions()
@property
def default_metric_permissions(self):
if self._default_metric_permissions is None:
- raise UnpopulatedPropertyError(self.ERROR_MSG)
+ error = "Project item must be populated with permissions first."
+ raise UnpopulatedPropertyError(error)
return self._default_metric_permissions()
- @property
- def default_virtualconnection_permissions(self):
- if self._default_virtualconnection_permissions is None:
- raise UnpopulatedPropertyError(self.ERROR_MSG)
- return self._default_virtualconnection_permissions()
-
- @property
- def default_database_permissions(self):
- if self._default_database_permissions is None:
- raise UnpopulatedPropertyError(self.ERROR_MSG)
- return self._default_database_permissions()
-
- @property
- def default_table_permissions(self):
- if self._default_table_permissions is None:
- raise UnpopulatedPropertyError(self.ERROR_MSG)
- return self._default_table_permissions()
-
@property
def id(self) -> Optional[str]:
return self._id
@@ -174,7 +158,7 @@ def _set_permissions(self, permissions):
self._permissions = permissions
def _set_default_permissions(self, permissions, content_type):
- attr = f"_default_{content_type}_permissions"
+ attr = "_default_{content}_permissions".format(content=content_type)
setattr(
self,
attr,
@@ -182,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 5048b3498..ce31b1428 100644
--- a/tableauserverclient/models/property_decorators.py
+++ b/tableauserverclient/models/property_decorators.py
@@ -1,8 +1,7 @@
import datetime
import re
from functools import wraps
-from typing import Any, Optional
-from collections.abc import Container
+from typing import Any, Container, Optional, Tuple
from tableauserverclient.datetime_helpers import parse_datetime
@@ -12,7 +11,7 @@ def property_type_decorator(func):
@wraps(func)
def wrapper(self, value):
if value is not None and not hasattr(enum_type, value):
- error = f"Invalid value: {value}. {func.__name__} must be of type {enum_type.__name__}."
+ error = "Invalid value: {0}. {1} must be of type {2}.".format(value, func.__name__, enum_type.__name__)
raise ValueError(error)
return func(self, value)
@@ -25,7 +24,7 @@ def property_is_boolean(func):
@wraps(func)
def wrapper(self, value):
if not isinstance(value, bool):
- error = f"Boolean expected for {func.__name__} flag."
+ error = "Boolean expected for {0} flag.".format(func.__name__)
raise ValueError(error)
return func(self, value)
@@ -36,7 +35,7 @@ def property_not_nullable(func):
@wraps(func)
def wrapper(self, value):
if value is None:
- error = f"{func.__name__} must be defined."
+ error = "{0} must be defined.".format(func.__name__)
raise ValueError(error)
return func(self, value)
@@ -47,7 +46,7 @@ def property_not_empty(func):
@wraps(func)
def wrapper(self, value):
if not value:
- error = f"{func.__name__} must not be empty."
+ error = "{0} must not be empty.".format(func.__name__)
raise ValueError(error)
return func(self, value)
@@ -67,7 +66,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.
@@ -82,7 +81,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 = f"Invalid property defined: '{value}'. Integer value expected."
+ error = "Invalid property defined: '{}'. Integer value expected.".format(value)
if range is None:
if isinstance(value, int):
@@ -134,7 +133,7 @@ def wrapper(self, value):
return func(self, value)
if not isinstance(value, str):
raise ValueError(
- f"Cannot convert {value.__class__.__name__} into a datetime, cannot update {func.__name__}"
+ "Cannot convert {} into a datetime, cannot update {}".format(value.__class__.__name__, func.__name__)
)
dt = parse_datetime(value)
@@ -147,11 +146,11 @@ def property_is_data_acceleration_config(func):
@wraps(func)
def wrapper(self, value):
if not isinstance(value, dict):
- raise ValueError(f"{value.__class__.__name__} is not type 'dict', cannot update {func.__name__})")
+ raise ValueError("{} is not type 'dict', cannot update {})".format(value.__class__.__name__, func.__name__))
if len(value) < 2 or not all(attr in value.keys() for attr in ("acceleration_enabled", "accelerate_now")):
- error = f"{func.__name__} should have 2 keys "
+ error = "{} should have 2 keys ".format(func.__name__)
error += "'acceleration_enabled' and 'accelerate_now'"
- error += f"instead you have {value.keys()}"
+ error += "instead you have {}".format(value.keys())
raise ValueError(error)
return func(self, value)
diff --git a/tableauserverclient/models/reference_item.py b/tableauserverclient/models/reference_item.py
index 4c1fff564..710548fcc 100644
--- a/tableauserverclient/models/reference_item.py
+++ b/tableauserverclient/models/reference_item.py
@@ -1,10 +1,10 @@
-class ResourceReference:
+class ResourceReference(object):
def __init__(self, id_, tag_name):
self.id = id_
self.tag_name = tag_name
def __str__(self):
- return f""
+ return "".format(self._id, self._tag_name)
__repr__ = __str__
diff --git a/tableauserverclient/models/revision_item.py b/tableauserverclient/models/revision_item.py
index 1b4cc6249..a0e6a1bd5 100644
--- a/tableauserverclient/models/revision_item.py
+++ b/tableauserverclient/models/revision_item.py
@@ -1,12 +1,12 @@
from datetime import datetime
-from typing import Optional
+from typing import List, Optional
from defusedxml.ElementTree import fromstring
from tableauserverclient.datetime_helpers import parse_datetime
-class RevisionItem:
+class RevisionItem(object):
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 e39042058..e416643ba 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:
+class ScheduleItem(object):
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 = (
- f"Status {response.status_code}: {response.reason}"
+ "Status {}: {}".format(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 b13f26740..57fc51af9 100644
--- a/tableauserverclient/models/server_info_item.py
+++ b/tableauserverclient/models/server_info_item.py
@@ -6,29 +6,7 @@
from tableauserverclient.helpers.logging import logger
-class ServerInfoItem:
- """
- The ServerInfoItem class contains the build and version information for
- Tableau Server. The server information is accessed with the
- server_info.get() method, which returns an instance of the ServerInfo class.
-
- Attributes
- ----------
- product_version : str
- Shows the version of the Tableau Server or Tableau Cloud
- (for example, 10.2.0).
-
- build_number : str
- Shows the specific build number (for example, 10200.17.0329.1446).
-
- rest_api_version : str
- Shows the supported REST API version number. Note that this might be
- different from the default value specified for the server, with the
- Server.version attribute. To take advantage of new features, you should
- query the server and set the Server.version to match the supported REST
- API version number.
- """
-
+class ServerInfoItem(object):
def __init__(self, product_version, build_number, rest_api_version):
self._product_version = product_version
self._build_number = build_number
@@ -62,11 +40,13 @@ def from_response(cls, resp, ns):
try:
parsed_response = fromstring(resp)
except xml.etree.ElementTree.ParseError as error:
- logger.exception(f"Unexpected response for ServerInfo: {resp}")
+ logger.info("Unexpected response for ServerInfo: {}".format(resp))
+ logger.info(error)
return cls("Unknown", "Unknown", "Unknown")
except Exception as error:
- logger.exception(f"Unexpected response for ServerInfo: {resp}")
- raise error
+ logger.info("Unexpected response for ServerInfo: {}".format(resp))
+ logger.info(error)
+ return cls("Unknown", "Unknown", "Unknown")
product_version_tag = parsed_response.find(".//t:productVersion", namespaces=ns)
rest_api_version_tag = parsed_response.find(".//t:restApiVersion", namespaces=ns)
diff --git a/tableauserverclient/models/site_item.py b/tableauserverclient/models/site_item.py
index e4e146f9c..b651e5773 100644
--- a/tableauserverclient/models/site_item.py
+++ b/tableauserverclient/models/site_item.py
@@ -14,79 +14,13 @@
VALID_CONTENT_URL_RE = r"^[a-zA-Z0-9_\-]*$"
-from typing import Optional, Union, TYPE_CHECKING
+from typing import List, Optional, Union, TYPE_CHECKING
if TYPE_CHECKING:
from tableauserverclient.server import Server
-class SiteItem:
- """
- The SiteItem class contains the members or attributes for the site resources
- on Tableau Server or Tableau Cloud. The SiteItem class defines the
- information you can request or query from Tableau Server or Tableau Cloud.
- The class members correspond to the attributes of a server request or
- response payload.
-
- Attributes
- ----------
- name: str
- The name of the site. The name of the default site is "".
-
- content_url: str
- The path to the site.
-
- admin_mode: str
- (Optional) For Tableau Server only. Specify ContentAndUsers to allow
- site administrators to use the server interface and tabcmd commands to
- add and remove users. (Specifying this option does not give site
- administrators permissions to manage users using the REST API.) Specify
- ContentOnly to prevent site administrators from adding or removing
- users. (Server administrators can always add or remove users.)
-
- user_quota: int
- (Optional) Specifies the total number of users for the site. The number
- can't exceed the number of licenses activated for the site; and if
- tiered capacity attributes are set, then user_quota will equal the sum
- of the tiered capacity values, and attempting to set user_quota will
- cause an error.
-
- tier_explorer_capacity: int
- tier_creator_capacity: int
- tier_viewer_capacity: int
- (Optional) The maximum number of licenses for users with the Creator,
- Explorer, or Viewer role, respectively, allowed on a site.
-
- storage_quota: int
- (Optional) Specifies the maximum amount of space for the new site, in
- megabytes. If you set a quota and the site exceeds it, publishers will
- be prevented from uploading new content until the site is under the
- limit again.
-
- disable_subscriptions: bool
- (Optional) Specify true to prevent users from being able to subscribe
- to workbooks on the specified site. The default is False.
-
- subscribe_others_enabled: bool
- (Optional) Specify false to prevent server administrators, site
- administrators, and project or content owners from being able to
- subscribe other users to workbooks on the specified site. The default
- is True.
-
- revision_history_enabled: bool
- (Optional) Specify true to enable revision history for content resources
- (workbooks and datasources). The default is False.
-
- revision_limit: int
- (Optional) Specifies the number of revisions of a content source
- (workbook or data source) to allow. On Tableau Server, the default is
- 25.
-
- state: str
- Shows the current state of the site (Active or Suspended).
-
- """
-
+class SiteItem(object):
_user_quota: Optional[int] = None
_tier_creator_capacity: Optional[int] = None
_tier_explorer_capacity: Optional[int] = None
@@ -939,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 61c75e2d6..e96fcc448 100644
--- a/tableauserverclient/models/subscription_item.py
+++ b/tableauserverclient/models/subscription_item.py
@@ -1,4 +1,4 @@
-from typing import TYPE_CHECKING
+from typing import List, Type, TYPE_CHECKING
from defusedxml.ElementTree import fromstring
@@ -10,7 +10,7 @@
from .target import Target
-class SubscriptionItem:
+class SubscriptionItem(object):
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 0afdd4df3..f9df8a8f3 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:
+class TableItem(object):
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 7d7981433..10cf58723 100644
--- a/tableauserverclient/models/tableau_auth.py
+++ b/tableauserverclient/models/tableau_auth.py
@@ -1,5 +1,5 @@
import abc
-from typing import Optional
+from typing import Dict, 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"
@@ -32,43 +32,6 @@ def deprecate_site_attribute():
# The traditional auth type: username/password
class TableauAuth(Credentials):
- """
- The TableauAuth class defines the information you can set in a sign-in
- request. The class members correspond to the attributes of a server request
- or response payload. To use this class, create a new instance, supplying
- user name, password, and site information if necessary, and pass the
- request object to the Auth.sign_in method.
-
- Parameters
- ----------
- username : str
- The user name for the sign-in request.
-
- password : str
- The password for the sign-in request.
-
- site_id : str, optional
- This corresponds to the contentUrl attribute in the Tableau REST API.
- The site_id is the portion of the URL that follows the /site/ in the
- URL. For example, "MarketingTeam" is the site_id in the following URL
- MyServer/#/site/MarketingTeam/projects. To specify the default site on
- Tableau Server, you can use an empty string '' (single quotes, no
- space). For Tableau Cloud, you must provide a value for the site_id.
-
- user_id_to_impersonate : str, optional
- Specifies the id (not the name) of the user to sign in as. This is not
- available for Tableau Online.
-
- Examples
- --------
- >>> import tableauserverclient as TSC
-
- >>> tableau_auth = TSC.TableauAuth('USERNAME', 'PASSWORD', site_id='CONTENTURL')
- >>> server = TSC.Server('https://SERVER_URL', use_server_version=True)
- >>> server.auth.sign_in(tableau_auth)
-
- """
-
def __init__(
self, username: str, password: str, site_id: Optional[str] = None, user_id_to_impersonate: Optional[str] = None
) -> None:
@@ -79,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):
@@ -92,43 +55,6 @@ def __repr__(self):
# A Tableau-generated Personal Access Token
class PersonalAccessTokenAuth(Credentials):
- """
- The PersonalAccessTokenAuth class defines the information you can set in a sign-in
- request. The class members correspond to the attributes of a server request
- or response payload. To use this class, create a new instance, supplying
- token name, token secret, and site information if necessary, and pass the
- request object to the Auth.sign_in method.
-
- Parameters
- ----------
- token_name : str
- The name of the personal access token.
-
- personal_access_token : str
- The personal access token secret for the sign in request.
-
- site_id : str, optional
- This corresponds to the contentUrl attribute in the Tableau REST API.
- The site_id is the portion of the URL that follows the /site/ in the
- URL. For example, "MarketingTeam" is the site_id in the following URL
- MyServer/#/site/MarketingTeam/projects. To specify the default site on
- Tableau Server, you can use an empty string '' (single quotes, no
- space). For Tableau Cloud, you must provide a value for the site_id.
-
- user_id_to_impersonate : str, optional
- Specifies the id (not the name) of the user to sign in as. This is not
- available for Tableau Online.
-
- Examples
- --------
- >>> import tableauserverclient as TSC
-
- >>> tableau_auth = TSC.PersonalAccessTokenAuth("token_name", "token_secret", site_id='CONTENTURL')
- >>> server = TSC.Server('https://SERVER_URL', use_server_version=True)
- >>> server.auth.sign_in(tableau_auth)
-
- """
-
def __init__(
self,
token_name: str,
@@ -143,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,
@@ -162,42 +88,6 @@ def __repr__(self):
# A standard JWT generated specifically for Tableau
class JWTAuth(Credentials):
- """
- The JWTAuth class defines the information you can set in a sign-in
- request. The class members correspond to the attributes of a server request
- or response payload. To use this class, create a new instance, supplying
- an encoded JSON Web Token, and site information if necessary, and pass the
- request object to the Auth.sign_in method.
-
- Parameters
- ----------
- token : str
- The encoded JSON Web Token.
-
- site_id : str, optional
- This corresponds to the contentUrl attribute in the Tableau REST API.
- The site_id is the portion of the URL that follows the /site/ in the
- URL. For example, "MarketingTeam" is the site_id in the following URL
- MyServer/#/site/MarketingTeam/projects. To specify the default site on
- Tableau Server, you can use an empty string '' (single quotes, no
- space). For Tableau Cloud, you must provide a value for the site_id.
-
- user_id_to_impersonate : str, optional
- Specifies the id (not the name) of the user to sign in as. This is not
- available for Tableau Online.
-
- Examples
- --------
- >>> import jwt
- >>> import tableauserverclient as TSC
-
- >>> jwt_token = jwt.encode(...)
- >>> tableau_auth = TSC.JWTAuth(token, site_id='CONTENTURL')
- >>> server = TSC.Server('https://SERVER_URL', use_server_version=True)
- >>> server.auth.sign_in(tableau_auth)
-
- """
-
def __init__(self, jwt: str, site_id: Optional[str] = None, user_id_to_impersonate: Optional[str] = None) -> None:
if jwt is None:
raise TabError("Must provide a JWT token when using JWT authentication")
@@ -205,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 01ee3d3a9..bac072076 100644
--- a/tableauserverclient/models/tableau_types.py
+++ b/tableauserverclient/models/tableau_types.py
@@ -28,8 +28,8 @@ class Resource:
TableauItem = Union[DatasourceItem, FlowItem, MetricItem, ProjectItem, ViewItem, WorkbookItem, VirtualConnectionItem]
-def plural_type(content_type: Union[Resource, str]) -> str:
+def plural_type(content_type: Resource) -> str:
if content_type == Resource.Lens:
return "lenses"
else:
- return f"{content_type}s"
+ return "{}s".format(content_type)
diff --git a/tableauserverclient/models/tag_item.py b/tableauserverclient/models/tag_item.py
index cde755f05..afa0a0762 100644
--- a/tableauserverclient/models/tag_item.py
+++ b/tableauserverclient/models/tag_item.py
@@ -1,15 +1,16 @@
import xml.etree.ElementTree as ET
+from typing import Set
from defusedxml.ElementTree import fromstring
-class TagItem:
+class TagItem(object):
@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 fa6f782ba..01cfcfb11 100644
--- a/tableauserverclient/models/task_item.py
+++ b/tableauserverclient/models/task_item.py
@@ -1,5 +1,5 @@
from datetime import datetime
-from typing import Optional
+from typing import List, Optional
from defusedxml.ElementTree import fromstring
@@ -8,7 +8,7 @@
from tableauserverclient.models.target import Target
-class TaskItem:
+class TaskItem(object):
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(f".//t:task/t:{task_type}", namespaces=ns)
+ all_tasks_xml = parsed_response.findall(".//t:task/t:{}".format(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 365e44c1d..fe659575a 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 Optional, TYPE_CHECKING
+from typing import Dict, List, Optional, TYPE_CHECKING, Tuple
from defusedxml.ElementTree import fromstring
@@ -18,35 +18,10 @@
from tableauserverclient.server import Pager
-class UserItem:
- """
- The UserItem class contains the members or attributes for the view
- resources on Tableau Server. The UserItem class defines the information you
- can request or query from Tableau Server. The class attributes correspond
- to the attributes of a server request or response payload.
-
-
- Parameters
- ----------
- name: str
- The name of the user.
-
- site_role: str
- The role of the user on the site.
-
- auth_setting: str
- Required attribute for Tableau Cloud. How the user autenticates to the
- server.
- """
-
+class UserItem(object):
tag_name: str = "user"
class Roles:
- """
- The Roles class contains the possible roles for a user on Tableau
- Server.
- """
-
Interactor = "Interactor"
Publisher = "Publisher"
ServerAdministrator = "ServerAdministrator"
@@ -68,11 +43,6 @@ class Roles:
SupportUser = "SupportUser"
class Auth:
- """
- The Auth class contains the possible authentication settings for a user
- on Tableau Cloud.
- """
-
OpenID = "OpenID"
SAML = "SAML"
TableauIDWithMFA = "TableauIDWithMFA"
@@ -87,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
@@ -99,7 +69,7 @@ def __init__(
def __str__(self) -> str:
str_site_role = self.site_role or "None"
- return f""
+ return "".format(self.id, self.name, str_site_role)
def __repr__(self):
return self.__str__() + " { " + ", ".join(" % s: % s" % item for item in vars(self).items()) + "}"
@@ -171,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)
@@ -240,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)
@@ -313,7 +283,7 @@ def _parse_element(user_xml, ns):
domain_name,
)
- class CSVImport:
+ class CSVImport(object):
"""
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
@@ -338,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:
@@ -367,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
@@ -375,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(f"Reading user {line[:4]}")
+ logger.info("Reading user {}".format(line[:4]))
UserItem.CSVImport._validate_import_line_or_throw(line, logger)
num_valid_lines += 1
except Exception as exc:
- logger.info(f"Error parsing {line[:4]}: {exc}")
+ logger.info("Error parsing {}: {}".format(line[:4], exc))
invalid_lines.append(line)
line = csv_file.readline()
return num_valid_lines, invalid_lines
@@ -388,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]] = [
[],
[],
[],
@@ -403,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(f"> details - {username}")
+ logger.debug("> details - {}".format(username))
UserItem.validate_username_or_throw(username)
for i in range(1, len(line)):
- logger.debug(f"column {UserItem.CSVImport.ColumnType(i).name}: {line[i]}")
+ logger.debug("column {}: {}".format(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(f"Invalid value {item} for {column_type}")
+ raise AttributeError("Invalid value {} for {}".format(item, 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 dc5f37a48..a26e364a3 100644
--- a/tableauserverclient/models/view_item.py
+++ b/tableauserverclient/models/view_item.py
@@ -1,8 +1,7 @@
import copy
from datetime import datetime
from requests import Response
-from typing import Callable, Optional
-from collections.abc import Iterator
+from typing import Callable, Iterator, List, Optional, Set
from defusedxml.ElementTree import fromstring
@@ -12,13 +11,13 @@
from .tag_item import TagItem
-class ViewItem:
+class ViewItem(object):
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
@@ -30,15 +29,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
)
@@ -147,21 +146,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 e9e22be1e..76a3b5dea 100644
--- a/tableauserverclient/models/virtual_connection_item.py
+++ b/tableauserverclient/models/virtual_connection_item.py
@@ -1,7 +1,6 @@
import datetime as dt
import json
-from typing import Callable, Optional
-from collections.abc import Iterable
+from typing import Callable, Dict, Iterable, List, Optional
from xml.etree.ElementTree import Element
from defusedxml.ElementTree import fromstring
@@ -24,7 +23,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:
@@ -41,7 +40,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)
@@ -54,12 +53,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 98d821fb4..e4d5e4aa0 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 Optional
+from typing import List, Optional, Tuple, Type
from defusedxml.ElementTree import fromstring
@@ -13,7 +13,7 @@ def _parse_event(events):
return NAMESPACE_RE.sub("", event.tag)
-class WebhookItem:
+class WebhookItem(object):
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 = f"webhook-source-event-{value}"
+ self._event = "webhook-source-event-{}".format(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 f""
+ return "".format(self.id, self.name, self.url, self.event)
diff --git a/tableauserverclient/models/workbook_item.py b/tableauserverclient/models/workbook_item.py
index 776d041e3..58fd2a9a9 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, Optional
+from typing import Callable, Dict, List, Optional, Set
from defusedxml.ElementTree import fromstring
@@ -20,85 +20,7 @@
from .data_freshness_policy_item import DataFreshnessPolicyItem
-class WorkbookItem:
- """
- The workbook resources for Tableau are defined in the WorkbookItem class.
- The class corresponds to the workbook resources you can access using the
- Tableau REST API. Some workbook methods take an instance of the WorkbookItem
- class as arguments. The workbook item specifies the project.
-
- Parameters
- ----------
- project_id : Optional[str], optional
- The project ID for the workbook, by default None.
-
- name : Optional[str], optional
- The name of the workbook, by default None.
-
- show_tabs : bool, optional
- Determines whether the workbook shows tabs for the view.
-
- Attributes
- ----------
- connections : list[ConnectionItem]
- The list of data connections (ConnectionItem) for the data sources used
- by the workbook. You must first call the workbooks.populate_connections
- method to access this data. See the ConnectionItem class.
-
- content_url : Optional[str]
- The name of the workbook as it appears in the URL.
-
- created_at : Optional[datetime.datetime]
- The date and time the workbook was created.
-
- description : Optional[str]
- User-defined description of the workbook.
-
- id : Optional[str]
- The identifier for the workbook. You need this value to query a specific
- workbook or to delete a workbook with the get_by_id and delete methods.
-
- owner_id : Optional[str]
- The identifier for the owner (UserItem) of the workbook.
-
- preview_image : bytes
- The thumbnail image for the view. You must first call the
- workbooks.populate_preview_image method to access this data.
-
- project_name : Optional[str]
- The name of the project that contains the workbook.
-
- size: int
- The size of the workbook in megabytes.
-
- hidden_views: Optional[list[str]]
- List of string names of views that need to be hidden when the workbook
- is published.
-
- tags: set[str]
- The set of tags associated with the workbook.
-
- updated_at : Optional[datetime.datetime]
- The date and time the workbook was last updated.
-
- views : list[ViewItem]
- The list of views (ViewItem) for the workbook. You must first call the
- workbooks.populate_views method to access this data. See the ViewItem
- class.
-
- web_page_url : Optional[str]
- The full URL for the workbook.
-
- Examples
- --------
- # creating a new instance of a WorkbookItem
- >>> import tableauserverclient as TSC
-
- >>> # Create new workbook_item with project id '3a8b6148-493c-11e6-a621-6f3499394a39'
-
- >>> new_workbook = TSC.WorkbookItem('3a8b6148-493c-11e6-a621-6f3499394a39')
- """
-
+class WorkbookItem(object):
def __init__(self, project_id: Optional[str] = None, name: Optional[str] = None, show_tabs: bool = False) -> None:
self._connections = None
self._content_url = None
@@ -113,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,
@@ -134,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
)
@@ -142,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)
@@ -230,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
@@ -269,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)
@@ -281,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:
@@ -394,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 54ac46d8d..d225ecff6 100644
--- a/tableauserverclient/namespace.py
+++ b/tableauserverclient/namespace.py
@@ -11,7 +11,7 @@ class UnknownNamespaceError(Exception):
pass
-class Namespace:
+class Namespace(object):
def __init__(self):
self._namespace = {"t": NEW_NAMESPACE}
self._detected = False
diff --git a/tableauserverclient/server/__init__.py b/tableauserverclient/server/__init__.py
index 87cc9460b..f5cd1d236 100644
--- a/tableauserverclient/server/__init__.py
+++ b/tableauserverclient/server/__init__.py
@@ -11,7 +11,7 @@
from tableauserverclient.server.sort import Sort
from tableauserverclient.server.server import Server
from tableauserverclient.server.pager import Pager
-from tableauserverclient.server.endpoint.exceptions import FailedSignInError, NotSignedInError
+from tableauserverclient.server.endpoint.exceptions import NotSignedInError
from tableauserverclient.server.endpoint import (
Auth,
@@ -57,7 +57,6 @@
"Sort",
"Server",
"Pager",
- "FailedSignInError",
"NotSignedInError",
"Auth",
"CustomViews",
diff --git a/tableauserverclient/server/endpoint/auth_endpoint.py b/tableauserverclient/server/endpoint/auth_endpoint.py
index 4211bb7ea..468d469a7 100644
--- a/tableauserverclient/server/endpoint/auth_endpoint.py
+++ b/tableauserverclient/server/endpoint/auth_endpoint.py
@@ -16,7 +16,7 @@
class Auth(Endpoint):
- class contextmgr:
+ class contextmgr(object):
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 f"{self.parent_srv.baseurl}/auth"
+ return "{0}/auth".format(self.parent_srv.baseurl)
@api(version="2.0")
def sign_in(self, auth_req: "Credentials") -> contextmgr:
@@ -41,32 +41,8 @@ def sign_in(self, auth_req: "Credentials") -> contextmgr:
optionally a user_id to impersonate.
Creates a context manager that will sign out of the server upon exit.
-
- Parameters
- ----------
- auth_req : Credentials
- The credentials object to use for signing in. Can be a TableauAuth,
- PersonalAccessTokenAuth, or JWTAuth object.
-
- Returns
- -------
- contextmgr
- A context manager that will sign out of the server upon exit.
-
- Examples
- --------
- >>> import tableauserverclient as TSC
-
- >>> # create an auth object
- >>> tableau_auth = TSC.TableauAuth('USERNAME', 'PASSWORD')
-
- >>> # create an instance for your server
- >>> server = TSC.Server('https://SERVER_URL')
-
- >>> # call the sign-in method with the auth object
- >>> server.auth.sign_in(tableau_auth)
"""
- url = f"{self.baseurl}/signin"
+ url = "{0}/{1}".format(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
@@ -87,25 +63,22 @@ 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(f"Signed into {self.parent_srv.server_address} as user with id {user_id}")
+ logger.info("Signed into {0} as user with id {1}".format(self.parent_srv.server_address, user_id))
return Auth.contextmgr(self.sign_out)
# We use the same request that username/password login uses for all auth types.
# The distinct methods are mostly useful for explicitly showing api version support for each auth type
@api(version="3.6")
def sign_in_with_personal_access_token(self, auth_req: "Credentials") -> contextmgr:
- """Passthrough to sign_in method"""
return self.sign_in(auth_req)
@api(version="3.17")
def sign_in_with_json_web_token(self, auth_req: "Credentials") -> contextmgr:
- """Passthrough to sign_in method"""
return self.sign_in(auth_req)
@api(version="2.0")
def sign_out(self) -> None:
- """Sign out of current session."""
- url = f"{self.baseurl}/signout"
+ url = "{0}/{1}".format(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
@@ -115,34 +88,7 @@ def sign_out(self) -> None:
@api(version="2.6")
def switch_site(self, site_item: "SiteItem") -> contextmgr:
- """
- Switch to a different site on the server. This will sign out of the
- current site and sign in to the new site. If used as a context manager,
- will sign out of the new site upon exit.
-
- Parameters
- ----------
- site_item : SiteItem
- The site to switch to.
-
- Returns
- -------
- contextmgr
- A context manager that will sign out of the new site upon exit.
-
- Examples
- --------
- >>> import tableauserverclient as TSC
-
- >>> # Find the site you want to switch to
- >>> new_site = server.sites.get_by_id("9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d")
- >>> # switch to the new site
- >>> with server.auth.switch_site(new_site):
- >>> # do something on the new site
- >>> pass
-
- """
- url = f"{self.baseurl}/switchSite"
+ url = "{0}/{1}".format(self.baseurl, "switchSite")
switch_req = RequestFactory.Auth.switch_req(site_item.content_url)
try:
server_response = self.post_request(url, switch_req)
@@ -158,14 +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(f"Signed into {self.parent_srv.server_address} as user with id {user_id}")
+ logger.info("Signed into {0} as user with id {1}".format(self.parent_srv.server_address, user_id))
return Auth.contextmgr(self.sign_out)
@api(version="3.10")
def revoke_all_server_admin_tokens(self) -> None:
- """
- Revokes all personal access tokens for all server admins on the server.
- """
- url = f"{self.baseurl}/revokeAllServerAdminTokens"
+ url = "{0}/{1}".format(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 b02b05d78..57a5b0100 100644
--- a/tableauserverclient/server/endpoint/custom_views_endpoint.py
+++ b/tableauserverclient/server/endpoint/custom_views_endpoint.py
@@ -1,23 +1,15 @@
import io
import logging
import os
-from contextlib import closing
from pathlib import Path
-from typing import Optional, Union
-from collections.abc import Iterator
+from typing import List, Optional, Tuple, Union
-from tableauserverclient.config import BYTES_PER_MB, config
+from tableauserverclient.config import BYTES_PER_MB, FILESIZE_LIMIT_MB
from tableauserverclient.filesys_helpers import get_file_object_size
from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api
from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError
from tableauserverclient.models import CustomViewItem, PaginationItem
-from tableauserverclient.server import (
- RequestFactory,
- RequestOptions,
- ImageRequestOptions,
- PDFRequestOptions,
- CSVRequestOptions,
-)
+from tableauserverclient.server import RequestFactory, RequestOptions, ImageRequestOptions
from tableauserverclient.helpers.logging import logger
@@ -41,11 +33,11 @@
class CustomViews(QuerysetEndpoint[CustomViewItem]):
def __init__(self, parent_srv):
- super().__init__(parent_srv)
+ super(CustomViews, self).__init__(parent_srv)
@property
def baseurl(self) -> str:
- return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/customviews"
+ return "{0}/sites/{1}/customviews".format(self.parent_srv.baseurl, self.parent_srv.site_id)
@property
def expurl(self) -> str:
@@ -63,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)
@@ -76,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(f"Querying custom view (ID: {view_id})")
- url = f"{self.baseurl}/{view_id}"
+ logger.info("Querying custom view (ID: {0})".format(view_id))
+ url = "{0}/{1}".format(self.baseurl, view_id)
server_response = self.get_request(url)
return CustomViewItem.from_response(server_response.content, self.parent_srv.namespace)
@@ -91,53 +83,17 @@ def image_fetcher():
return self._get_view_image(view_item, req_options)
view_item._set_image(image_fetcher)
- logger.info(f"Populated image for custom view (ID: {view_item.id})")
+ logger.info("Populated image for custom view (ID: {0})".format(view_item.id))
def _get_view_image(self, view_item: CustomViewItem, req_options: Optional["ImageRequestOptions"]) -> bytes:
- url = f"{self.baseurl}/{view_item.id}/image"
+ url = "{0}/{1}/image".format(self.baseurl, view_item.id)
server_response = self.get_request(url, req_options)
image = server_response.content
return image
- @api(version="3.23")
- def populate_pdf(self, custom_view_item: CustomViewItem, req_options: Optional["PDFRequestOptions"] = None) -> None:
- if not custom_view_item.id:
- error = "Custom View item missing ID."
- raise MissingRequiredFieldError(error)
-
- def pdf_fetcher():
- return self._get_custom_view_pdf(custom_view_item, req_options)
-
- custom_view_item._set_pdf(pdf_fetcher)
- logger.info(f"Populated pdf for custom view (ID: {custom_view_item.id})")
-
- def _get_custom_view_pdf(
- self, custom_view_item: CustomViewItem, req_options: Optional["PDFRequestOptions"]
- ) -> bytes:
- url = f"{self.baseurl}/{custom_view_item.id}/pdf"
- server_response = self.get_request(url, req_options)
- pdf = server_response.content
- return pdf
-
- @api(version="3.23")
- def populate_csv(self, custom_view_item: CustomViewItem, req_options: Optional["CSVRequestOptions"] = None) -> None:
- if not custom_view_item.id:
- error = "Custom View item missing ID."
- raise MissingRequiredFieldError(error)
-
- def csv_fetcher():
- return self._get_custom_view_csv(custom_view_item, req_options)
-
- custom_view_item._set_csv(csv_fetcher)
- logger.info(f"Populated csv for custom view (ID: {custom_view_item.id})")
-
- def _get_custom_view_csv(
- self, custom_view_item: CustomViewItem, req_options: Optional["CSVRequestOptions"]
- ) -> Iterator[bytes]:
- url = f"{self.baseurl}/{custom_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)
+ """
+ Not yet implemented: pdf or csv exports
+ """
@api(version="3.18")
def update(self, view_item: CustomViewItem) -> Optional[CustomViewItem]:
@@ -149,10 +105,10 @@ def update(self, view_item: CustomViewItem) -> Optional[CustomViewItem]:
return view_item
# Update the custom view owner or name
- url = f"{self.baseurl}/{view_item.id}"
+ url = "{0}/{1}".format(self.baseurl, view_item.id)
update_req = RequestFactory.CustomView.update_req(view_item)
server_response = self.put_request(url, update_req)
- logger.info(f"Updated custom view (ID: {view_item.id})")
+ logger.info("Updated custom view (ID: {0})".format(view_item.id))
return CustomViewItem.from_response(server_response.content, self.parent_srv.namespace)
# Delete 1 view by id
@@ -161,9 +117,9 @@ def delete(self, view_id: str) -> None:
if not view_id:
error = "Custom View ID undefined."
raise ValueError(error)
- url = f"{self.baseurl}/{view_id}"
+ url = "{0}/{1}".format(self.baseurl, view_id)
self.delete_request(url)
- logger.info(f"Deleted single custom view (ID: {view_id})")
+ logger.info("Deleted single custom view (ID: {0})".format(view_id))
@api(version="3.21")
def download(self, view_item: CustomViewItem, file: PathOrFileW) -> PathOrFileW:
@@ -188,7 +144,7 @@ def publish(self, view_item: CustomViewItem, file: PathOrFileR) -> Optional[Cust
else:
raise ValueError("File path or file object required for publishing custom view.")
- if size >= config.FILESIZE_LIMIT_MB * BYTES_PER_MB:
+ if size >= FILESIZE_LIMIT_MB * BYTES_PER_MB:
upload_session_id = self.parent_srv.fileuploads.upload(file)
url = f"{url}?uploadSessionId={upload_session_id}"
xml_request, content_type = RequestFactory.CustomView.publish_req_chunked(view_item)
diff --git a/tableauserverclient/server/endpoint/data_acceleration_report_endpoint.py b/tableauserverclient/server/endpoint/data_acceleration_report_endpoint.py
index 579001156..256a6e766 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().__init__(parent_srv)
+ super(DataAccelerationReport, self).__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 f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/dataAccelerationReport"
+ return "{0}/sites/{1}/dataAccelerationReport".format(self.parent_srv.baseurl, self.parent_srv.site_id)
@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 ba3ecd74f..fd02d2e4a 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 Optional, TYPE_CHECKING, Union
+from typing import List, Optional, TYPE_CHECKING, Tuple, Union
if TYPE_CHECKING:
@@ -17,14 +17,14 @@
class DataAlerts(Endpoint):
def __init__(self, parent_srv: "Server") -> None:
- super().__init__(parent_srv)
+ super(DataAlerts, self).__init__(parent_srv)
@property
def baseurl(self) -> str:
- return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/dataAlerts"
+ return "{0}/sites/{1}/dataAlerts".format(self.parent_srv.baseurl, self.parent_srv.site_id)
@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(f"Querying single dataAlert (ID: {dataAlert_id})")
- url = f"{self.baseurl}/{dataAlert_id}"
+ logger.info("Querying single dataAlert (ID: {0})".format(dataAlert_id))
+ url = "{0}/{1}".format(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 = f"{self.baseurl}/{dataAlert_id}"
+ url = "{0}/{1}".format(self.baseurl, dataAlert_id)
self.delete_request(url)
- logger.info(f"Deleted single dataAlert (ID: {dataAlert_id})")
+ logger.info("Deleted single dataAlert (ID: {0})".format(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 = f"{self.baseurl}/{dataAlert_id}/users/{user_id}"
+ url = "{0}/{1}/users/{2}".format(self.baseurl, dataAlert_id, user_id)
self.delete_request(url)
- logger.info(f"Deleted User (ID {user_id}) from dataAlert (ID: {dataAlert_id})")
+ logger.info("Deleted User (ID {0}) from dataAlert (ID: {1})".format(user_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 = f"{self.baseurl}/{dataAlert_item.id}/users"
+ url = "{0}/{1}/users".format(self.baseurl, dataAlert_item.id)
update_req = RequestFactory.DataAlert.add_user_to_alert(dataAlert_item, user_id)
server_response = self.post_request(url, update_req)
- logger.info(f"Added user (ID {user_id}) to dataAlert item (ID: {dataAlert_item.id})")
+ logger.info("Added user (ID {0}) to dataAlert item (ID: {1})".format(user_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 = f"{self.baseurl}/{dataAlert_item.id}"
+ url = "{0}/{1}".format(self.baseurl, dataAlert_item.id)
update_req = RequestFactory.DataAlert.update_req(dataAlert_item)
server_response = self.put_request(url, update_req)
- logger.info(f"Updated dataAlert item (ID: {dataAlert_item.id})")
+ logger.info("Updated dataAlert item (ID: {0})".format(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 c0e106eb2..2f8fece07 100644
--- a/tableauserverclient/server/endpoint/databases_endpoint.py
+++ b/tableauserverclient/server/endpoint/databases_endpoint.py
@@ -1,6 +1,5 @@
import logging
-from typing import Union
-from collections.abc import Iterable
+from typing import Union, Iterable, Set
from tableauserverclient.server.endpoint.default_permissions_endpoint import _DefaultPermissionsEndpoint
from tableauserverclient.server.endpoint.dqw_endpoint import _DataQualityWarningEndpoint
@@ -16,7 +15,7 @@
class Databases(Endpoint, TaggingMixin):
def __init__(self, parent_srv):
- super().__init__(parent_srv)
+ super(Databases, self).__init__(parent_srv)
self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl)
self._default_permissions = _DefaultPermissionsEndpoint(parent_srv, lambda: self.baseurl)
@@ -24,7 +23,7 @@ def __init__(self, parent_srv):
@property
def baseurl(self):
- return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/databases"
+ return "{0}/sites/{1}/databases".format(self.parent_srv.baseurl, self.parent_srv.site_id)
@api(version="3.5")
def get(self, req_options=None):
@@ -41,8 +40,8 @@ def get_by_id(self, database_id):
if not database_id:
error = "database ID undefined."
raise ValueError(error)
- logger.info(f"Querying single database (ID: {database_id})")
- url = f"{self.baseurl}/{database_id}"
+ logger.info("Querying single database (ID: {0})".format(database_id))
+ url = "{0}/{1}".format(self.baseurl, database_id)
server_response = self.get_request(url)
return DatabaseItem.from_response(server_response.content, self.parent_srv.namespace)[0]
@@ -51,9 +50,9 @@ def delete(self, database_id):
if not database_id:
error = "Database ID undefined."
raise ValueError(error)
- url = f"{self.baseurl}/{database_id}"
+ url = "{0}/{1}".format(self.baseurl, database_id)
self.delete_request(url)
- logger.info(f"Deleted single database (ID: {database_id})")
+ logger.info("Deleted single database (ID: {0})".format(database_id))
@api(version="3.5")
def update(self, database_item):
@@ -61,10 +60,10 @@ def update(self, database_item):
error = "Database item missing ID."
raise MissingRequiredFieldError(error)
- url = f"{self.baseurl}/{database_item.id}"
+ url = "{0}/{1}".format(self.baseurl, database_item.id)
update_req = RequestFactory.Database.update_req(database_item)
server_response = self.put_request(url, update_req)
- logger.info(f"Updated database item (ID: {database_item.id})")
+ logger.info("Updated database item (ID: {0})".format(database_item.id))
updated_database = DatabaseItem.from_response(server_response.content, self.parent_srv.namespace)[0]
return updated_database
@@ -79,10 +78,10 @@ def column_fetcher():
return self._get_tables_for_database(database_item)
database_item._set_tables(column_fetcher)
- logger.info(f"Populated tables for database (ID: {database_item.id}")
+ logger.info("Populated tables for database (ID: {0}".format(database_item.id))
def _get_tables_for_database(self, database_item):
- url = f"{self.baseurl}/{database_item.id}/tables"
+ url = "{0}/{1}/tables".format(self.baseurl, database_item.id)
server_response = self.get_request(url)
tables = TableItem.from_response(server_response.content, self.parent_srv.namespace)
return tables
@@ -128,7 +127,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 6bd809c28..7f3a47075 100644
--- a/tableauserverclient/server/endpoint/datasources_endpoint.py
+++ b/tableauserverclient/server/endpoint/datasources_endpoint.py
@@ -6,8 +6,7 @@
from contextlib import closing
from pathlib import Path
-from typing import Optional, TYPE_CHECKING, Union
-from collections.abc import Iterable, Mapping, Sequence
+from typing import Iterable, List, Mapping, Optional, Sequence, Set, Tuple, TYPE_CHECKING, Union
from tableauserverclient.helpers.headers import fix_filename
from tableauserverclient.server.query import QuerySet
@@ -23,7 +22,7 @@
from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint
from tableauserverclient.server.endpoint.resource_tagger import TaggingMixin
-from tableauserverclient.config import ALLOWED_FILE_EXTENSIONS, BYTES_PER_MB, config
+from tableauserverclient.config import ALLOWED_FILE_EXTENSIONS, FILESIZE_LIMIT_MB, BYTES_PER_MB, config
from tableauserverclient.filesys_helpers import (
make_download_path,
get_file_type,
@@ -58,7 +57,7 @@
class Datasources(QuerysetEndpoint[DatasourceItem], TaggingMixin[DatasourceItem]):
def __init__(self, parent_srv: "Server") -> None:
- super().__init__(parent_srv)
+ super(Datasources, self).__init__(parent_srv)
self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl)
self._data_quality_warnings = _DataQualityWarningEndpoint(self.parent_srv, "datasource")
@@ -66,11 +65,11 @@ def __init__(self, parent_srv: "Server") -> None:
@property
def baseurl(self) -> str:
- return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/datasources"
+ return "{0}/sites/{1}/datasources".format(self.parent_srv.baseurl, self.parent_srv.site_id)
# 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)
@@ -84,8 +83,8 @@ def get_by_id(self, datasource_id: str) -> DatasourceItem:
if not datasource_id:
error = "Datasource ID undefined."
raise ValueError(error)
- logger.info(f"Querying single datasource (ID: {datasource_id})")
- url = f"{self.baseurl}/{datasource_id}"
+ logger.info("Querying single datasource (ID: {0})".format(datasource_id))
+ url = "{0}/{1}".format(self.baseurl, datasource_id)
server_response = self.get_request(url)
return DatasourceItem.from_response(server_response.content, self.parent_srv.namespace)[0]
@@ -100,10 +99,10 @@ def connections_fetcher():
return self._get_datasource_connections(datasource_item)
datasource_item._set_connections(connections_fetcher)
- logger.info(f"Populated connections for datasource (ID: {datasource_item.id})")
+ logger.info("Populated connections for datasource (ID: {0})".format(datasource_item.id))
def _get_datasource_connections(self, datasource_item, req_options=None):
- url = f"{self.baseurl}/{datasource_item.id}/connections"
+ url = "{0}/{1}/connections".format(self.baseurl, datasource_item.id)
server_response = self.get_request(url, req_options)
connections = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace)
return connections
@@ -114,9 +113,9 @@ def delete(self, datasource_id: str) -> None:
if not datasource_id:
error = "Datasource ID undefined."
raise ValueError(error)
- url = f"{self.baseurl}/{datasource_id}"
+ url = "{0}/{1}".format(self.baseurl, datasource_id)
self.delete_request(url)
- logger.info(f"Deleted single datasource (ID: {datasource_id})")
+ logger.info("Deleted single datasource (ID: {0})".format(datasource_id))
# Download 1 datasource by id
@api(version="2.0")
@@ -153,11 +152,11 @@ def update(self, datasource_item: DatasourceItem) -> DatasourceItem:
self.update_tags(datasource_item)
# Update the datasource itself
- url = f"{self.baseurl}/{datasource_item.id}"
+ url = "{0}/{1}".format(self.baseurl, datasource_item.id)
update_req = RequestFactory.Datasource.update_req(datasource_item)
server_response = self.put_request(url, update_req)
- logger.info(f"Updated datasource item (ID: {datasource_item.id})")
+ logger.info("Updated datasource item (ID: {0})".format(datasource_item.id))
updated_datasource = copy.copy(datasource_item)
return updated_datasource._parse_common_elements(server_response.content, self.parent_srv.namespace)
@@ -166,7 +165,7 @@ def update(self, datasource_item: DatasourceItem) -> DatasourceItem:
def update_connection(
self, datasource_item: DatasourceItem, connection_item: ConnectionItem
) -> Optional[ConnectionItem]:
- url = f"{self.baseurl}/{datasource_item.id}/connections/{connection_item.id}"
+ url = "{0}/{1}/connections/{2}".format(self.baseurl, datasource_item.id, connection_item.id)
update_req = RequestFactory.Connection.update_req(connection_item)
server_response = self.put_request(url, update_req)
@@ -175,16 +174,18 @@ def update_connection(
return None
if len(connections) > 1:
- logger.debug(f"Multiple connections returned ({len(connections)})")
+ logger.debug("Multiple connections returned ({0})".format(len(connections)))
connection = list(filter(lambda x: x.id == connection_item.id, connections))[0]
- logger.info(f"Updated datasource item (ID: {datasource_item.id} & connection item {connection_item.id}")
+ logger.info(
+ "Updated datasource item (ID: {0} & connection item {1}".format(datasource_item.id, connection_item.id)
+ )
return connection
@api(version="2.8")
def refresh(self, datasource_item: DatasourceItem) -> JobItem:
id_ = getattr(datasource_item, "id", datasource_item)
- url = f"{self.baseurl}/{id_}/refresh"
+ url = "{0}/{1}/refresh".format(self.baseurl, id_)
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]
@@ -193,7 +194,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 = f"{self.baseurl}/{id_}/createExtract?encrypt={encrypt}"
+ url = "{0}/{1}/createExtract?encrypt={2}".format(self.baseurl, id_, 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]
@@ -202,7 +203,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 = f"{self.baseurl}/{id_}/deleteExtract"
+ url = "{0}/{1}/deleteExtract".format(self.baseurl, id_)
empty_req = RequestFactory.Empty.empty_req()
self.post_request(url, empty_req)
@@ -222,12 +223,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 OSError(error)
+ raise IOError(error)
filename = os.path.basename(file)
file_extension = os.path.splitext(filename)[1][1:]
file_size = os.path.getsize(file)
- logger.debug(f"Publishing file `{filename}`, size `{file_size}`")
+ logger.debug("Publishing file `{}`, size `{}`".format(filename, file_size))
# If name is not defined, grab the name from the file to publish
if not datasource_item.name:
datasource_item.name = os.path.splitext(filename)[0]
@@ -246,10 +247,10 @@ def publish(
elif file_type == "xml":
file_extension = "tds"
else:
- error = f"Unsupported file type {file_type}"
+ error = "Unsupported file type {}".format(file_type)
raise ValueError(error)
- filename = f"{datasource_item.name}.{file_extension}"
+ filename = "{}.{}".format(datasource_item.name, file_extension)
file_size = get_file_object_size(file)
else:
@@ -260,27 +261,27 @@ def publish(
raise ValueError(error)
# Construct the url with the defined mode
- url = f"{self.baseurl}?datasourceType={file_extension}"
+ url = "{0}?datasourceType={1}".format(self.baseurl, file_extension)
if mode == self.parent_srv.PublishMode.Overwrite or mode == self.parent_srv.PublishMode.Append:
- url += f"&{mode.lower()}=true"
+ url += "&{0}=true".format(mode.lower())
if as_job:
- url += "&{}=true".format("asJob")
+ url += "&{0}=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:
+ if file_size >= FILESIZE_LIMIT_MB * BYTES_PER_MB:
logger.info(
"Publishing {} to server with chunking method (datasource over {}MB, chunk size {}MB)".format(
- filename, config.FILESIZE_LIMIT_MB, config.CHUNK_SIZE_MB
+ filename, FILESIZE_LIMIT_MB, config.CHUNK_SIZE_MB
)
)
upload_session_id = self.parent_srv.fileuploads.upload(file)
- url = f"{url}&uploadSessionId={upload_session_id}"
+ url = "{0}&uploadSessionId={1}".format(url, upload_session_id)
xml_request, content_type = RequestFactory.Datasource.publish_req_chunked(
datasource_item, connection_credentials, connections
)
else:
- logger.info(f"Publishing {filename} to server")
+ logger.info("Publishing {0} to server".format(filename))
if isinstance(file, (Path, str)):
with open(file, "rb") as f:
@@ -308,11 +309,11 @@ def publish(
if as_job:
new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0]
- logger.info(f"Published {filename} (JOB_ID: {new_job.id}")
+ logger.info("Published {0} (JOB_ID: {1}".format(filename, new_job.id))
return new_job
else:
new_datasource = DatasourceItem.from_response(server_response.content, self.parent_srv.namespace)[0]
- logger.info(f"Published {filename} (ID: {new_datasource.id})")
+ logger.info("Published {0} (ID: {1})".format(filename, new_datasource.id))
return new_datasource
@api(version="3.13")
@@ -326,23 +327,23 @@ def update_hyper_data(
) -> JobItem:
if isinstance(datasource_or_connection_item, DatasourceItem):
datasource_id = datasource_or_connection_item.id
- url = f"{self.baseurl}/{datasource_id}/data"
+ url = "{0}/{1}/data".format(self.baseurl, datasource_id)
elif isinstance(datasource_or_connection_item, ConnectionItem):
datasource_id = datasource_or_connection_item.datasource_id
connection_id = datasource_or_connection_item.id
- url = f"{self.baseurl}/{datasource_id}/connections/{connection_id}/data"
+ url = "{0}/{1}/connections/{2}/data".format(self.baseurl, datasource_id, connection_id)
else:
assert isinstance(datasource_or_connection_item, str)
- url = f"{self.baseurl}/{datasource_or_connection_item}/data"
+ url = "{0}/{1}/data".format(self.baseurl, datasource_or_connection_item)
if payload is not None:
if not os.path.isfile(payload):
error = "File path does not lead to an existing file."
- raise OSError(error)
+ raise IOError(error)
- logger.info(f"Uploading {payload} to server with chunking method for Update job")
+ logger.info("Uploading {0} to server with chunking method for Update job".format(payload))
upload_session_id = self.parent_srv.fileuploads.upload(payload)
- url = f"{url}?uploadSessionId={upload_session_id}"
+ url = "{0}?uploadSessionId={1}".format(url, upload_session_id)
json_request = json.dumps({"actions": actions})
parameters = {"headers": {"requestid": request_id}}
@@ -355,7 +356,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")
@@ -389,12 +390,12 @@ def revisions_fetcher():
return self._get_datasource_revisions(datasource_item)
datasource_item._set_revisions(revisions_fetcher)
- logger.info(f"Populated revisions for datasource (ID: {datasource_item.id})")
+ logger.info("Populated revisions for datasource (ID: {0})".format(datasource_item.id))
def _get_datasource_revisions(
self, datasource_item: DatasourceItem, req_options: Optional["RequestOptions"] = None
- ) -> list[RevisionItem]:
- url = f"{self.baseurl}/{datasource_item.id}/revisions"
+ ) -> List[RevisionItem]:
+ url = "{0}/{1}/revisions".format(self.baseurl, datasource_item.id)
server_response = self.get_request(url, req_options)
revisions = RevisionItem.from_response(server_response.content, self.parent_srv.namespace, datasource_item)
return revisions
@@ -412,9 +413,9 @@ def download_revision(
error = "Datasource ID undefined."
raise ValueError(error)
if revision_number is None:
- url = f"{self.baseurl}/{datasource_id}/content"
+ url = "{0}/{1}/content".format(self.baseurl, datasource_id)
else:
- url = f"{self.baseurl}/{datasource_id}/revisions/{revision_number}/content"
+ url = "{0}/{1}/revisions/{2}/content".format(self.baseurl, datasource_id, revision_number)
if not include_extract:
url += "?includeExtract=False"
@@ -436,7 +437,9 @@ def download_revision(
f.write(chunk)
return_path = os.path.abspath(download_path)
- logger.info(f"Downloaded datasource revision {revision_number} to {return_path} (ID: {datasource_id})")
+ logger.info(
+ "Downloaded datasource revision {0} to {1} (ID: {2})".format(revision_number, return_path, datasource_id)
+ )
return return_path
@api(version="2.3")
@@ -446,17 +449,19 @@ 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(f"Deleted single datasource revision (ID: {datasource_id}) (Revision: {revision_number})")
+ logger.info(
+ "Deleted single datasource revision (ID: {0}) (Revision: {1})".format(datasource_id, 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 499324e8e..19112d713 100644
--- a/tableauserverclient/server/endpoint/default_permissions_endpoint.py
+++ b/tableauserverclient/server/endpoint/default_permissions_endpoint.py
@@ -4,8 +4,7 @@
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, Optional, Union
-from collections.abc import Sequence
+from typing import TYPE_CHECKING, Callable, List, Optional, Sequence, Union
if TYPE_CHECKING:
from ..server import Server
@@ -26,7 +25,7 @@ class _DefaultPermissionsEndpoint(Endpoint):
"""
def __init__(self, parent_srv: "Server", owner_baseurl: Callable[[], str]) -> None:
- super().__init__(parent_srv)
+ super(_DefaultPermissionsEndpoint, self).__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.
@@ -34,25 +33,23 @@ def __init__(self, parent_srv: "Server", owner_baseurl: Callable[[], str]) -> No
self.owner_baseurl = owner_baseurl
def __str__(self):
- return f""
+ return "".format(self.owner_baseurl())
__repr__ = __str__
def update_default_permissions(
- self, resource: BaseItem, permissions: Sequence[PermissionsRule], content_type: Union[Resource, str]
- ) -> list[PermissionsRule]:
- url = f"{self.owner_baseurl()}/{resource.id}/default-permissions/{plural_type(content_type)}"
+ 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))
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(f"Updated default {content_type} permissions for resource {resource.id}")
+ logger.info("Updated default {} permissions for resource {}".format(content_type, resource.id))
logger.info(permissions)
return permissions
- def delete_default_permission(
- self, resource: BaseItem, rule: PermissionsRule, content_type: Union[Resource, str]
- ) -> None:
+ def delete_default_permission(self, resource: BaseItem, rule: PermissionsRule, content_type: Resource) -> None:
for capability, mode in rule.capabilities.items():
# Made readability better but line is too long, will make this look better
url = (
@@ -68,27 +65,29 @@ def delete_default_permission(
)
)
- logger.debug(f"Removing {mode} permission for capability {capability}")
+ logger.debug("Removing {0} permission for capability {1}".format(mode, capability))
self.delete_request(url)
- logger.info(f"Deleted permission for {rule.grantee.tag_name} {rule.grantee.id} item {resource.id}")
+ logger.info(
+ "Deleted permission for {0} {1} item {2}".format(rule.grantee.tag_name, rule.grantee.id, resource.id)
+ )
- def populate_default_permissions(self, item: BaseItem, content_type: Union[Resource, str]) -> None:
+ 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(f"Populated default {content_type} permissions for item (ID: {item.id})")
+ logger.info("Populated default {0} permissions for item (ID: {1})".format(content_type, item.id))
def _get_default_permissions(
- self, item: BaseItem, content_type: Union[Resource, str], req_options: Optional["RequestOptions"] = None
- ) -> list[PermissionsRule]:
- url = f"{self.owner_baseurl()}/{item.id}/default-permissions/{plural_type(content_type)}"
+ 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))
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 90e31483b..5296523ee 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().__init__(parent_srv)
+ super(_DataQualityWarningEndpoint, self).__init__(parent_srv)
self.resource_type = resource_type
@property
def baseurl(self):
- return "{}/sites/{}/dataQualityWarnings/{}".format(
+ return "{0}/sites/{1}/dataQualityWarnings/{2}".format(
self.parent_srv.baseurl, self.parent_srv.site_id, self.resource_type
)
def add(self, resource, warning):
- url = f"{self.baseurl}/{resource.id}"
+ url = "{baseurl}/{content_luid}".format(baseurl=self.baseurl, content_luid=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(f"Added dqw for resource {resource.id}")
+ logger.info("Added dqw for resource {0}".format(resource.id))
return warnings
def update(self, resource, warning):
- url = f"{self.baseurl}/{resource.id}"
+ url = "{baseurl}/{content_luid}".format(baseurl=self.baseurl, content_luid=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(f"Added dqw for resource {resource.id}")
+ logger.info("Added dqw for resource {0}".format(resource.id))
return warnings
def clear(self, resource):
- url = f"{self.baseurl}/{resource.id}"
+ url = "{baseurl}/{content_luid}".format(baseurl=self.baseurl, content_luid=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(f"Populated permissions for item (ID: {item.id})")
+ logger.info("Populated permissions for item (ID: {0})".format(item.id))
def _get_data_quality_warnings(self, item, req_options=None):
- url = f"{self.baseurl}/{item.id}"
+ url = "{baseurl}/{content_luid}".format(baseurl=self.baseurl, content_luid=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 9e1160705..be0602df5 100644
--- a/tableauserverclient/server/endpoint/endpoint.py
+++ b/tableauserverclient/server/endpoint/endpoint.py
@@ -8,9 +8,12 @@
from typing import (
Any,
Callable,
+ Dict,
Generic,
+ List,
Optional,
TYPE_CHECKING,
+ Tuple,
TypeVar,
Union,
)
@@ -19,7 +22,6 @@
from tableauserverclient.server.request_options import RequestOptions
from tableauserverclient.server.endpoint.exceptions import (
- FailedSignInError,
ServerResponseError,
InternalServerError,
NonXMLResponseError,
@@ -54,7 +56,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:
@@ -80,7 +82,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] = f"Tableau Server Client/{_client_version}"
+ parameters["headers"][USER_AGENT_HEADER] = "Tableau Server Client/{}".format(_client_version)
# result: parameters["headers"]["User-Agent"] is set
# return explicitly for testing only
@@ -88,12 +90,12 @@ def set_user_agent(parameters):
def _blocking_request(self, method, url, parameters={}) -> Optional[Union["Response", Exception]]:
response = None
- logger.debug(f"[{datetime.timestamp()}] Begin blocking request to {url}")
+ logger.debug("[{}] Begin blocking request to {}".format(datetime.timestamp(), url))
try:
response = method(url, **parameters)
- logger.debug(f"[{datetime.timestamp()}] Call finished")
+ logger.debug("[{}] Call finished".format(datetime.timestamp()))
except Exception as e:
- logger.debug(f"Error making request to server: {e}")
+ logger.debug("Error making request to server: {}".format(e))
raise e
return response
@@ -109,13 +111,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(f"request method {method.__name__}, url: {url}")
+ logger.debug("request method {}, url: {}".format(method.__name__, url))
if content:
redacted = helpers.strings.redact_xml(content[:200])
# this needs to be under a trace or something, it's a LOT
@@ -127,21 +129,21 @@ def _make_request(
server_response: Optional[Union["Response", Exception]] = self.send_request_while_show_progress_threaded(
method, url, parameters, request_timeout
)
- logger.debug(f"[{datetime.timestamp()}] Async request returned: received {server_response}")
+ logger.debug("[{}] Async request returned: received {}".format(datetime.timestamp(), server_response))
# is this blocking retry really necessary? I guess if it was just the threading messing it up?
if server_response is None:
logger.debug(server_response)
- logger.debug(f"[{datetime.timestamp()}] Async request failed: retrying")
+ logger.debug("[{}] Async request failed: retrying".format(datetime.timestamp()))
server_response = self._blocking_request(method, url, parameters)
if server_response is None:
- logger.debug(f"[{datetime.timestamp()}] Request failed")
+ logger.debug("[{}] Request failed".format(datetime.timestamp()))
raise RuntimeError
if isinstance(server_response, Exception):
raise server_response
self._check_status(server_response, url)
loggable_response = self.log_response_safely(server_response)
- logger.debug(f"Server response from {url}")
+ logger.debug("Server response from {0}".format(url))
# uncomment the following to log full responses in debug mode
# BE CAREFUL WHEN SHARING THESE RESULTS - MAY CONTAIN YOUR SENSITIVE DATA
# logger.debug(loggable_response)
@@ -152,16 +154,16 @@ def _make_request(
return server_response
def _check_status(self, server_response: "Response", url: Optional[str] = None):
- logger.debug(f"Response status: {server_response}")
+ logger.debug("Response status: {}".format(server_response))
if not hasattr(server_response, "status_code"):
- raise OSError("Response is not a http response?")
+ raise EnvironmentError("Response is not a http response?")
if server_response.status_code >= 500:
raise InternalServerError(server_response, url)
elif server_response.status_code not in Success_codes:
try:
if server_response.status_code == 401:
# TODO: catch this in server.py and attempt to sign in again, in case it's a session expiry
- raise FailedSignInError.from_response(server_response.content, self.parent_srv.namespace, url)
+ raise NotSignedInError(server_response.content, url)
raise ServerResponseError.from_response(server_response.content, self.parent_srv.namespace, url)
except ParseError:
@@ -181,9 +183,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 = f"Content type `{content_type}`"
+ loggable_response = "Content type `{}`".format(content_type)
if content_type == "application/octet-stream":
- loggable_response = f"A stream of type {content_type} [Truncated File Contents]"
+ loggable_response = "A stream of type {} [Truncated File Contents]".format(content_type)
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
@@ -311,7 +313,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 = f"{p!r} not available in {server_ver}, it will be ignored. Added in {min_ver}"
+ error = "{!r} not available in {}, it will be ignored. Added in {}".format(p, server_ver, min_ver)
warnings.warn(error)
return func(self, *args, **kwargs)
@@ -351,5 +353,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 77332da3e..9dfd38da6 100644
--- a/tableauserverclient/server/endpoint/exceptions.py
+++ b/tableauserverclient/server/endpoint/exceptions.py
@@ -1,31 +1,24 @@
from defusedxml.ElementTree import fromstring
-from typing import Mapping, Optional, TypeVar
-
-
-def split_pascal_case(s: str) -> str:
- return "".join([f" {c}" if c.isupper() else c for c in s]).strip()
+from typing import Optional
class TableauError(Exception):
pass
-T = TypeVar("T")
-
-
-class XMLError(TableauError):
- def __init__(self, code: str, summary: str, detail: str, url: Optional[str] = None) -> None:
+class ServerResponseError(TableauError):
+ def __init__(self, code, summary, detail, url=None):
self.code = code
self.summary = summary
self.detail = detail
self.url = url
- super().__init__(str(self))
+ super(ServerResponseError, self).__init__(str(self))
def __str__(self):
- return f"\n\n\t{self.code}: {self.summary}\n\t\t{self.detail}"
+ return "\n\n\t{0}: {1}\n\t\t{2}".format(self.code, self.summary, self.detail)
@classmethod
- def from_response(cls, resp, ns, url):
+ def from_response(cls, resp, ns, url=None):
# Check elements exist before .text
parsed_response = fromstring(resp)
try:
@@ -40,10 +33,6 @@ def from_response(cls, resp, ns, url):
return error_response
-class ServerResponseError(XMLError):
- pass
-
-
class InternalServerError(TableauError):
def __init__(self, server_response, request_url: Optional[str] = None):
self.code = server_response.status_code
@@ -51,7 +40,7 @@ def __init__(self, server_response, request_url: Optional[str] = None):
self.url = request_url or "server"
def __str__(self):
- return f"\n\nInternal error {self.code} at {self.url}\n{self.content}"
+ return "\n\nInternal error {0} at {1}\n{2}".format(self.code, self.url, self.content)
class MissingRequiredFieldError(TableauError):
@@ -62,11 +51,6 @@ class NotSignedInError(TableauError):
pass
-class FailedSignInError(XMLError, NotSignedInError):
- def __str__(self):
- return f"{split_pascal_case(self.__class__.__name__)}: {super().__str__()}"
-
-
class ItemTypeNotAllowed(TableauError):
pass
diff --git a/tableauserverclient/server/endpoint/favorites_endpoint.py b/tableauserverclient/server/endpoint/favorites_endpoint.py
index 8330e6d2c..5f298f37e 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 f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/favorites"
+ return "{0}/sites/{1}/favorites".format(self.parent_srv.baseurl, self.parent_srv.site_id)
# Gets all favorites
@api(version="2.5")
def get(self, user_item: UserItem, req_options: Optional[RequestOptions] = None) -> None:
- logger.info(f"Querying all favorites for user {user_item.name}")
- url = f"{self.baseurl}/{user_item.id}"
+ logger.info("Querying all favorites for user {0}".format(user_item.name))
+ url = "{0}/{1}".format(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 = f"{self.baseurl}/{user_item.id}"
+ url = "{0}/{1}".format(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(f"Favorited {item.name} for user (ID: {user_item.id})")
+ logger.info("Favorited {0} for user (ID: {1})".format(item.name, user_item.id))
return server_response
@api(version="2.0")
def add_favorite_workbook(self, user_item: UserItem, workbook_item: WorkbookItem) -> None:
- url = f"{self.baseurl}/{user_item.id}"
+ url = "{0}/{1}".format(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(f"Favorited {workbook_item.name} for user (ID: {user_item.id})")
+ logger.info("Favorited {0} for user (ID: {1})".format(workbook_item.name, user_item.id))
@api(version="2.0")
def add_favorite_view(self, user_item: UserItem, view_item: ViewItem) -> None:
- url = f"{self.baseurl}/{user_item.id}"
+ url = "{0}/{1}".format(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(f"Favorited {view_item.name} for user (ID: {user_item.id})")
+ logger.info("Favorited {0} for user (ID: {1})".format(view_item.name, user_item.id))
@api(version="2.3")
def add_favorite_datasource(self, user_item: UserItem, datasource_item: DatasourceItem) -> None:
- url = f"{self.baseurl}/{user_item.id}"
+ url = "{0}/{1}".format(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(f"Favorited {datasource_item.name} for user (ID: {user_item.id})")
+ logger.info("Favorited {0} for user (ID: {1})".format(datasource_item.name, user_item.id))
@api(version="3.1")
def add_favorite_project(self, user_item: UserItem, project_item: ProjectItem) -> None:
- url = f"{self.baseurl}/{user_item.id}"
+ url = "{0}/{1}".format(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(f"Favorited {project_item.name} for user (ID: {user_item.id})")
+ logger.info("Favorited {0} for user (ID: {1})".format(project_item.name, user_item.id))
@api(version="3.3")
def add_favorite_flow(self, user_item: UserItem, flow_item: FlowItem) -> None:
- url = f"{self.baseurl}/{user_item.id}"
+ url = "{0}/{1}".format(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(f"Favorited {flow_item.name} for user (ID: {user_item.id})")
+ logger.info("Favorited {0} for user (ID: {1})".format(flow_item.name, user_item.id))
@api(version="3.3")
def add_favorite_metric(self, user_item: UserItem, metric_item: MetricItem) -> None:
- url = f"{self.baseurl}/{user_item.id}"
+ url = "{0}/{1}".format(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(f"Favorited metric {metric_item.name} for user (ID: {user_item.id})")
+ logger.info("Favorited metric {0} for user (ID: {1})".format(metric_item.name, 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 = 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})")
+ 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))
self.delete_request(url)
@api(version="2.0")
def delete_favorite_workbook(self, user_item: UserItem, workbook_item: WorkbookItem) -> None:
- 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})")
+ 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))
self.delete_request(url)
@api(version="2.0")
def delete_favorite_view(self, user_item: UserItem, view_item: ViewItem) -> None:
- 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})")
+ 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))
self.delete_request(url)
@api(version="2.3")
def delete_favorite_datasource(self, user_item: UserItem, datasource_item: DatasourceItem) -> None:
- 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})")
+ 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))
self.delete_request(url)
@api(version="3.1")
def delete_favorite_project(self, user_item: UserItem, project_item: ProjectItem) -> None:
- 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})")
+ 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))
self.delete_request(url)
@api(version="3.3")
def delete_favorite_flow(self, user_item: UserItem, flow_item: FlowItem) -> None:
- 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})")
+ 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))
self.delete_request(url)
@api(version="3.15")
def delete_favorite_metric(self, user_item: UserItem, metric_item: MetricItem) -> None:
- 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})")
+ 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))
self.delete_request(url)
diff --git a/tableauserverclient/server/endpoint/fileuploads_endpoint.py b/tableauserverclient/server/endpoint/fileuploads_endpoint.py
index 1ae10e72d..0d30797c1 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().__init__(parent_srv)
+ super(Fileuploads, self).__init__(parent_srv)
@property
def baseurl(self):
- return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/fileUploads"
+ return "{0}/sites/{1}/fileUploads".format(self.parent_srv.baseurl, self.parent_srv.site_id)
@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(f"Initiated file upload session (ID: {upload_id})")
+ logger.info("Initiated file upload session (ID: {0})".format(upload_id))
return upload_id
@api(version="2.0")
def append(self, upload_id, data, content_type):
- url = f"{self.baseurl}/{upload_id}"
+ url = "{0}/{1}".format(self.baseurl, upload_id)
server_response = self.put_request(url, data, content_type)
- logger.info(f"Uploading a chunk to session (ID: {upload_id})")
+ logger.info("Uploading a chunk to session (ID: {0})".format(upload_id))
return FileuploadItem.from_response(server_response.content, self.parent_srv.namespace)
def _read_chunks(self, file):
@@ -52,10 +52,12 @@ def _read_chunks(self, file):
def upload(self, file):
upload_id = self.initiate()
for chunk in self._read_chunks(file):
- logger.debug(f"{datetime.timestamp()} processing chunk...")
+ logger.debug("{} processing chunk...".format(datetime.timestamp()))
request, content_type = RequestFactory.Fileupload.chunk_req(chunk)
- logger.debug(f"{datetime.timestamp()} created chunk request")
+ logger.debug("{} created chunk request".format(datetime.timestamp()))
fileupload_item = self.append(upload_id, request, content_type)
- logger.info(f"\t{datetime.timestamp()} Published {(fileupload_item.file_size / BYTES_PER_MB)}MB")
- logger.info(f"File upload finished (ID: {upload_id})")
+ logger.info(
+ "\t{0} Published {1}MB".format(datetime.timestamp(), (fileupload_item.file_size / BYTES_PER_MB))
+ )
+ logger.info("File upload finished (ID: {0})".format(upload_id))
return upload_id
diff --git a/tableauserverclient/server/endpoint/flow_runs_endpoint.py b/tableauserverclient/server/endpoint/flow_runs_endpoint.py
index 2c3bb84bc..c339a0645 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 Optional, TYPE_CHECKING, Union
+from typing import List, Optional, Tuple, TYPE_CHECKING
from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api
from tableauserverclient.server.endpoint.exceptions import FlowRunFailedException, FlowRunCancelledException
-from tableauserverclient.models import FlowRunItem
+from tableauserverclient.models import FlowRunItem, PaginationItem
from tableauserverclient.exponential_backoff import ExponentialBackoffTimer
from tableauserverclient.helpers.logging import logger
@@ -16,24 +16,22 @@
class FlowRuns(QuerysetEndpoint[FlowRunItem]):
def __init__(self, parent_srv: "Server") -> None:
- super().__init__(parent_srv)
+ super(FlowRuns, self).__init__(parent_srv)
return None
@property
def baseurl(self) -> str:
- return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/flows/runs"
+ return "{0}/sites/{1}/flows/runs".format(self.parent_srv.baseurl, self.parent_srv.site_id)
# Get all flows
@api(version="3.10")
- # 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]
+ def get(self, req_options: Optional["RequestOptions"] = None) -> Tuple[List[FlowRunItem], PaginationItem]:
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
+ return all_flow_run_items, pagination_item
# Get 1 flow by id
@api(version="3.10")
@@ -41,21 +39,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(f"Querying single flow (ID: {flow_run_id})")
- url = f"{self.baseurl}/{flow_run_id}"
+ logger.info("Querying single flow (ID: {0})".format(flow_run_id))
+ url = "{0}/{1}".format(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: Union[str, FlowRunItem]) -> None:
+ def cancel(self, flow_run_id: str) -> None:
if not flow_run_id:
error = "Flow ID undefined."
raise ValueError(error)
id_ = getattr(flow_run_id, "id", flow_run_id)
- url = f"{self.baseurl}/{id_}"
+ url = "{0}/{1}".format(self.baseurl, id_)
self.put_request(url)
- logger.info(f"Deleted single flow (ID: {id_})")
+ logger.info("Deleted single flow (ID: {0})".format(id_))
@api(version="3.10")
def wait_for_job(self, flow_run_id: str, *, timeout: Optional[int] = None) -> FlowRunItem:
@@ -71,7 +69,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(f"FlowRun {flow_run_id} Completed: Status: {flow_run.status}")
+ logger.info("FlowRun {} Completed: Status: {}".format(flow_run_id, 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 9e21661e6..eea3f9710 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 TYPE_CHECKING
+from typing import List, Optional, Tuple, 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 f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/tasks/flows"
+ return "{0}/sites/{1}/tasks/flows".format(self.parent_srv.baseurl, self.parent_srv.site_id)
@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 7eb5dc3ba..53d072f50 100644
--- a/tableauserverclient/server/endpoint/flows_endpoint.py
+++ b/tableauserverclient/server/endpoint/flows_endpoint.py
@@ -5,8 +5,7 @@
import os
from contextlib import closing
from pathlib import Path
-from typing import Optional, TYPE_CHECKING, Union
-from collections.abc import Iterable
+from typing import Iterable, List, Optional, TYPE_CHECKING, Tuple, Union
from tableauserverclient.helpers.headers import fix_filename
@@ -54,18 +53,18 @@
class Flows(QuerysetEndpoint[FlowItem], TaggingMixin[FlowItem]):
def __init__(self, parent_srv):
- super().__init__(parent_srv)
+ super(Flows, self).__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 f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/flows"
+ return "{0}/sites/{1}/flows".format(self.parent_srv.baseurl, self.parent_srv.site_id)
# 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)
@@ -79,8 +78,8 @@ def get_by_id(self, flow_id: str) -> FlowItem:
if not flow_id:
error = "Flow ID undefined."
raise ValueError(error)
- logger.info(f"Querying single flow (ID: {flow_id})")
- url = f"{self.baseurl}/{flow_id}"
+ logger.info("Querying single flow (ID: {0})".format(flow_id))
+ url = "{0}/{1}".format(self.baseurl, flow_id)
server_response = self.get_request(url)
return FlowItem.from_response(server_response.content, self.parent_srv.namespace)[0]
@@ -95,10 +94,10 @@ def connections_fetcher():
return self._get_flow_connections(flow_item)
flow_item._set_connections(connections_fetcher)
- logger.info(f"Populated connections for flow (ID: {flow_item.id})")
+ logger.info("Populated connections for flow (ID: {0})".format(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"
+ def _get_flow_connections(self, flow_item, req_options: Optional["RequestOptions"] = None) -> List[ConnectionItem]:
+ url = "{0}/{1}/connections".format(self.baseurl, flow_item.id)
server_response = self.get_request(url, req_options)
connections = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace)
return connections
@@ -109,9 +108,9 @@ def delete(self, flow_id: str) -> None:
if not flow_id:
error = "Flow ID undefined."
raise ValueError(error)
- url = f"{self.baseurl}/{flow_id}"
+ url = "{0}/{1}".format(self.baseurl, flow_id)
self.delete_request(url)
- logger.info(f"Deleted single flow (ID: {flow_id})")
+ logger.info("Deleted single flow (ID: {0})".format(flow_id))
# Download 1 flow by id
@api(version="3.3")
@@ -119,7 +118,7 @@ def download(self, flow_id: str, filepath: Optional[PathOrFileW] = None) -> Path
if not flow_id:
error = "Flow ID undefined."
raise ValueError(error)
- url = f"{self.baseurl}/{flow_id}/content"
+ url = "{0}/{1}/content".format(self.baseurl, flow_id)
with closing(self.get_request(url, parameters={"stream": True})) as server_response:
m = Message()
@@ -138,7 +137,7 @@ def download(self, flow_id: str, filepath: Optional[PathOrFileW] = None) -> Path
f.write(chunk)
return_path = os.path.abspath(download_path)
- logger.info(f"Downloaded flow to {return_path} (ID: {flow_id})")
+ logger.info("Downloaded flow to {0} (ID: {1})".format(return_path, flow_id))
return return_path
# Update flow
@@ -151,28 +150,28 @@ def update(self, flow_item: FlowItem) -> FlowItem:
self._resource_tagger.update_tags(self.baseurl, flow_item)
# Update the flow itself
- url = f"{self.baseurl}/{flow_item.id}"
+ url = "{0}/{1}".format(self.baseurl, flow_item.id)
update_req = RequestFactory.Flow.update_req(flow_item)
server_response = self.put_request(url, update_req)
- logger.info(f"Updated flow item (ID: {flow_item.id})")
+ logger.info("Updated flow item (ID: {0})".format(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 = f"{self.baseurl}/{flow_item.id}/connections/{connection_item.id}"
+ url = "{0}/{1}/connections/{2}".format(self.baseurl, flow_item.id, connection_item.id)
update_req = RequestFactory.Connection.update_req(connection_item)
server_response = self.put_request(url, update_req)
connection = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace)[0]
- logger.info(f"Updated flow item (ID: {flow_item.id} & connection item {connection_item.id}")
+ logger.info("Updated flow item (ID: {0} & connection item {1}".format(flow_item.id, connection_item.id))
return connection
@api(version="3.3")
def refresh(self, flow_item: FlowItem) -> JobItem:
- url = f"{self.baseurl}/{flow_item.id}/run"
+ url = "{0}/{1}/run".format(self.baseurl, flow_item.id)
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]
@@ -181,7 +180,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."
@@ -190,7 +189,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 OSError(error)
+ raise IOError(error)
filename = os.path.basename(file)
file_extension = os.path.splitext(filename)[1][1:]
@@ -214,30 +213,30 @@ def publish(
elif file_type == "xml":
file_extension = "tfl"
else:
- error = f"Unsupported file type {file_type}!"
+ error = "Unsupported file type {}!".format(file_type)
raise ValueError(error)
# Generate filename for file object.
# This is needed when publishing the flow in a single request
- filename = f"{flow_item.name}.{file_extension}"
+ filename = "{}.{}".format(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 = f"{self.baseurl}?flowType={file_extension}"
+ url = "{0}?flowType={1}".format(self.baseurl, file_extension)
if mode == self.parent_srv.PublishMode.Overwrite or mode == self.parent_srv.PublishMode.Append:
- url += f"&{mode.lower()}=true"
+ url += "&{0}=true".format(mode.lower())
# Determine if chunking is required (64MB is the limit for single upload method)
if file_size >= FILESIZE_LIMIT:
- logger.info(f"Publishing {filename} to server with chunking method (flow over 64MB)")
+ logger.info("Publishing {0} to server with chunking method (flow over 64MB)".format(filename))
upload_session_id = self.parent_srv.fileuploads.upload(file)
- url = f"{url}&uploadSessionId={upload_session_id}"
+ url = "{0}&uploadSessionId={1}".format(url, upload_session_id)
xml_request, content_type = RequestFactory.Flow.publish_req_chunked(flow_item, connections)
else:
- logger.info(f"Publishing {filename} to server")
+ logger.info("Publishing {0} to server".format(filename))
if isinstance(file, (str, Path)):
with open(file, "rb") as f:
@@ -260,7 +259,7 @@ def publish(
raise err
else:
new_flow = FlowItem.from_response(server_response.content, self.parent_srv.namespace)[0]
- logger.info(f"Published {filename} (ID: {new_flow.id})")
+ logger.info("Published {0} (ID: {1})".format(filename, new_flow.id))
return new_flow
@api(version="3.3")
@@ -295,7 +294,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 c512b011b..8acf31692 100644
--- a/tableauserverclient/server/endpoint/groups_endpoint.py
+++ b/tableauserverclient/server/endpoint/groups_endpoint.py
@@ -8,8 +8,7 @@
from tableauserverclient.helpers.logging import logger
-from typing import Optional, TYPE_CHECKING, Union
-from collections.abc import Iterable
+from typing import Iterable, List, Optional, TYPE_CHECKING, Tuple, Union
from tableauserverclient.server.query import QuerySet
@@ -20,10 +19,10 @@
class Groups(QuerysetEndpoint[GroupItem]):
@property
def baseurl(self) -> str:
- return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/groups"
+ return "{0}/sites/{1}/groups".format(self.parent_srv.baseurl, self.parent_srv.site_id)
@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
@@ -51,12 +50,12 @@ def user_pager():
def _get_users_for_group(
self, group_item: GroupItem, req_options: Optional["RequestOptions"] = None
- ) -> tuple[list[UserItem], PaginationItem]:
- url = f"{self.baseurl}/{group_item.id}/users"
+ ) -> Tuple[List[UserItem], PaginationItem]:
+ url = "{0}/{1}/users".format(self.baseurl, group_item.id)
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(f"Populated users for group (ID: {group_item.id})")
+ logger.info("Populated users for group (ID: {0})".format(group_item.id))
return user_item, pagination_item
@api(version="2.0")
@@ -65,13 +64,13 @@ def delete(self, group_id: str) -> None:
if not group_id:
error = "Group ID undefined."
raise ValueError(error)
- url = f"{self.baseurl}/{group_id}"
+ url = "{0}/{1}".format(self.baseurl, group_id)
self.delete_request(url)
- logger.info(f"Deleted single group (ID: {group_id})")
+ logger.info("Deleted single group (ID: {0})".format(group_id))
@api(version="2.0")
def update(self, group_item: GroupItem, as_job: bool = False) -> Union[GroupItem, JobItem]:
- url = f"{self.baseurl}/{group_item.id}"
+ url = "{0}/{1}".format(self.baseurl, group_item.id)
if not group_item.id:
error = "Group item missing ID."
@@ -84,7 +83,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(f"Updated group item (ID: {group_item.id})")
+ logger.info("Updated group item (ID: {0})".format(group_item.id))
if as_job:
return JobItem.from_response(server_response.content, self.parent_srv.namespace)[0]
else:
@@ -119,9 +118,9 @@ def remove_user(self, group_item: GroupItem, user_id: str) -> None:
if not user_id:
error = "User ID undefined."
raise ValueError(error)
- url = f"{self.baseurl}/{group_item.id}/users/{user_id}"
+ url = "{0}/{1}/users/{2}".format(self.baseurl, group_item.id, user_id)
self.delete_request(url)
- logger.info(f"Removed user (id: {user_id}) from group (ID: {group_item.id})")
+ logger.info("Removed user (id: {0}) from group (ID: {1})".format(user_id, group_item.id))
@api(version="3.21")
def remove_users(self, group_item: GroupItem, users: Iterable[Union[str, UserItem]]) -> None:
@@ -133,7 +132,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(f"Removed users to group (ID: {group_item.id})")
+ logger.info("Removed users to group (ID: {0})".format(group_item.id))
return None
@api(version="2.0")
@@ -145,15 +144,15 @@ def add_user(self, group_item: GroupItem, user_id: str) -> UserItem:
if not user_id:
error = "User ID undefined."
raise ValueError(error)
- url = f"{self.baseurl}/{group_item.id}/users"
+ url = "{0}/{1}/users".format(self.baseurl, group_item.id)
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(f"Added user (id: {user_id}) to group (ID: {group_item.id})")
+ logger.info("Added user (id: {0}) to group (ID: {1})".format(user_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):
@@ -163,7 +162,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(f"Added users to group (ID: {group_item.id})")
+ logger.info("Added users to group (ID: {0})".format(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 c7f5ed0e5..06e7cc627 100644
--- a/tableauserverclient/server/endpoint/groupsets_endpoint.py
+++ b/tableauserverclient/server/endpoint/groupsets_endpoint.py
@@ -1,4 +1,4 @@
-from typing import Literal, Optional, TYPE_CHECKING, Union
+from typing import List, Literal, Optional, Tuple, 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 723d3dd38..ae8cf2633 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 Optional, Union
+from typing import List, Optional, Tuple, Union
class Jobs(QuerysetEndpoint[BackgroundJobItem]):
@property
def baseurl(self):
- return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/jobs"
+ return "{0}/sites/{1}/jobs".format(self.parent_srv.baseurl, self.parent_srv.site_id)
@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 = f"{self.baseurl}/{job_id}"
+ url = "{0}/{1}".format(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 = f"{self.baseurl}/{job_id}"
+ url = "{0}/{1}".format(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(f"Job {job_id} Completed: Finish Code: {job.finish_code} - Notes:{job.notes}")
+ logger.info("Job {} Completed: Finish Code: {} - Notes:{}".format(job_id, job.finish_code, 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 ede4d38e3..374130509 100644
--- a/tableauserverclient/server/endpoint/linked_tasks_endpoint.py
+++ b/tableauserverclient/server/endpoint/linked_tasks_endpoint.py
@@ -1,4 +1,4 @@
-from typing import Optional, Union
+from typing import List, Optional, Tuple, 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 e5dbcbcf8..38c3eebb6 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 f"{self.parent_srv.server_address}/api/metadata/graphql"
+ return "{0}/api/metadata/graphql".format(self.parent_srv.server_address)
@property
def control_baseurl(self):
- return f"{self.parent_srv.server_address}/api/metadata/v1/control"
+ return "{0}/api/metadata/v1/control".format(self.parent_srv.server_address)
@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 3fea1f5b6..ab1ec5852 100644
--- a/tableauserverclient/server/endpoint/metrics_endpoint.py
+++ b/tableauserverclient/server/endpoint/metrics_endpoint.py
@@ -8,7 +8,7 @@
import logging
-from typing import Optional, TYPE_CHECKING
+from typing import List, Optional, TYPE_CHECKING, Tuple
if TYPE_CHECKING:
from ..request_options import RequestOptions
@@ -20,18 +20,18 @@
class Metrics(QuerysetEndpoint[MetricItem]):
def __init__(self, parent_srv: "Server") -> None:
- super().__init__(parent_srv)
+ super(Metrics, self).__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 f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/metrics"
+ return "{0}/sites/{1}/metrics".format(self.parent_srv.baseurl, self.parent_srv.site_id)
# 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(f"Querying single metric (ID: {metric_id})")
- url = f"{self.baseurl}/{metric_id}"
+ logger.info("Querying single metric (ID: {0})".format(metric_id))
+ url = "{0}/{1}".format(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 = f"{self.baseurl}/{metric_id}"
+ url = "{0}/{1}".format(self.baseurl, metric_id)
self.delete_request(url)
- logger.info(f"Deleted single metric (ID: {metric_id})")
+ logger.info("Deleted single metric (ID: {0})".format(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 = f"{self.baseurl}/{metric_item.id}"
+ url = "{0}/{1}".format(self.baseurl, metric_item.id)
update_req = RequestFactory.Metric.update_req(metric_item)
server_response = self.put_request(url, update_req)
- logger.info(f"Updated metric item (ID: {metric_item.id})")
+ logger.info("Updated metric item (ID: {0})".format(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 10d420ff7..4433625f2 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, Optional, Union
+from typing import Callable, TYPE_CHECKING, List, 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().__init__(parent_srv)
+ super(_PermissionsEndpoint, self).__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 f""
+ return "".format(self.owner_baseurl)
- def update(self, resource: TableauItem, permissions: list[PermissionsRule]) -> list[PermissionsRule]:
- url = f"{self.owner_baseurl()}/{resource.id}/permissions"
+ def update(self, resource: TableauItem, permissions: List[PermissionsRule]) -> List[PermissionsRule]:
+ url = "{0}/{1}/permissions".format(self.owner_baseurl(), resource.id)
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(f"Updated permissions for resource {resource.id}: {permissions}")
+ logger.info("Updated permissions for resource {0}: {1}".format(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 = "{}/{}/permissions/{}/{}/{}/{}".format(
+ url = "{0}/{1}/permissions/{2}/{3}/{4}/{5}".format(
self.owner_baseurl(),
resource.id,
rule.grantee.tag_name + "s",
@@ -63,11 +63,13 @@ def delete(self, resource: TableauItem, rules: Union[PermissionsRule, list[Permi
mode,
)
- logger.debug(f"Removing {mode} permission for capability {capability}")
+ logger.debug("Removing {0} permission for capability {1}".format(mode, capability))
self.delete_request(url)
- logger.info(f"Deleted permission for {rule.grantee.tag_name} {rule.grantee.id} item {resource.id}")
+ logger.info(
+ "Deleted permission for {0} {1} item {2}".format(rule.grantee.tag_name, rule.grantee.id, resource.id)
+ )
def populate(self, item: TableauItem):
if not item.id:
@@ -78,12 +80,12 @@ def permission_fetcher():
return self._get_permissions(item)
item._set_permissions(permission_fetcher)
- logger.info(f"Populated permissions for item (ID: {item.id})")
+ logger.info("Populated permissions for item (ID: {0})".format(item.id))
def _get_permissions(self, item: TableauItem, req_options: Optional["RequestOptions"] = None):
- url = f"{self.owner_baseurl()}/{item.id}/permissions"
+ url = "{0}/{1}/permissions".format(self.owner_baseurl(), item.id)
server_response = self.get_request(url, req_options)
permissions = PermissionsRule.from_response(server_response.content, self.parent_srv.namespace)
- logger.info(f"Permissions for resource {item.id}: {permissions}")
+ logger.info("Permissions for resource {0}: {1}".format(item.id, permissions))
return permissions
diff --git a/tableauserverclient/server/endpoint/projects_endpoint.py b/tableauserverclient/server/endpoint/projects_endpoint.py
index 74bb865c7..565817e37 100644
--- a/tableauserverclient/server/endpoint/projects_endpoint.py
+++ b/tableauserverclient/server/endpoint/projects_endpoint.py
@@ -5,10 +5,9 @@
from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError
from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint
from tableauserverclient.server import RequestFactory, RequestOptions
-from tableauserverclient.models.permissions_item import PermissionsRule
from tableauserverclient.models import ProjectItem, PaginationItem, Resource
-from typing import Optional, TYPE_CHECKING
+from typing import List, Optional, Tuple, TYPE_CHECKING
from tableauserverclient.server.query import QuerySet
@@ -21,17 +20,17 @@
class Projects(QuerysetEndpoint[ProjectItem]):
def __init__(self, parent_srv: "Server") -> None:
- super().__init__(parent_srv)
+ super(Projects, self).__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 f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/projects"
+ return "{0}/sites/{1}/projects".format(self.parent_srv.baseurl, self.parent_srv.site_id)
@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)
@@ -44,9 +43,9 @@ def delete(self, project_id: str) -> None:
if not project_id:
error = "Project ID undefined."
raise ValueError(error)
- url = f"{self.baseurl}/{project_id}"
+ url = "{0}/{1}".format(self.baseurl, project_id)
self.delete_request(url)
- logger.info(f"Deleted single project (ID: {project_id})")
+ logger.info("Deleted single project (ID: {0})".format(project_id))
@api(version="2.0")
def update(self, project_item: ProjectItem, samples: bool = False) -> ProjectItem:
@@ -55,10 +54,10 @@ def update(self, project_item: ProjectItem, samples: bool = False) -> ProjectIte
raise MissingRequiredFieldError(error)
params = {"params": {RequestOptions.Field.PublishSamples: samples}}
- url = f"{self.baseurl}/{project_item.id}"
+ url = "{0}/{1}".format(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(f"Updated project item (ID: {project_item.id})")
+ logger.info("Updated project item (ID: {0})".format(project_item.id))
updated_project = ProjectItem.from_response(server_response.content, self.parent_srv.namespace)[0]
return updated_project
@@ -67,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 = f"{self.baseurl}?publishSamples={project_item._samples}"
+ url = "{0}?publishSamples={1}".format(self.baseurl, 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(f"Created new project (ID: {new_project.id})")
+ logger.info("Created new project (ID: {0})".format(new_project.id))
return new_project
@api(version="2.0")
@@ -79,135 +78,85 @@ def populate_permissions(self, item: ProjectItem) -> None:
self._permissions.populate(item)
@api(version="2.0")
- def update_permissions(self, item: ProjectItem, rules: list[PermissionsRule]) -> list[PermissionsRule]:
+ def update_permissions(self, item, rules):
return self._permissions.update(item, rules)
@api(version="2.0")
- def delete_permission(self, item: ProjectItem, rules: list[PermissionsRule]) -> None:
+ def delete_permission(self, item, rules):
self._permissions.delete(item, rules)
@api(version="2.1")
- def populate_workbook_default_permissions(self, item: ProjectItem) -> None:
+ def populate_workbook_default_permissions(self, item):
self._default_permissions.populate_default_permissions(item, Resource.Workbook)
@api(version="2.1")
- def populate_datasource_default_permissions(self, item: ProjectItem) -> None:
+ def populate_datasource_default_permissions(self, item):
self._default_permissions.populate_default_permissions(item, Resource.Datasource)
@api(version="3.2")
- def populate_metric_default_permissions(self, item: ProjectItem) -> None:
+ def populate_metric_default_permissions(self, item):
self._default_permissions.populate_default_permissions(item, Resource.Metric)
@api(version="3.4")
- def populate_datarole_default_permissions(self, item: ProjectItem) -> None:
+ def populate_datarole_default_permissions(self, item):
self._default_permissions.populate_default_permissions(item, Resource.Datarole)
@api(version="3.4")
- def populate_flow_default_permissions(self, item: ProjectItem) -> None:
+ def populate_flow_default_permissions(self, item):
self._default_permissions.populate_default_permissions(item, Resource.Flow)
@api(version="3.4")
- def populate_lens_default_permissions(self, item: ProjectItem) -> None:
+ def populate_lens_default_permissions(self, item):
self._default_permissions.populate_default_permissions(item, Resource.Lens)
- @api(version="3.23")
- def populate_virtualconnection_default_permissions(self, item: ProjectItem) -> None:
- self._default_permissions.populate_default_permissions(item, Resource.VirtualConnection)
-
- @api(version="3.23")
- def populate_database_default_permissions(self, item: ProjectItem) -> None:
- self._default_permissions.populate_default_permissions(item, Resource.Database)
-
- @api(version="3.23")
- def populate_table_default_permissions(self, item: ProjectItem) -> None:
- self._default_permissions.populate_default_permissions(item, Resource.Table)
-
@api(version="2.1")
- def update_workbook_default_permissions(
- self, item: ProjectItem, rules: list[PermissionsRule]
- ) -> list[PermissionsRule]:
+ def update_workbook_default_permissions(self, item, rules):
return self._default_permissions.update_default_permissions(item, rules, Resource.Workbook)
@api(version="2.1")
- def update_datasource_default_permissions(
- self, item: ProjectItem, rules: list[PermissionsRule]
- ) -> list[PermissionsRule]:
+ def update_datasource_default_permissions(self, item, rules):
return self._default_permissions.update_default_permissions(item, rules, Resource.Datasource)
@api(version="3.2")
- def update_metric_default_permissions(
- self, item: ProjectItem, rules: list[PermissionsRule]
- ) -> list[PermissionsRule]:
+ def update_metric_default_permissions(self, item, rules):
return self._default_permissions.update_default_permissions(item, rules, Resource.Metric)
@api(version="3.4")
- def update_datarole_default_permissions(
- self, item: ProjectItem, rules: list[PermissionsRule]
- ) -> list[PermissionsRule]:
+ def update_datarole_default_permissions(self, item, rules):
return self._default_permissions.update_default_permissions(item, rules, Resource.Datarole)
@api(version="3.4")
- def update_flow_default_permissions(self, item: ProjectItem, rules: list[PermissionsRule]) -> list[PermissionsRule]:
+ def update_flow_default_permissions(self, item, rules):
return self._default_permissions.update_default_permissions(item, rules, Resource.Flow)
@api(version="3.4")
- def update_lens_default_permissions(self, item: ProjectItem, rules: list[PermissionsRule]) -> list[PermissionsRule]:
+ def update_lens_default_permissions(self, item, rules):
return self._default_permissions.update_default_permissions(item, rules, Resource.Lens)
- @api(version="3.23")
- def update_virtualconnection_default_permissions(
- self, item: ProjectItem, rules: list[PermissionsRule]
- ) -> list[PermissionsRule]:
- return self._default_permissions.update_default_permissions(item, rules, Resource.VirtualConnection)
-
- @api(version="3.23")
- def update_database_default_permissions(
- self, item: ProjectItem, rules: list[PermissionsRule]
- ) -> list[PermissionsRule]:
- return self._default_permissions.update_default_permissions(item, rules, Resource.Database)
-
- @api(version="3.23")
- def update_table_default_permissions(
- self, item: ProjectItem, rules: list[PermissionsRule]
- ) -> list[PermissionsRule]:
- return self._default_permissions.update_default_permissions(item, rules, Resource.Table)
-
@api(version="2.1")
- def delete_workbook_default_permissions(self, item: ProjectItem, rule: PermissionsRule) -> None:
+ def delete_workbook_default_permissions(self, item, rule):
self._default_permissions.delete_default_permission(item, rule, Resource.Workbook)
@api(version="2.1")
- def delete_datasource_default_permissions(self, item: ProjectItem, rule: PermissionsRule) -> None:
+ def delete_datasource_default_permissions(self, item, rule):
self._default_permissions.delete_default_permission(item, rule, Resource.Datasource)
@api(version="3.2")
- def delete_metric_default_permissions(self, item: ProjectItem, rule: PermissionsRule) -> None:
+ def delete_metric_default_permissions(self, item, rule):
self._default_permissions.delete_default_permission(item, rule, Resource.Metric)
@api(version="3.4")
- def delete_datarole_default_permissions(self, item: ProjectItem, rule: PermissionsRule) -> None:
+ def delete_datarole_default_permissions(self, item, rule):
self._default_permissions.delete_default_permission(item, rule, Resource.Datarole)
@api(version="3.4")
- def delete_flow_default_permissions(self, item: ProjectItem, rule: PermissionsRule) -> None:
+ def delete_flow_default_permissions(self, item, rule):
self._default_permissions.delete_default_permission(item, rule, Resource.Flow)
@api(version="3.4")
- def delete_lens_default_permissions(self, item: ProjectItem, rule: PermissionsRule) -> None:
+ def delete_lens_default_permissions(self, item, rule):
self._default_permissions.delete_default_permission(item, rule, Resource.Lens)
- @api(version="3.23")
- def delete_virtualconnection_default_permissions(self, item: ProjectItem, rule: PermissionsRule) -> None:
- self._default_permissions.delete_default_permission(item, rule, Resource.VirtualConnection)
-
- @api(version="3.23")
- def delete_database_default_permissions(self, item: ProjectItem, rule: PermissionsRule) -> None:
- self._default_permissions.delete_default_permission(item, rule, Resource.Database)
-
- @api(version="3.23")
- def delete_table_default_permissions(self, item: ProjectItem, rule: PermissionsRule) -> None:
- self._default_permissions.delete_default_permission(item, rule, Resource.Table)
-
def filter(self, *invalid, page_size: Optional[int] = None, **kwargs) -> QuerySet[ProjectItem]:
"""
Queries the Tableau Server for items using the specified filters. Page
diff --git a/tableauserverclient/server/endpoint/resource_tagger.py b/tableauserverclient/server/endpoint/resource_tagger.py
index 63c03b3e3..1894e3b8a 100644
--- a/tableauserverclient/server/endpoint/resource_tagger.py
+++ b/tableauserverclient/server/endpoint/resource_tagger.py
@@ -1,7 +1,6 @@
import abc
import copy
-from typing import Generic, Optional, Protocol, TypeVar, Union, TYPE_CHECKING, runtime_checkable
-from collections.abc import Iterable
+from typing import Generic, Iterable, Optional, Protocol, Set, TypeVar, Union, TYPE_CHECKING, runtime_checkable
import urllib.parse
from tableauserverclient.server.endpoint.endpoint import Endpoint, api
@@ -25,7 +24,7 @@
class _ResourceTagger(Endpoint):
# Add new tags to resource
def _add_tags(self, baseurl, resource_id, tag_set):
- url = f"{baseurl}/{resource_id}/tags"
+ url = "{0}/{1}/tags".format(baseurl, resource_id)
add_req = RequestFactory.Tag.add_req(tag_set)
try:
@@ -40,7 +39,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 = f"{baseurl}/{resource_id}/tags/{encoded_tag_name}"
+ url = "{0}/{1}/tags/{2}".format(baseurl, resource_id, encoded_tag_name)
try:
self.delete_request(url)
@@ -60,7 +59,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(f"Updated tags to {resource_item.tags}")
+ logger.info("Updated tags to {0}".format(resource_item.tags))
class Response(Protocol):
@@ -69,8 +68,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]:
@@ -96,14 +95,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 = {tags}
+ tag_set = set([tags])
else:
tag_set = set(tags)
@@ -119,7 +118,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 = {tags}
+ tag_set = set([tags])
else:
tag_set = set(tags)
@@ -159,9 +158,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 = {tags}
+ tag_set = set([tags])
else:
tag_set = set(tags)
@@ -171,9 +170,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 = {tags}
+ tag_set = set([tags])
else:
tag_set = set(tags)
diff --git a/tableauserverclient/server/endpoint/schedules_endpoint.py b/tableauserverclient/server/endpoint/schedules_endpoint.py
index eec4536f9..cfaee3324 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, Optional, Union
+from typing import TYPE_CHECKING, Callable, List, Optional, Tuple, 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 f"{self.parent_srv.baseurl}/schedules"
+ return "{0}/schedules".format(self.parent_srv.baseurl)
@property
def siteurl(self) -> str:
- return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/schedules"
+ return "{0}/sites/{1}/schedules".format(self.parent_srv.baseurl, self.parent_srv.site_id)
@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(f"Querying a single schedule by id ({schedule_id})")
- url = f"{self.baseurl}/{schedule_id}"
+ logger.info("Querying a single schedule by id ({})".format(schedule_id))
+ url = "{0}/{1}".format(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 = f"{self.baseurl}/{schedule_id}"
+ url = "{0}/{1}".format(self.baseurl, schedule_id)
self.delete_request(url)
- logger.info(f"Deleted single schedule (ID: {schedule_id})")
+ logger.info("Deleted single schedule (ID: {0})".format(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 = f"{self.baseurl}/{schedule_item.id}"
+ url = "{0}/{1}".format(self.baseurl, schedule_item.id)
update_req = RequestFactory.Schedule.update_req(schedule_item)
server_response = self.put_request(url, update_req)
- logger.info(f"Updated schedule item (ID: {schedule_item.id})")
+ logger.info("Updated schedule item (ID: {})".format(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(f"Created new schedule (ID: {new_schedule.id})")
+ logger.info("Created new schedule (ID: {})".format(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:
@@ -115,7 +115,8 @@ def add_to_schedule(
) # type:ignore[arg-type]
results = (self._add_to(*x) for x in items)
- return [x for x in results if not x.result]
+ # list() is needed for python 3.x compatibility
+ return list(filter(lambda x: not x.result, results)) # type:ignore[arg-type]
def _add_to(
self,
@@ -132,13 +133,13 @@ def _add_to(
item_task_type,
) -> AddResponse:
id_ = resource.id
- url = f"{self.siteurl}/{schedule_id}/{type_}s"
+ url = "{0}/{1}/{2}s".format(self.siteurl, schedule_id, type_)
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(f"Added {type_} to {id_} to schedule {schedule_id}")
+ logger.info("Added {} to {} to schedule {}".format(type_, id_, 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 dc934496a..26aaf2910 100644
--- a/tableauserverclient/server/endpoint/server_info_endpoint.py
+++ b/tableauserverclient/server/endpoint/server_info_endpoint.py
@@ -1,5 +1,4 @@
import logging
-from typing import Union
from .endpoint import Endpoint, api
from .exceptions import ServerResponseError
@@ -22,49 +21,15 @@ def serverInfo(self):
return self._info
def __repr__(self):
- return f""
+ return "".format(self.serverInfo)
@property
- def baseurl(self) -> str:
- return f"{self.parent_srv.baseurl}/serverInfo"
+ def baseurl(self):
+ return "{0}/serverInfo".format(self.parent_srv.baseurl)
@api(version="2.4")
- def get(self) -> Union[ServerInfoItem, None]:
- """
- Retrieve the build and version information for the server.
-
- This method makes an unauthenticated call, so no sign in or
- authentication token is required.
-
- Returns
- -------
- :class:`~tableauserverclient.models.ServerInfoItem`
-
- Raises
- ------
- :class:`~tableauserverclient.exceptions.ServerInfoEndpointNotFoundError`
- Raised when the server info endpoint is not found.
-
- :class:`~tableauserverclient.exceptions.EndpointUnavailableError`
- Raised when the server info endpoint is not available.
-
- Examples
- --------
- >>> import tableauserverclient as TSC
-
- >>> # create a instance of server
- >>> server = TSC.Server('https://MY-SERVER')
-
- >>> # set the version number > 2.3
- >>> # the server_info.get() method works in 2.4 and later
- >>> server.version = '2.5'
-
- >>> s_info = server.server_info.get()
- >>> print("\nServer info:")
- >>> print("\tProduct version: {0}".format(s_info.product_version))
- >>> print("\tREST API version: {0}".format(s_info.rest_api_version))
- >>> print("\tBuild number: {0}".format(s_info.build_number))
- """
+ def get(self):
+ """Retrieve the server info for the server. This is an unauthenticated call"""
try:
server_response = self.get_unauthenticated_request(self.baseurl)
except ServerResponseError as e:
diff --git a/tableauserverclient/server/endpoint/sites_endpoint.py b/tableauserverclient/server/endpoint/sites_endpoint.py
index 55d2a5ad0..dfec49ae1 100644
--- a/tableauserverclient/server/endpoint/sites_endpoint.py
+++ b/tableauserverclient/server/endpoint/sites_endpoint.py
@@ -8,49 +8,20 @@
from tableauserverclient.helpers.logging import logger
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING, List, Optional, Tuple
if TYPE_CHECKING:
from ..request_options import RequestOptions
class Sites(Endpoint):
- """
- Using the site methods of the Tableau Server REST API you can:
-
- List sites on a server or get details of a specific site
- Create, update, or delete a site
- List views in a site
- Encrypt, decrypt, or reencrypt extracts on a site
-
- """
-
@property
def baseurl(self) -> str:
- return f"{self.parent_srv.baseurl}/sites"
+ return "{0}/sites".format(self.parent_srv.baseurl)
# Gets all sites
@api(version="2.0")
- def get(self, req_options: Optional["RequestOptions"] = None) -> tuple[list[SiteItem], PaginationItem]:
- """
- Query all sites on the server. This method requires server admin
- permissions. This endpoint is paginated, meaning that the server will
- only return a subset of the data at a time. The response will contain
- information about the total number of sites and the number of sites
- returned in the current response. Use the PaginationItem object to
- request more data.
-
- REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref.htm#query_sites
-
- Parameters
- ----------
- req_options : RequestOptions, optional
- Filtering options for the request.
-
- Returns
- -------
- 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
@@ -62,33 +33,6 @@ def get(self, req_options: Optional["RequestOptions"] = None) -> tuple[list[Site
# Gets 1 site by id
@api(version="2.0")
def get_by_id(self, site_id: str) -> SiteItem:
- """
- Query a single site on the server. You can only retrieve the site that
- you are currently authenticated for.
-
- REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref.htm#query_site
-
- Parameters
- ----------
- site_id : str
- The site ID.
-
- Returns
- -------
- SiteItem
-
- Raises
- ------
- ValueError
- If the site ID is not defined.
-
- ValueError
- If the site ID does not match the site for which you are currently authenticated.
-
- Examples
- --------
- >>> site = server.sites.get_by_id('1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p')
- """
if not site_id:
error = "Site ID undefined."
raise ValueError(error)
@@ -96,45 +40,20 @@ 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(f"Querying single site (ID: {site_id})")
- url = f"{self.baseurl}/{site_id}"
+ logger.info("Querying single site (ID: {0})".format(site_id))
+ url = "{0}/{1}".format(self.baseurl, site_id)
server_response = self.get_request(url)
return SiteItem.from_response(server_response.content, self.parent_srv.namespace)[0]
# Gets 1 site by name
@api(version="2.0")
def get_by_name(self, site_name: str) -> SiteItem:
- """
- Query a single site on the server. You can only retrieve the site that
- you are currently authenticated for.
-
- REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref.htm#query_site
-
- Parameters
- ----------
- site_name : str
- The site name.
-
- Returns
- -------
- SiteItem
-
- Raises
- ------
- ValueError
- If the site name is not defined.
-
- Examples
- --------
- >>> site = server.sites.get_by_name('Tableau')
-
- """
if not site_name:
error = "Site Name undefined."
raise ValueError(error)
print("Note: You can only work with the site for which you are currently authenticated")
- logger.info(f"Querying single site (Name: {site_name})")
- url = f"{self.baseurl}/{site_name}?key=name"
+ logger.info("Querying single site (Name: {0})".format(site_name))
+ url = "{0}/{1}?key=name".format(self.baseurl, site_name)
print(self.baseurl, url)
server_response = self.get_request(url)
return SiteItem.from_response(server_response.content, self.parent_srv.namespace)[0]
@@ -142,31 +61,6 @@ def get_by_name(self, site_name: str) -> SiteItem:
# Gets 1 site by content url
@api(version="2.0")
def get_by_content_url(self, content_url: str) -> SiteItem:
- """
- Query a single site on the server. You can only retrieve the site that
- you are currently authenticated for.
-
- REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref.htm#query_site
-
- Parameters
- ----------
- content_url : str
- The content URL.
-
- Returns
- -------
- SiteItem
-
- Raises
- ------
- ValueError
- If the site name is not defined.
-
- Examples
- --------
- >>> site = server.sites.get_by_name('Tableau')
-
- """
if content_url is None:
error = "Content URL undefined."
raise ValueError(error)
@@ -174,51 +68,15 @@ 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(f"Querying single site (Content URL: {content_url})")
+ logger.info("Querying single site (Content URL: {0})".format(content_url))
logger.debug("Querying other sites requires Server Admin permissions")
- url = f"{self.baseurl}/{content_url}?key=contentUrl"
+ url = "{0}/{1}?key=contentUrl".format(self.baseurl, content_url)
server_response = self.get_request(url)
return SiteItem.from_response(server_response.content, self.parent_srv.namespace)[0]
# Update site
@api(version="2.0")
def update(self, site_item: SiteItem) -> SiteItem:
- """
- Modifies the settings for site.
-
- The site item object must include the site ID and overrides all other settings.
-
- REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref.htm#update_site
-
- Parameters
- ----------
- site_item : SiteItem
- The site item that you want to update. The settings specified in the
- site item override the current site settings.
-
- Returns
- -------
- SiteItem
- The site item object that was updated.
-
- Raises
- ------
- MissingRequiredFieldError
- If the site item is missing an ID.
-
- ValueError
- If the site ID does not match the site for which you are currently authenticated.
-
- ValueError
- If the site admin mode is set to ContentOnly and a user quota is also set.
-
- Examples
- --------
- >>> ...
- >>> site_item.name = 'New Name'
- >>> updated_site = server.sites.update(site_item)
-
- """
if not site_item.id:
error = "Site item missing ID."
raise MissingRequiredFieldError(error)
@@ -232,94 +90,30 @@ 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 = f"{self.baseurl}/{site_item.id}"
+ url = "{0}/{1}".format(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(f"Updated site item (ID: {site_item.id})")
+ logger.info("Updated site item (ID: {0})".format(site_item.id))
update_site = copy.copy(site_item)
return update_site._parse_common_tags(server_response.content, self.parent_srv.namespace)
# Delete 1 site object
@api(version="2.0")
def delete(self, site_id: str) -> None:
- """
- Deletes the specified site from the server. You can only delete the site
- if you are a Server Admin.
-
- REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref.htm#delete_site
-
- Parameters
- ----------
- site_id : str
- The site ID.
-
- Raises
- ------
- ValueError
- If the site ID is not defined.
-
- ValueError
- If the site ID does not match the site for which you are currently authenticated.
-
- Examples
- --------
- >>> server.sites.delete('1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p')
- """
if not site_id:
error = "Site ID undefined."
raise ValueError(error)
- url = f"{self.baseurl}/{site_id}"
+ url = "{0}/{1}".format(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(f"Deleted single site (ID: {site_id}) and signed out")
+ logger.info("Deleted single site (ID: {0}) and signed out".format(site_id))
# Create new site
@api(version="2.0")
def create(self, site_item: SiteItem) -> SiteItem:
- """
- Creates a new site on the server for the specified site item object.
-
- Tableau Server only.
-
- REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref.htm#create_site
-
- Parameters
- ----------
- site_item : SiteItem
- The settings for the site that you want to create. You need to
- create an instance of SiteItem and pass it to the create method.
-
- Returns
- -------
- SiteItem
- The site item object that was created.
-
- Raises
- ------
- ValueError
- If the site admin mode is set to ContentOnly and a user quota is also set.
-
- Examples
- --------
- >>> import tableauserverclient as TSC
-
- >>> # create an instance of server
- >>> server = TSC.Server('https://MY-SERVER')
-
- >>> # create shortcut for admin mode
- >>> content_users=TSC.SiteItem.AdminMode.ContentAndUsers
-
- >>> # create a new SiteItem
- >>> new_site = TSC.SiteItem(name='Tableau', content_url='tableau', admin_mode=content_users, user_quota=15, storage_quota=1000, disable_subscriptions=True)
-
- >>> # call the sites create method with the SiteItem
- >>> new_site = server.sites.create(new_site)
-
-
- """
if site_item.admin_mode:
if site_item.admin_mode == SiteItem.AdminMode.ContentOnly and site_item.user_quota:
error = "You cannot set admin_mode to ContentOnly and also set a user quota"
@@ -329,92 +123,33 @@ 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(f"Created new site (ID: {new_site.id})")
+ logger.info("Created new site (ID: {0})".format(new_site.id))
return new_site
@api(version="3.5")
def encrypt_extracts(self, site_id: str) -> None:
- """
- Encrypts all extracts on the site.
-
- REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_extract_and_encryption.htm#encrypt_extracts
-
- Parameters
- ----------
- site_id : str
- The site ID.
-
- Raises
- ------
- ValueError
- If the site ID is not defined.
-
- Examples
- --------
- >>> server.sites.encrypt_extracts('1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p')
- """
if not site_id:
error = "Site ID undefined."
raise ValueError(error)
- url = f"{self.baseurl}/{site_id}/encrypt-extracts"
+ url = "{0}/{1}/encrypt-extracts".format(self.baseurl, site_id)
empty_req = RequestFactory.Empty.empty_req()
self.post_request(url, empty_req)
@api(version="3.5")
def decrypt_extracts(self, site_id: str) -> None:
- """
- Decrypts all extracts on the site.
-
- REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_extract_and_encryption.htm#decrypt_extracts
-
- Parameters
- ----------
- site_id : str
- The site ID.
-
- Raises
- ------
- ValueError
- If the site ID is not defined.
-
- Examples
- --------
- >>> server.sites.decrypt_extracts('1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p')
- """
if not site_id:
error = "Site ID undefined."
raise ValueError(error)
- url = f"{self.baseurl}/{site_id}/decrypt-extracts"
+ url = "{0}/{1}/decrypt-extracts".format(self.baseurl, site_id)
empty_req = RequestFactory.Empty.empty_req()
self.post_request(url, empty_req)
@api(version="3.5")
def re_encrypt_extracts(self, site_id: str) -> None:
- """
- Reencrypt all extracts on a site with new encryption keys. If no site is
- specified, extracts on the default site will be reencrypted.
-
- REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_extract_and_encryption.htm#reencrypt_extracts
-
- Parameters
- ----------
- site_id : str
- The site ID.
-
- Raises
- ------
- ValueError
- If the site ID is not defined.
-
- Examples
- --------
- >>> server.sites.re_encrypt_extracts('1a2b3c4d-5e6f-7g8h-9i0j-1k2l3m4n5o6p')
-
- """
if not site_id:
error = "Site ID undefined."
raise ValueError(error)
- url = f"{self.baseurl}/{site_id}/reencrypt-extracts"
+ url = "{0}/{1}/reencrypt-extracts".format(self.baseurl, site_id)
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 c9abc9b06..a9f2e7bf5 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 Optional, TYPE_CHECKING
+from typing import List, Optional, TYPE_CHECKING, Tuple
if TYPE_CHECKING:
from ..request_options import RequestOptions
@@ -16,10 +16,10 @@
class Subscriptions(Endpoint):
@property
def baseurl(self) -> str:
- return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/subscriptions"
+ return "{0}/sites/{1}/subscriptions".format(self.parent_srv.baseurl, self.parent_srv.site_id)
@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(f"Querying a single subscription by id ({subscription_id})")
- url = f"{self.baseurl}/{subscription_id}"
+ logger.info("Querying a single subscription by id ({})".format(subscription_id))
+ url = "{}/{}".format(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(f"Creating a subscription ({subscription_item})")
+ logger.info("Creating a subscription ({})".format(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 = f"{self.baseurl}/{subscription_id}"
+ url = "{0}/{1}".format(self.baseurl, subscription_id)
self.delete_request(url)
- logger.info(f"Deleted subscription (ID: {subscription_id})")
+ logger.info("Deleted subscription (ID: {0})".format(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 = f"{self.baseurl}/{subscription_item.id}"
+ url = "{0}/{1}".format(self.baseurl, subscription_item.id)
update_req = RequestFactory.Subscription.update_req(subscription_item)
server_response = self.put_request(url, update_req)
- logger.info(f"Updated subscription item (ID: {subscription_item.id})")
+ logger.info("Updated subscription item (ID: {0})".format(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 120d3ba9c..36ef78c0a 100644
--- a/tableauserverclient/server/endpoint/tables_endpoint.py
+++ b/tableauserverclient/server/endpoint/tables_endpoint.py
@@ -1,6 +1,5 @@
import logging
-from typing import Union
-from collections.abc import Iterable
+from typing import Iterable, Set, Union
from tableauserverclient.server.endpoint.dqw_endpoint import _DataQualityWarningEndpoint
from tableauserverclient.server.endpoint.endpoint import api, Endpoint
@@ -16,14 +15,14 @@
class Tables(Endpoint, TaggingMixin[TableItem]):
def __init__(self, parent_srv):
- super().__init__(parent_srv)
+ super(Tables, self).__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 f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/tables"
+ return "{0}/sites/{1}/tables".format(self.parent_srv.baseurl, self.parent_srv.site_id)
@api(version="3.5")
def get(self, req_options=None):
@@ -40,8 +39,8 @@ def get_by_id(self, table_id):
if not table_id:
error = "table ID undefined."
raise ValueError(error)
- logger.info(f"Querying single table (ID: {table_id})")
- url = f"{self.baseurl}/{table_id}"
+ logger.info("Querying single table (ID: {0})".format(table_id))
+ url = "{0}/{1}".format(self.baseurl, table_id)
server_response = self.get_request(url)
return TableItem.from_response(server_response.content, self.parent_srv.namespace)[0]
@@ -50,9 +49,9 @@ def delete(self, table_id):
if not table_id:
error = "Database ID undefined."
raise ValueError(error)
- url = f"{self.baseurl}/{table_id}"
+ url = "{0}/{1}".format(self.baseurl, table_id)
self.delete_request(url)
- logger.info(f"Deleted single table (ID: {table_id})")
+ logger.info("Deleted single table (ID: {0})".format(table_id))
@api(version="3.5")
def update(self, table_item):
@@ -60,10 +59,10 @@ def update(self, table_item):
error = "table item missing ID."
raise MissingRequiredFieldError(error)
- url = f"{self.baseurl}/{table_item.id}"
+ url = "{0}/{1}".format(self.baseurl, table_item.id)
update_req = RequestFactory.Table.update_req(table_item)
server_response = self.put_request(url, update_req)
- logger.info(f"Updated table item (ID: {table_item.id})")
+ logger.info("Updated table item (ID: {0})".format(table_item.id))
updated_table = TableItem.from_response(server_response.content, self.parent_srv.namespace)[0]
return updated_table
@@ -81,10 +80,10 @@ def column_fetcher():
)
table_item._set_columns(column_fetcher)
- logger.info(f"Populated columns for table (ID: {table_item.id}")
+ logger.info("Populated columns for table (ID: {0}".format(table_item.id))
def _get_columns_for_table(self, table_item, req_options=None):
- url = f"{self.baseurl}/{table_item.id}/columns"
+ url = "{0}/{1}/columns".format(self.baseurl, table_item.id)
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)
@@ -92,12 +91,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 = f"{self.baseurl}/{table_item.id}/columns/{column_item.id}"
+ url = "{0}/{1}/columns/{2}".format(self.baseurl, table_item.id, 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(f"Updated table item (ID: {table_item.id} & column item {column_item.id}")
+ logger.info("Updated table item (ID: {0} & column item {1}".format(table_item.id, column_item.id))
return column
@api(version="3.5")
@@ -129,7 +128,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 eb82c43bc..a727a515f 100644
--- a/tableauserverclient/server/endpoint/tasks_endpoint.py
+++ b/tableauserverclient/server/endpoint/tasks_endpoint.py
@@ -1,5 +1,5 @@
import logging
-from typing import Optional, TYPE_CHECKING
+from typing import List, Optional, Tuple, 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 f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/tasks"
+ return "{0}/sites/{1}/tasks".format(self.parent_srv.baseurl, self.parent_srv.site_id)
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 f"{task_type}es"
+ return "{}es".format(task_type)
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 = f"{self.baseurl}/{self.__normalize_task_type(task_type)}"
+ url = "{0}/{1}".format(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 = f"{self.baseurl}/{self.__normalize_task_type(TaskItem.Type.ExtractRefresh)}"
+ url = "{0}/{1}".format(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 = "{}/{}/{}/runNow".format(
+ url = "{0}/{1}/{2}/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 = f"{self.baseurl}/{self.__normalize_task_type(task_type)}/{task_id}"
+ url = "{0}/{1}/{2}".format(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 d81907ae9..c4b6418b7 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 Optional
+from typing import List, Optional, Tuple
from tableauserverclient.server.query import QuerySet
@@ -14,75 +14,13 @@
class Users(QuerysetEndpoint[UserItem]):
- """
- The user resources for Tableau Server are defined in the UserItem class.
- The class corresponds to the user resources you can access using the
- Tableau Server REST API. The user methods are based upon the endpoints for
- users in the REST API and operate on the UserItem class. Only server and
- site administrators can access the user resources.
- """
-
@property
def baseurl(self) -> str:
- return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/users"
+ return "{0}/sites/{1}/users".format(self.parent_srv.baseurl, self.parent_srv.site_id)
# Gets all users
@api(version="2.0")
- def get(self, req_options: Optional[RequestOptions] = None) -> tuple[list[UserItem], PaginationItem]:
- """
- Query all users on the site. Request is paginated and returns a subset of users.
- By default, the request returns the first 100 users on the site.
-
- Parameters
- ----------
- req_options : Optional[RequestOptions]
- Optional request options to filter and sort the results.
-
- Returns
- -------
- tuple[list[UserItem], PaginationItem]
- Returns a tuple with a list of UserItem objects and a PaginationItem object.
-
- Raises
- ------
- ServerResponseError
- code: 400006
- summary: Invalid page number
- detail: The page number is not an integer, is less than one, or is
- greater than the final page number for users at the requested
- page size.
-
- ServerResponseError
- code: 400007
- summary: Invalid page size
- detail: The page size parameter is not an integer, is less than one.
-
- ServerResponseError
- code: 403014
- summary: Page size limit exceeded
- detail: The specified page size is larger than the maximum page size
-
- ServerResponseError
- code: 404000
- summary: Site not found
- detail: The site ID in the URI doesn't correspond to an existing site.
-
- ServerResponseError
- code: 405000
- summary: Invalid request method
- detail: Request type was not GET.
-
- Examples
- --------
- >>> import tableauserverclient as TSC
- >>> tableau_auth = TSC.TableauAuth('USERNAME', 'PASSWORD')
- >>> server = TSC.Server('https://SERVERURL')
-
- >>> with server.auth.sign_in(tableau_auth):
- >>> users_page, pagination_item = server.users.get()
- >>> print("\nThere are {} user on site: ".format(pagination_item.total_available))
- >>> print([user.name for user in users_page])
- """
+ def get(self, req_options: Optional[RequestOptions] = None) -> Tuple[List[UserItem], PaginationItem]:
logger.info("Querying all users on site")
if req_options is None:
@@ -98,253 +36,55 @@ def get(self, req_options: Optional[RequestOptions] = None) -> tuple[list[UserIt
# Gets 1 user by id
@api(version="2.0")
def get_by_id(self, user_id: str) -> UserItem:
- """
- Query a single user by ID.
-
- Parameters
- ----------
- user_id : str
- The ID of the user to query.
-
- Returns
- -------
- UserItem
- The user item that was queried.
-
- Raises
- ------
- ValueError
- If the user ID is not specified.
-
- ServerResponseError
- code: 404000
- summary: Site not found
- detail: The site ID in the URI doesn't correspond to an existing site.
-
- ServerResponseError
- code: 403133
- summary: Query user permissions forbidden
- detail: The user does not have permissions to query user information
- for other users
-
- ServerResponseError
- code: 404002
- summary: User not found
- detail: The user ID in the URI doesn't correspond to an existing user.
-
- ServerResponseError
- code: 405000
- summary: Invalid request method
- detail: Request type was not GET.
-
- Examples
- --------
- >>> user1 = server.users.get_by_id('9f9e9d9c-8b8a-8f8e-7d7c-7b7a6f6d6e6d')
- """
if not user_id:
error = "User ID undefined."
raise ValueError(error)
- logger.info(f"Querying single user (ID: {user_id})")
- url = f"{self.baseurl}/{user_id}"
+ logger.info("Querying single user (ID: {0})".format(user_id))
+ url = "{0}/{1}".format(self.baseurl, user_id)
server_response = self.get_request(url)
return UserItem.from_response(server_response.content, self.parent_srv.namespace).pop()
# Update user
@api(version="2.0")
def update(self, user_item: UserItem, password: Optional[str] = None) -> UserItem:
- """
- Modifies information about the specified user.
-
- If Tableau Server is configured to use local authentication, you can
- update the user's name, email address, password, or site role.
-
- If Tableau Server is configured to use Active Directory
- authentication, you can change the user's display name (full name),
- email address, and site role. However, if you synchronize the user with
- Active Directory, the display name and email address will be
- overwritten with the information that's in Active Directory.
-
- For Tableau Cloud, you can update the site role for a user, but you
- cannot update or change a user's password, user name (email address),
- or full name.
-
- Parameters
- ----------
- user_item : UserItem
- The user item to update.
-
- password : Optional[str]
- The new password for the user.
-
- Returns
- -------
- UserItem
- The user item that was updated.
-
- Raises
- ------
- MissingRequiredFieldError
- If the user item is missing an ID.
-
- Examples
- --------
- >>> user = server.users.get_by_id('9f9e9d9c-8b8a-8f8e-7d7c-7b7a6f6d6e6d')
- >>> user.fullname = 'New Full Name'
- >>> updated_user = server.users.update(user)
-
- """
if not user_item.id:
error = "User item missing ID."
raise MissingRequiredFieldError(error)
- url = f"{self.baseurl}/{user_item.id}"
+ url = "{0}/{1}".format(self.baseurl, user_item.id)
update_req = RequestFactory.User.update_req(user_item, password)
server_response = self.put_request(url, update_req)
- logger.info(f"Updated user item (ID: {user_item.id})")
+ logger.info("Updated user item (ID: {0})".format(user_item.id))
updated_item = copy.copy(user_item)
return updated_item._parse_common_tags(server_response.content, self.parent_srv.namespace)
# Delete 1 user by id
@api(version="2.0")
def remove(self, user_id: str, map_assets_to: Optional[str] = None) -> None:
- """
- Removes a user from the site. You can also specify a user to map the
- assets to when you remove the user.
-
- Parameters
- ----------
- user_id : str
- The ID of the user to remove.
-
- map_assets_to : Optional[str]
- The ID of the user to map the assets to when you remove the user.
-
- Returns
- -------
- None
-
- Raises
- ------
- ValueError
- If the user ID is not specified.
-
- Examples
- --------
- >>> server.users.remove('9f9e9d9c-8b8a-8f8e-7d7c-7b7a6f6d6e6d')
- """
if not user_id:
error = "User ID undefined."
raise ValueError(error)
- url = f"{self.baseurl}/{user_id}"
+ url = "{0}/{1}".format(self.baseurl, user_id)
if map_assets_to is not None:
url += f"?mapAssetsTo={map_assets_to}"
self.delete_request(url)
- logger.info(f"Removed single user (ID: {user_id})")
+ logger.info("Removed single user (ID: {0})".format(user_id))
# Add new user to site
@api(version="2.0")
def add(self, user_item: UserItem) -> UserItem:
- """
- Adds the user to the site.
-
- To add a new user to the site you need to first create a new user_item
- (from UserItem class). When you create a new user, you specify the name
- of the user and their site role. For Tableau Cloud, you also specify
- the auth_setting attribute in your request. When you add user to
- Tableau Cloud, the name of the user must be the email address that is
- used to sign in to Tableau Cloud. After you add a user, Tableau Cloud
- sends the user an email invitation. The user can click the link in the
- invitation to sign in and update their full name and password.
-
- Parameters
- ----------
- user_item : UserItem
- The user item to add to the site.
-
- Returns
- -------
- UserItem
- The user item that was added to the site with attributes from the
- site populated.
-
- Raises
- ------
- ValueError
- If the user item is missing a name
-
- ValueError
- If the user item is missing a site role
-
- ServerResponseError
- code: 400000
- summary: Bad Request
- detail: The content of the request body is missing or incomplete, or
- contains malformed XML.
-
- ServerResponseError
- code: 400003
- summary: Bad Request
- detail: The user authentication setting ServerDefault is not
- supported for you site. Try again using TableauIDWithMFA instead.
-
- ServerResponseError
- code: 400013
- summary: Invalid site role
- detail: The value of the siteRole attribute must be Explorer,
- ExplorerCanPublish, SiteAdministratorCreator,
- SiteAdministratorExplorer, Unlicensed, or Viewer.
-
- ServerResponseError
- code: 404000
- summary: Site not found
- detail: The site ID in the URI doesn't correspond to an existing site.
-
- ServerResponseError
- code: 404002
- summary: User not found
- detail: The server is configured to use Active Directory for
- authentication, and the username specified in the request body
- doesn't match an existing user in Active Directory.
-
- ServerResponseError
- code: 405000
- summary: Invalid request method
- detail: Request type was not POST.
-
- ServerResponseError
- code: 409000
- summary: User conflict
- detail: The specified user already exists on the site.
-
- ServerResponseError
- code: 409005
- summary: Guest user conflict
- detail: The Tableau Server API doesn't allow adding a user with the
- guest role to a site.
-
-
- Examples
- --------
- >>> import tableauserverclient as TSC
- >>> server = TSC.Server('https://SERVERURL')
- >>> # Login to the server
-
- >>> new_user = TSC.UserItem(name='new_user', site_role=TSC.UserItem.Role.Unlicensed)
- >>> new_user = server.users.add(new_user)
-
- """
url = self.baseurl
- logger.info(f"Add user {user_item.name}")
+ logger.info("Add user {}".format(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(f"Added new user (ID: {new_user.id})")
+ logger.info("Added new user (ID: {0})".format(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:
@@ -358,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"):
@@ -382,42 +122,6 @@ def create_from_file(self, filepath: str) -> tuple[list[UserItem], list[tuple[Us
# Get workbooks for user
@api(version="2.0")
def populate_workbooks(self, user_item: UserItem, req_options: Optional[RequestOptions] = None) -> None:
- """
- Returns information about the workbooks that the specified user owns
- and has Read (view) permissions for.
-
- This method retrieves the workbook information for the specified user.
- The REST API is designed to return only the information you ask for
- explicitly. When you query for all the users, the workbook information
- for each user is not included. Use this method to retrieve information
- about the workbooks that the user owns or has Read (view) permissions.
- The method adds the list of workbooks to the user item object
- (user_item.workbooks).
-
- Parameters
- ----------
- user_item : UserItem
- The user item to populate workbooks for.
-
- req_options : Optional[RequestOptions]
- Optional request options to filter and sort the results.
-
- Returns
- -------
- None
-
- Raises
- ------
- MissingRequiredFieldError
- If the user item is missing an ID.
-
- Examples
- --------
- >>> user = server.users.get_by_id('9f9e9d9c-8b8a-8f8e-7d7c-7b7a6f6d6e6d')
- >>> server.users.populate_workbooks(user)
- >>> for wb in user.workbooks:
- >>> print(wb.name)
- """
if not user_item.id:
error = "User item missing ID."
raise MissingRequiredFieldError(error)
@@ -429,71 +133,20 @@ def wb_pager():
def _get_wbs_for_user(
self, user_item: UserItem, req_options: Optional[RequestOptions] = None
- ) -> tuple[list[WorkbookItem], PaginationItem]:
- url = f"{self.baseurl}/{user_item.id}/workbooks"
+ ) -> Tuple[List[WorkbookItem], PaginationItem]:
+ url = "{0}/{1}/workbooks".format(self.baseurl, user_item.id)
server_response = self.get_request(url, req_options)
- logger.info(f"Populated workbooks for user (ID: {user_item.id})")
+ logger.info("Populated workbooks for user (ID: {0})".format(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
def populate_favorites(self, user_item: UserItem) -> None:
- """
- Populate the favorites for the user.
-
- Parameters
- ----------
- user_item : UserItem
- The user item to populate favorites for.
-
- Returns
- -------
- None
-
- Examples
- --------
- >>> import tableauserverclient as TSC
- >>> server = TSC.Server('https://SERVERURL')
- >>> # Login to the server
-
- >>> user = server.users.get_by_id('9f9e9d9c-8b8a-8f8e-7d7c-7b7a6f6d6e6d')
- >>> server.users.populate_favorites(user)
- >>> for obj_type, items in user.favorites.items():
- >>> print(f"Favorites for {obj_type}:")
- >>> for item in items:
- >>> print(item.name)
- """
self.parent_srv.favorites.get(user_item)
# Get groups for user
@api(version="3.7")
def populate_groups(self, user_item: UserItem, req_options: Optional[RequestOptions] = None) -> None:
- """
- Populate the groups for the user.
-
- Parameters
- ----------
- user_item : UserItem
- The user item to populate groups for.
-
- req_options : Optional[RequestOptions]
- Optional request options to filter and sort the results.
-
- Returns
- -------
- None
-
- Raises
- ------
- MissingRequiredFieldError
- If the user item is missing an ID.
-
- Examples
- --------
- >>> server.users.populate_groups(user)
- >>> for group in user.groups:
- >>> print(group.name)
- """
if not user_item.id:
error = "User item missing ID."
raise MissingRequiredFieldError(error)
@@ -508,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 = f"{self.baseurl}/{user_item.id}/groups"
+ ) -> Tuple[List[GroupItem], PaginationItem]:
+ url = "{0}/{1}/groups".format(self.baseurl, user_item.id)
server_response = self.get_request(url, req_options)
- logger.info(f"Populated groups for user (ID: {user_item.id})")
+ logger.info("Populated groups for user (ID: {0})".format(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 3709fc41d..f2ccf658e 100644
--- a/tableauserverclient/server/endpoint/views_endpoint.py
+++ b/tableauserverclient/server/endpoint/views_endpoint.py
@@ -11,8 +11,7 @@
from tableauserverclient.helpers.logging import logger
-from typing import Optional, TYPE_CHECKING, Union
-from collections.abc import Iterable, Iterator
+from typing import Iterable, Iterator, List, Optional, Set, Tuple, TYPE_CHECKING, Union
if TYPE_CHECKING:
from tableauserverclient.server.request_options import (
@@ -26,22 +25,22 @@
class Views(QuerysetEndpoint[ViewItem], TaggingMixin[ViewItem]):
def __init__(self, parent_srv):
- super().__init__(parent_srv)
+ super(Views, self).__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 f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}"
+ return "{0}/sites/{1}".format(self.parent_srv.baseurl, self.parent_srv.site_id)
@property
def baseurl(self) -> str:
- return f"{self.siteurl}/views"
+ return "{0}/views".format(self.siteurl)
@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:
@@ -56,8 +55,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(f"Querying single view (ID: {view_id})")
- url = f"{self.baseurl}/{view_id}"
+ logger.info("Querying single view (ID: {0})".format(view_id))
+ url = "{0}/{1}".format(self.baseurl, view_id)
if usage:
url += "?includeUsageStatistics=true"
server_response = self.get_request(url)
@@ -73,10 +72,10 @@ def image_fetcher():
return self._get_preview_for_view(view_item)
view_item._set_preview_image(image_fetcher)
- logger.info(f"Populated preview image for view (ID: {view_item.id})")
+ logger.info("Populated preview image for view (ID: {0})".format(view_item.id))
def _get_preview_for_view(self, view_item: ViewItem) -> bytes:
- url = f"{self.siteurl}/workbooks/{view_item.workbook_id}/views/{view_item.id}/previewImage"
+ url = "{0}/workbooks/{1}/views/{2}/previewImage".format(self.siteurl, view_item.workbook_id, view_item.id)
server_response = self.get_request(url)
image = server_response.content
return image
@@ -91,10 +90,10 @@ def image_fetcher():
return self._get_view_image(view_item, req_options)
view_item._set_image(image_fetcher)
- logger.info(f"Populated image for view (ID: {view_item.id})")
+ logger.info("Populated image for view (ID: {0})".format(view_item.id))
def _get_view_image(self, view_item: ViewItem, req_options: Optional["ImageRequestOptions"]) -> bytes:
- url = f"{self.baseurl}/{view_item.id}/image"
+ url = "{0}/{1}/image".format(self.baseurl, view_item.id)
server_response = self.get_request(url, req_options)
image = server_response.content
return image
@@ -109,10 +108,10 @@ def pdf_fetcher():
return self._get_view_pdf(view_item, req_options)
view_item._set_pdf(pdf_fetcher)
- logger.info(f"Populated pdf for view (ID: {view_item.id})")
+ logger.info("Populated pdf for view (ID: {0})".format(view_item.id))
def _get_view_pdf(self, view_item: ViewItem, req_options: Optional["PDFRequestOptions"]) -> bytes:
- url = f"{self.baseurl}/{view_item.id}/pdf"
+ url = "{0}/{1}/pdf".format(self.baseurl, view_item.id)
server_response = self.get_request(url, req_options)
pdf = server_response.content
return pdf
@@ -127,10 +126,10 @@ def csv_fetcher():
return self._get_view_csv(view_item, req_options)
view_item._set_csv(csv_fetcher)
- logger.info(f"Populated csv for view (ID: {view_item.id})")
+ logger.info("Populated csv for view (ID: {0})".format(view_item.id))
def _get_view_csv(self, view_item: ViewItem, req_options: Optional["CSVRequestOptions"]) -> Iterator[bytes]:
- url = f"{self.baseurl}/{view_item.id}/data"
+ url = "{0}/{1}/data".format(self.baseurl, view_item.id)
with closing(self.get_request(url, request_object=req_options, parameters={"stream": True})) as server_response:
yield from server_response.iter_content(1024)
@@ -145,10 +144,10 @@ def excel_fetcher():
return self._get_view_excel(view_item, req_options)
view_item._set_excel(excel_fetcher)
- logger.info(f"Populated excel for view (ID: {view_item.id})")
+ logger.info("Populated excel for view (ID: {0})".format(view_item.id))
def _get_view_excel(self, view_item: ViewItem, req_options: Optional["ExcelRequestOptions"]) -> Iterator[bytes]:
- url = f"{self.baseurl}/{view_item.id}/crosstab/excel"
+ url = "{0}/{1}/crosstab/excel".format(self.baseurl, view_item.id)
with closing(self.get_request(url, request_object=req_options, parameters={"stream": True})) as server_response:
yield from server_response.iter_content(1024)
@@ -177,7 +176,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 944b72502..f71db00cc 100644
--- a/tableauserverclient/server/endpoint/virtual_connections_endpoint.py
+++ b/tableauserverclient/server/endpoint/virtual_connections_endpoint.py
@@ -1,8 +1,7 @@
from functools import partial
import json
from pathlib import Path
-from typing import Optional, TYPE_CHECKING, Union
-from collections.abc import Iterable
+from typing import Iterable, List, Optional, Set, TYPE_CHECKING, Tuple, Union
from tableauserverclient.models.connection_item import ConnectionItem
from tableauserverclient.models.pagination_item import PaginationItem
@@ -29,7 +28,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)
@@ -45,7 +44,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)
@@ -84,7 +83,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)
@@ -160,7 +159,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 06643f99d..597f9c425 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 Optional, TYPE_CHECKING
+from typing import List, Optional, TYPE_CHECKING, Tuple
if TYPE_CHECKING:
from ..server import Server
@@ -15,14 +15,14 @@
class Webhooks(Endpoint):
def __init__(self, parent_srv: "Server") -> None:
- super().__init__(parent_srv)
+ super(Webhooks, self).__init__(parent_srv)
@property
def baseurl(self) -> str:
- return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/webhooks"
+ return "{0}/sites/{1}/webhooks".format(self.parent_srv.baseurl, self.parent_srv.site_id)
@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(f"Querying single webhook (ID: {webhook_id})")
- url = f"{self.baseurl}/{webhook_id}"
+ logger.info("Querying single webhook (ID: {0})".format(webhook_id))
+ url = "{0}/{1}".format(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 = f"{self.baseurl}/{webhook_id}"
+ url = "{0}/{1}".format(self.baseurl, webhook_id)
self.delete_request(url)
- logger.info(f"Deleted single webhook (ID: {webhook_id})")
+ logger.info("Deleted single webhook (ID: {0})".format(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(f"Created new webhook (ID: {new_webhook.id})")
+ logger.info("Created new webhook (ID: {0})".format(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 = f"{self.baseurl}/{webhook_id}/test"
+ url = "{0}/{1}/test".format(self.baseurl, webhook_id)
testOutcome = self.get_request(url)
- logger.info(f"Testing webhook (ID: {webhook_id} returned {testOutcome})")
+ logger.info("Testing webhook (ID: {0} returned {1})".format(webhook_id, testOutcome))
return testOutcome
diff --git a/tableauserverclient/server/endpoint/workbooks_endpoint.py b/tableauserverclient/server/endpoint/workbooks_endpoint.py
index 460017d1a..da6eda3de 100644
--- a/tableauserverclient/server/endpoint/workbooks_endpoint.py
+++ b/tableauserverclient/server/endpoint/workbooks_endpoint.py
@@ -7,7 +7,6 @@
from pathlib import Path
from tableauserverclient.helpers.headers import fix_filename
-from tableauserverclient.models.permissions_item import PermissionsRule
from tableauserverclient.server.query import QuerySet
from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api, parameter_added_in
@@ -26,11 +25,15 @@
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
@@ -58,34 +61,18 @@
class Workbooks(QuerysetEndpoint[WorkbookItem], TaggingMixin[WorkbookItem]):
def __init__(self, parent_srv: "Server") -> None:
- super().__init__(parent_srv)
+ super(Workbooks, self).__init__(parent_srv)
self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl)
return None
@property
def baseurl(self) -> str:
- return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/workbooks"
+ return "{0}/sites/{1}/workbooks".format(self.parent_srv.baseurl, self.parent_srv.site_id)
# Get all workbooks on site
@api(version="2.0")
- def get(self, req_options: Optional["RequestOptions"] = None) -> tuple[list[WorkbookItem], PaginationItem]:
- """
- Queries the server and returns information about the workbooks the site.
-
- Parameters
- ----------
- req_options : RequestOptions, optional
- (Optional) You can pass the method a request object that contains
- additional parameters to filter the request. For example, if you
- were searching for a specific workbook, you could specify the name
- of the workbook or the name of the owner.
-
- Returns
- -------
- Tuple containing one page's worth of workbook items and pagination
- information.
- """
+ 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)
@@ -96,44 +83,18 @@ def get(self, req_options: Optional["RequestOptions"] = None) -> tuple[list[Work
# Get 1 workbook
@api(version="2.0")
def get_by_id(self, workbook_id: str) -> WorkbookItem:
- """
- Returns information about the specified workbook on the site.
-
- Parameters
- ----------
- workbook_id : str
- The workbook ID.
-
- Returns
- -------
- WorkbookItem
- The workbook item.
- """
if not workbook_id:
error = "Workbook ID undefined."
raise ValueError(error)
- logger.info(f"Querying single workbook (ID: {workbook_id})")
- url = f"{self.baseurl}/{workbook_id}"
+ logger.info("Querying single workbook (ID: {0})".format(workbook_id))
+ url = "{0}/{1}".format(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:
- """
- Refreshes the extract of an existing workbook.
-
- Parameters
- ----------
- workbook_item : WorkbookItem | str
- The workbook item or workbook ID.
-
- Returns
- -------
- JobItem
- The job item.
- """
id_ = getattr(workbook_item, "id", workbook_item)
- url = f"{self.baseurl}/{id_}/refresh"
+ url = "{0}/{1}/refresh".format(self.baseurl, id_)
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]
@@ -146,37 +107,10 @@ def create_extract(
workbook_item: WorkbookItem,
encrypt: bool = False,
includeAll: bool = True,
- datasources: Optional[list["DatasourceItem"]] = None,
+ datasources: Optional[List["DatasourceItem"]] = None,
) -> JobItem:
- """
- Create one or more extracts on 1 workbook, optionally encrypted.
-
- REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref.htm#create_extracts_for_workbook
-
- Parameters
- ----------
- workbook_item : WorkbookItem
- The workbook item to create extracts for.
-
- encrypt : bool, default False
- Set to True to encrypt the extracts.
-
- includeAll : bool, default True
- If True, all data sources in the workbook will have an extract
- created for them. If False, then a data source must be supplied in
- the request.
-
- datasources : list[DatasourceItem] | None
- List of DatasourceItem objects for the data sources to create
- extracts for. Only required if includeAll is False.
-
- Returns
- -------
- JobItem
- The job item for the extract creation.
- """
id_ = getattr(workbook_item, "id", workbook_item)
- url = f"{self.baseurl}/{id_}/createExtract?encrypt={encrypt}"
+ url = "{0}/{1}/createExtract?encrypt={2}".format(self.baseurl, id_, encrypt)
datasource_req = RequestFactory.Workbook.embedded_extract_req(includeAll, datasources)
server_response = self.post_request(url, datasource_req)
@@ -186,31 +120,8 @@ def create_extract(
# delete all the extracts on 1 workbook
@api(version="3.3")
def delete_extract(self, workbook_item: WorkbookItem, includeAll: bool = True, datasources=None) -> JobItem:
- """
- Delete all extracts of embedded datasources on 1 workbook.
-
- REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref.htm#delete_extracts_from_workbook
-
- Parameters
- ----------
- workbook_item : WorkbookItem
- The workbook item to delete extracts from.
-
- includeAll : bool, default True
- If True, all data sources in the workbook will have their extracts
- deleted. If False, then a data source must be supplied in the
- request.
-
- datasources : list[DatasourceItem] | None
- List of DatasourceItem objects for the data sources to delete
- extracts from. Only required if includeAll is False.
-
- Returns
- -------
- JobItem
- """
id_ = getattr(workbook_item, "id", workbook_item)
- url = f"{self.baseurl}/{id_}/deleteExtract"
+ url = "{0}/{1}/deleteExtract".format(self.baseurl, id_)
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]
@@ -219,24 +130,12 @@ def delete_extract(self, workbook_item: WorkbookItem, includeAll: bool = True, d
# Delete 1 workbook by id
@api(version="2.0")
def delete(self, workbook_id: str) -> None:
- """
- Deletes a workbook with the specified ID.
-
- Parameters
- ----------
- workbook_id : str
- The workbook ID.
-
- Returns
- -------
- None
- """
if not workbook_id:
error = "Workbook ID undefined."
raise ValueError(error)
- url = f"{self.baseurl}/{workbook_id}"
+ url = "{0}/{1}".format(self.baseurl, workbook_id)
self.delete_request(url)
- logger.info(f"Deleted single workbook (ID: {workbook_id})")
+ logger.info("Deleted single workbook (ID: {0})".format(workbook_id))
# Update workbook
@api(version="2.0")
@@ -246,29 +145,6 @@ def update(
workbook_item: WorkbookItem,
include_view_acceleration_status: bool = False,
) -> WorkbookItem:
- """
- Modifies an existing workbook. Use this method to change the owner or
- the project that the workbook belongs to, or to change whether the
- workbook shows views in tabs. The workbook item must include the
- workbook ID and overrides the existing settings.
-
- See https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_workbooks_and_views.htm#update_workbook
- for a list of fields that can be updated.
-
- Parameters
- ----------
- workbook_item : WorkbookItem
- The workbook item to update. ID is required. Other fields are
- optional. Any fields that are not specified will not be changed.
-
- include_view_acceleration_status : bool, default False
- Set to True to include the view acceleration status in the response.
-
- Returns
- -------
- WorkbookItem
- The updated workbook item.
- """
if not workbook_item.id:
error = "Workbook item missing ID. Workbook must be retrieved from server first."
raise MissingRequiredFieldError(error)
@@ -276,47 +152,27 @@ def update(
self.update_tags(workbook_item)
# Update the workbook itself
- url = f"{self.baseurl}/{workbook_item.id}"
+ url = "{0}/{1}".format(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(f"Updated workbook item (ID: {workbook_item.id})")
+ logger.info("Updated workbook item (ID: {0})".format(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:
- """
- Updates a workbook connection information (server addres, server port,
- user name, and password).
-
- The workbook connections must be populated before the strings can be
- updated.
-
- Rest API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref.htm#update_workbook_connection
-
- Parameters
- ----------
- workbook_item : WorkbookItem
- The workbook item to update.
-
- connection_item : ConnectionItem
- The connection item to update.
-
- Returns
- -------
- ConnectionItem
- The updated connection item.
- """
- url = f"{self.baseurl}/{workbook_item.id}/connections/{connection_item.id}"
+ url = "{0}/{1}/connections/{2}".format(self.baseurl, workbook_item.id, connection_item.id)
update_req = RequestFactory.Connection.update_req(connection_item)
server_response = self.put_request(url, update_req)
connection = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace)[0]
- logger.info(f"Updated workbook item (ID: {workbook_item.id} & connection item {connection_item.id})")
+ logger.info(
+ "Updated workbook item (ID: {0} & connection item {1})".format(workbook_item.id, connection_item.id)
+ )
return connection
# Download workbook contents with option of passing in filepath
@@ -329,34 +185,6 @@ def download(
filepath: Optional[PathOrFileW] = None,
include_extract: bool = True,
) -> PathOrFileW:
- """
- Downloads a workbook to the specified directory (optional).
-
- Parameters
- ----------
- workbook_id : str
- The workbook ID.
-
- filepath : Path or File object, optional
- Downloads the file to the location you specify. If no location is
- specified, the file is downloaded to the current working directory.
- The default is Filepath=None.
-
- include_extract : bool, default True
- Set to False to exclude the extract from the download. The default
- is True.
-
- Returns
- -------
- Path or File object
- The path to the downloaded workbook or the file object.
-
- Raises
- ------
- ValueError
- If the workbook ID is not defined.
- """
-
return self.download_revision(
workbook_id,
None,
@@ -367,48 +195,18 @@ def download(
# Get all views of workbook
@api(version="2.0")
def populate_views(self, workbook_item: WorkbookItem, usage: bool = False) -> None:
- """
- Populates (or gets) a list of views for a workbook.
-
- You must first call this method to populate views before you can iterate
- through the views.
-
- This method retrieves the view information for the specified workbook.
- The REST API is designed to return only the information you ask for
- explicitly. When you query for all the workbooks, the view information
- is not included. Use this method to retrieve the views. The method adds
- the list of views to the workbook item (workbook_item.views). This is a
- list of ViewItem.
-
- Parameters
- ----------
- workbook_item : WorkbookItem
- The workbook item to populate views for.
-
- usage : bool, default False
- Set to True to include usage statistics for each view.
-
- Returns
- -------
- None
-
- Raises
- ------
- MissingRequiredFieldError
- If the workbook item is missing an ID.
- """
if not workbook_item.id:
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(f"Populated views for workbook (ID: {workbook_item.id})")
+ logger.info("Populated views for workbook (ID: {0})".format(workbook_item.id))
- def _get_views_for_workbook(self, workbook_item: WorkbookItem, usage: bool) -> list[ViewItem]:
- url = f"{self.baseurl}/{workbook_item.id}/views"
+ def _get_views_for_workbook(self, workbook_item: WorkbookItem, usage: bool) -> List[ViewItem]:
+ url = "{0}/{1}/views".format(self.baseurl, workbook_item.id)
if usage:
url += "?includeUsageStatistics=true"
server_response = self.get_request(url)
@@ -422,36 +220,6 @@ def _get_views_for_workbook(self, workbook_item: WorkbookItem, usage: bool) -> l
# Get all connections of workbook
@api(version="2.0")
def populate_connections(self, workbook_item: WorkbookItem) -> None:
- """
- Populates a list of data source connections for the specified workbook.
-
- You must populate connections before you can iterate through the
- connections.
-
- This method retrieves the data source connection information for the
- specified workbook. The REST API is designed to return only the
- information you ask for explicitly. When you query all the workbooks,
- the data source connection information is not included. Use this method
- to retrieve the connection information for any data sources used by the
- workbook. The method adds the list of data connections to the workbook
- item (workbook_item.connections). This is a list of ConnectionItem.
-
- REST API docs: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref.htm#query_workbook_connections
-
- Parameters
- ----------
- workbook_item : WorkbookItem
- The workbook item to populate connections for.
-
- Returns
- -------
- None
-
- Raises
- ------
- MissingRequiredFieldError
- If the workbook item is missing an ID.
- """
if not workbook_item.id:
error = "Workbook item missing ID. Workbook must be retrieved from server first."
raise MissingRequiredFieldError(error)
@@ -460,12 +228,12 @@ def connection_fetcher():
return self._get_workbook_connections(workbook_item)
workbook_item._set_connections(connection_fetcher)
- logger.info(f"Populated connections for workbook (ID: {workbook_item.id})")
+ logger.info("Populated connections for workbook (ID: {0})".format(workbook_item.id))
def _get_workbook_connections(
self, workbook_item: WorkbookItem, req_options: Optional["RequestOptions"] = None
- ) -> list[ConnectionItem]:
- url = f"{self.baseurl}/{workbook_item.id}/connections"
+ ) -> List[ConnectionItem]:
+ url = "{0}/{1}/connections".format(self.baseurl, workbook_item.id)
server_response = self.get_request(url, req_options)
connections = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace)
return connections
@@ -473,34 +241,6 @@ def _get_workbook_connections(
# Get the pdf of the entire workbook if its tabs are enabled, pdf of the default view if its tabs are disabled
@api(version="3.4")
def populate_pdf(self, workbook_item: WorkbookItem, req_options: Optional["RequestOptions"] = None) -> None:
- """
- Populates the PDF for the specified workbook item.
-
- This method populates a PDF with image(s) of the workbook view(s) you
- specify.
-
- REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref.htm#download_workbook_pdf
-
- Parameters
- ----------
- workbook_item : WorkbookItem
- The workbook item to populate the PDF for.
-
- req_options : RequestOptions, optional
- (Optional) You can pass in request options to specify the page type
- and orientation of the PDF content, as well as the maximum age of
- the PDF rendered on the server. See PDFRequestOptions class for more
- details.
-
- Returns
- -------
- None
-
- Raises
- ------
- MissingRequiredFieldError
- If the workbook item is missing an ID.
- """
if not workbook_item.id:
error = "Workbook item missing ID."
raise MissingRequiredFieldError(error)
@@ -509,46 +249,16 @@ def pdf_fetcher() -> bytes:
return self._get_wb_pdf(workbook_item, req_options)
workbook_item._set_pdf(pdf_fetcher)
- logger.info(f"Populated pdf for workbook (ID: {workbook_item.id})")
+ logger.info("Populated pdf for workbook (ID: {0})".format(workbook_item.id))
def _get_wb_pdf(self, workbook_item: WorkbookItem, req_options: Optional["RequestOptions"]) -> bytes:
- url = f"{self.baseurl}/{workbook_item.id}/pdf"
+ url = "{0}/{1}/pdf".format(self.baseurl, workbook_item.id)
server_response = self.get_request(url, req_options)
pdf = server_response.content
return pdf
@api(version="3.8")
def populate_powerpoint(self, workbook_item: WorkbookItem, req_options: Optional["RequestOptions"] = None) -> None:
- """
- Populates the PowerPoint for the specified workbook item.
-
- This method populates a PowerPoint with image(s) of the workbook view(s) you
- specify.
-
- REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref.htm#download_workbook_powerpoint
-
- Parameters
- ----------
- workbook_item : WorkbookItem
- The workbook item to populate the PDF for.
-
- req_options : RequestOptions, optional
- (Optional) You can pass in request options to specify the maximum
- number of minutes a workbook .pptx will be cached before being
- refreshed. To prevent multiple .pptx requests from overloading the
- server, the shortest interval you can set is one minute. There is no
- maximum value, but the server job enacting the caching action may
- expire before a long cache period is reached.
-
- Returns
- -------
- None
-
- Raises
- ------
- MissingRequiredFieldError
- If the workbook item is missing an ID.
- """
if not workbook_item.id:
error = "Workbook item missing ID."
raise MissingRequiredFieldError(error)
@@ -557,10 +267,10 @@ def pptx_fetcher() -> bytes:
return self._get_wb_pptx(workbook_item, req_options)
workbook_item._set_powerpoint(pptx_fetcher)
- logger.info(f"Populated powerpoint for workbook (ID: {workbook_item.id})")
+ logger.info("Populated powerpoint for workbook (ID: {0})".format(workbook_item.id))
def _get_wb_pptx(self, workbook_item: WorkbookItem, req_options: Optional["RequestOptions"]) -> bytes:
- url = f"{self.baseurl}/{workbook_item.id}/powerpoint"
+ url = "{0}/{1}/powerpoint".format(self.baseurl, workbook_item.id)
server_response = self.get_request(url, req_options)
pptx = server_response.content
return pptx
@@ -568,26 +278,6 @@ def _get_wb_pptx(self, workbook_item: WorkbookItem, req_options: Optional["Reque
# Get preview image of workbook
@api(version="2.0")
def populate_preview_image(self, workbook_item: WorkbookItem) -> None:
- """
- This method gets the preview image (thumbnail) for the specified workbook item.
-
- This method uses the workbook's ID to get the preview image. The method
- adds the preview image to the workbook item (workbook_item.preview_image).
-
- Parameters
- ----------
- workbook_item : WorkbookItem
- The workbook item to populate the preview image for.
-
- Returns
- -------
- None
-
- Raises
- ------
- MissingRequiredFieldError
- If the workbook item is missing an ID.
- """
if not workbook_item.id:
error = "Workbook item missing ID. Workbook must be retrieved from server first."
raise MissingRequiredFieldError(error)
@@ -596,75 +286,24 @@ def image_fetcher() -> bytes:
return self._get_wb_preview_image(workbook_item)
workbook_item._set_preview_image(image_fetcher)
- logger.info(f"Populated preview image for workbook (ID: {workbook_item.id})")
+ logger.info("Populated preview image for workbook (ID: {0})".format(workbook_item.id))
def _get_wb_preview_image(self, workbook_item: WorkbookItem) -> bytes:
- url = f"{self.baseurl}/{workbook_item.id}/previewImage"
+ url = "{0}/{1}/previewImage".format(self.baseurl, workbook_item.id)
server_response = self.get_request(url)
preview_image = server_response.content
return preview_image
@api(version="2.0")
def populate_permissions(self, item: WorkbookItem) -> None:
- """
- Populates the permissions for the specified workbook item.
-
- REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_permissions.htm#query_workbook_permissions
-
- Parameters
- ----------
- item : WorkbookItem
- The workbook item to populate permissions for.
-
- Returns
- -------
- None
- """
self._permissions.populate(item)
@api(version="2.0")
- def update_permissions(self, resource: WorkbookItem, rules: list[PermissionsRule]) -> list[PermissionsRule]:
- """
- Updates the permissions for the specified workbook item. The method
- replaces the existing permissions with the new permissions. Any missing
- permissions are removed.
-
- REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_permissions.htm#replace_permissions_for_content
-
- Parameters
- ----------
- resource : WorkbookItem
- The workbook item to update permissions for.
-
- rules : list[PermissionsRule]
- A list of permissions rules to apply to the workbook item.
-
- Returns
- -------
- list[PermissionsRule]
- The updated permissions rules.
- """
+ def update_permissions(self, resource, rules):
return self._permissions.update(resource, rules)
@api(version="2.0")
- def delete_permission(self, item: WorkbookItem, capability_item: PermissionsRule) -> None:
- """
- Deletes a single permission rule from the specified workbook item.
-
- REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_permissions.htm#delete_workbook_permission
-
- Parameters
- ----------
- item : WorkbookItem
- The workbook item to delete the permission from.
-
- capability_item : PermissionsRule
- The permission rule to delete.
-
- Returns
- -------
- None
- """
+ def delete_permission(self, item, capability_item):
return self._permissions.delete(item, capability_item)
@api(version="2.0")
@@ -680,87 +319,10 @@ def publish(
skip_connection_check: bool = False,
parameters=None,
):
- """
- Publish a workbook to the specified site.
-
- Note: The REST API cannot automatically include extracts or other
- resources that the workbook uses. Therefore, a .twb file that uses data
- from an Excel or csv file on a local computer cannot be published,
- unless you package the data and workbook in a .twbx file, or publish the
- data source separately.
-
- For workbooks that are larger than 64 MB, the publish method
- automatically takes care of chunking the file in parts for uploading.
- Using this method is considerably more convenient than calling the
- publish REST APIs directly.
-
- REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_workbooks_and_views.htm#publish_workbook
-
- Parameters
- ----------
- workbook_item : WorkbookItem
- The workbook_item specifies the workbook you are publishing. When
- you are adding a workbook, you need to first create a new instance
- of a workbook_item that includes a project_id of an existing
- project. The name of the workbook will be the name of the file,
- unless you also specify a name for the new workbook when you create
- the instance.
-
- file : Path or File object
- The file path or file object of the workbook to publish. When
- providing a file object, you must also specifiy the name of the
- workbook in your instance of the workbook_itemworkbook_item , as
- the name cannot be derived from the file name.
-
- mode : str
- Specifies whether you are publishing a new workbook (CreateNew) or
- overwriting an existing workbook (Overwrite). You cannot appending
- workbooks. You can also use the publish mode attributes, for
- example: TSC.Server.PublishMode.Overwrite.
-
- connections : list[ConnectionItem] | None
- List of ConnectionItems objects for the connections created within
- the workbook.
-
- as_job : bool, default False
- Set to True to run the upload as a job (asynchronous upload). If set
- to True a job will start to perform the publishing process and a Job
- object is returned. Defaults to False.
-
- skip_connection_check : bool, default False
- Set to True to skip connection check at time of upload. Publishing
- will succeed but unchecked connection issues may result in a
- non-functioning workbook. Defaults to False.
-
- Raises
- ------
- OSError
- If the file path does not lead to an existing file.
-
- ServerResponseError
- If the server response is not successful.
-
- TypeError
- If the file is not a file path or file object.
-
- ValueError
- If the file extension is not supported
-
- ValueError
- If the mode is invalid.
-
- ValueError
- Workbooks cannot be appended.
-
- Returns
- -------
- WorkbookItem | JobItem
- The workbook item or job item that was published.
- """
if isinstance(file, (str, os.PathLike)):
if not os.path.isfile(file):
error = "File path does not lead to an existing file."
- raise OSError(error)
+ raise IOError(error)
filename = os.path.basename(file)
file_extension = os.path.splitext(filename)[1][1:]
@@ -784,12 +346,12 @@ def publish(
elif file_type == "xml":
file_extension = "twb"
else:
- error = f"Unsupported file type {file_type}!"
+ error = "Unsupported file type {}!".format(file_type)
raise ValueError(error)
# Generate filename for file object.
# This is needed when publishing the workbook in a single request
- filename = f"{workbook_item.name}.{file_extension}"
+ filename = "{}.{}".format(workbook_item.name, file_extension)
file_size = get_file_object_size(file)
else:
@@ -800,30 +362,30 @@ def publish(
raise ValueError(error)
# Construct the url with the defined mode
- url = f"{self.baseurl}?workbookType={file_extension}"
+ url = "{0}?workbookType={1}".format(self.baseurl, file_extension)
if mode == self.parent_srv.PublishMode.Overwrite:
- url += f"&{mode.lower()}=true"
+ url += "&{0}=true".format(mode.lower())
elif mode == self.parent_srv.PublishMode.Append:
error = "Workbooks cannot be appended."
raise ValueError(error)
if as_job:
- url += "&{}=true".format("asJob")
+ url += "&{0}=true".format("asJob")
if skip_connection_check:
- url += "&{}=true".format("skipConnectionCheck")
+ url += "&{0}=true".format("skipConnectionCheck")
# Determine if chunking is required (64MB is the limit for single upload method)
if file_size >= FILESIZE_LIMIT:
- logger.info(f"Publishing {workbook_item.name} to server with chunking method (workbook over 64MB)")
+ logger.info("Publishing {0} to server with chunking method (workbook over 64MB)".format(workbook_item.name))
upload_session_id = self.parent_srv.fileuploads.upload(file)
- url = f"{url}&uploadSessionId={upload_session_id}"
+ url = "{0}&uploadSessionId={1}".format(url, upload_session_id)
xml_request, content_type = RequestFactory.Workbook.publish_req_chunked(
workbook_item,
connections=connections,
)
else:
- logger.info(f"Publishing {filename} to server")
+ logger.info("Publishing {0} to server".format(filename))
if isinstance(file, (str, Path)):
with open(file, "rb") as f:
@@ -841,7 +403,7 @@ def publish(
file_contents,
connections=connections,
)
- logger.debug(f"Request xml: {redact_xml(xml_request[:1000])} ")
+ logger.debug("Request xml: {0} ".format(redact_xml(xml_request[:1000])))
# Send the publishing request to server
try:
@@ -853,38 +415,16 @@ def publish(
if as_job:
new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0]
- logger.info(f"Published {workbook_item.name} (JOB_ID: {new_job.id}")
+ logger.info("Published {0} (JOB_ID: {1}".format(workbook_item.name, new_job.id))
return new_job
else:
new_workbook = WorkbookItem.from_response(server_response.content, self.parent_srv.namespace)[0]
- logger.info(f"Published {workbook_item.name} (ID: {new_workbook.id})")
+ logger.info("Published {0} (ID: {1})".format(workbook_item.name, new_workbook.id))
return new_workbook
# Populate workbook item's revisions
@api(version="2.3")
def populate_revisions(self, workbook_item: WorkbookItem) -> None:
- """
- Populates (or gets) a list of revisions for a workbook.
-
- You must first call this method to populate revisions before you can
- iterate through the revisions.
-
- REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_workbooks_and_views.htm#get_workbook_revisions
-
- Parameters
- ----------
- workbook_item : WorkbookItem
- The workbook item to populate revisions for.
-
- Returns
- -------
- None
-
- Raises
- ------
- MissingRequiredFieldError
- If the workbook item is missing an ID.
- """
if not workbook_item.id:
error = "Workbook item missing ID. Workbook must be retrieved from server first."
raise MissingRequiredFieldError(error)
@@ -893,12 +433,12 @@ def revisions_fetcher():
return self._get_workbook_revisions(workbook_item)
workbook_item._set_revisions(revisions_fetcher)
- logger.info(f"Populated revisions for workbook (ID: {workbook_item.id})")
+ logger.info("Populated revisions for workbook (ID: {0})".format(workbook_item.id))
def _get_workbook_revisions(
self, workbook_item: WorkbookItem, req_options: Optional["RequestOptions"] = None
- ) -> list[RevisionItem]:
- url = f"{self.baseurl}/{workbook_item.id}/revisions"
+ ) -> List[RevisionItem]:
+ url = "{0}/{1}/revisions".format(self.baseurl, workbook_item.id)
server_response = self.get_request(url, req_options)
revisions = RevisionItem.from_response(server_response.content, self.parent_srv.namespace, workbook_item)
return revisions
@@ -912,47 +452,13 @@ def download_revision(
filepath: Optional[PathOrFileW] = None,
include_extract: bool = True,
) -> PathOrFileW:
- """
- Downloads a workbook revision to the specified directory (optional).
-
- REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_workbooks_and_views.htm#download_workbook_revision
-
- Parameters
- ----------
- workbook_id : str
- The workbook ID.
-
- revision_number : str | None
- The revision number of the workbook. If None, the latest revision is
- downloaded.
-
- filepath : Path or File object, optional
- Downloads the file to the location you specify. If no location is
- specified, the file is downloaded to the current working directory.
- The default is Filepath=None.
-
- include_extract : bool, default True
- Set to False to exclude the extract from the download. The default
- is True.
-
- Returns
- -------
- Path or File object
- The path to the downloaded workbook or the file object.
-
- Raises
- ------
- ValueError
- If the workbook ID is not defined.
- """
-
if not workbook_id:
error = "Workbook ID undefined."
raise ValueError(error)
if revision_number is None:
- url = f"{self.baseurl}/{workbook_id}/content"
+ url = "{0}/{1}/content".format(self.baseurl, workbook_id)
else:
- url = f"{self.baseurl}/{workbook_id}/revisions/{revision_number}/content"
+ url = "{0}/{1}/revisions/{2}/content".format(self.baseurl, workbook_id, revision_number)
if not include_extract:
url += "?includeExtract=False"
@@ -974,129 +480,37 @@ def download_revision(
f.write(chunk)
return_path = os.path.abspath(download_path)
- logger.info(f"Downloaded workbook revision {revision_number} to {return_path} (ID: {workbook_id})")
+ logger.info(
+ "Downloaded workbook revision {0} to {1} (ID: {2})".format(revision_number, return_path, workbook_id)
+ )
return return_path
@api(version="2.3")
def delete_revision(self, workbook_id: str, revision_number: str) -> None:
- """
- Deletes a specific revision from a workbook on Tableau Server.
-
- REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_revisions.htm#remove_workbook_revision
-
- Parameters
- ----------
- workbook_id : str
- The workbook ID.
-
- revision_number : str
- The revision number of the workbook to delete.
-
- Returns
- -------
- None
-
- Raises
- ------
- ValueError
- If the workbook ID or revision number is not defined.
- """
if workbook_id is None or revision_number is None:
raise ValueError
url = "/".join([self.baseurl, workbook_id, "revisions", revision_number])
self.delete_request(url)
- logger.info(f"Deleted single workbook revision (ID: {workbook_id}) (Revision: {revision_number})")
+ logger.info("Deleted single workbook revision (ID: {0}) (Revision: {1})".format(workbook_id, 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
- """
- Adds a workbook to a schedule for extract refresh.
-
- REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_jobs_tasks_and_schedules.htm#add_workbook_to_schedule
-
- Parameters
- ----------
- schedule_id : str
- The schedule ID.
-
- item : WorkbookItem
- The workbook item to add to the schedule.
-
- Returns
- -------
- list[AddResponse]
- The response from the server.
- """
+ ) -> 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]:
- """
- Adds tags to a workbook. One or more tags may be added at a time. If a
- tag already exists on the workbook, it will not be duplicated.
-
- REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_workbooks_and_views.htm#add_tags_to_workbook
-
- Parameters
- ----------
- item : WorkbookItem | str
- The workbook item or workbook ID to add tags to.
-
- tags : Iterable[str] | str
- The tag or tags to add to the workbook. Tags can be a single tag or
- a list of tags.
-
- Returns
- -------
- set[str]
- The set of tags added to the workbook.
- """
+ 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")
def delete_tags(self, item: Union[WorkbookItem, str], tags: Union[Iterable[str], str]) -> None:
- """
- Deletes tags from a workbook. One or more tags may be deleted at a time.
-
- REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_workbooks_and_views.htm#delete_tag_from_workbook
-
- Parameters
- ----------
- item : WorkbookItem | str
- The workbook item or workbook ID to delete tags from.
-
- tags : Iterable[str] | str
- The tag or tags to delete from the workbook. Tags can be a single
- tag or a list of tags.
-
- Returns
- -------
- None
- """
return super().delete_tags(item, tags)
@api(version="1.0")
def update_tags(self, item: WorkbookItem) -> None:
- """
- Updates the tags on a workbook. This method is used to update the tags
- on the server to match the tags on the workbook item. This method is a
- convenience method that calls add_tags and delete_tags to update the
- tags on the server.
-
- Parameters
- ----------
- item : WorkbookItem
- The workbook item to update the tags for. The tags on the workbook
- item will be used to update the tags on the server.
-
- Returns
- -------
- None
- """
return super().update_tags(item)
def filter(self, *invalid, page_size: Optional[int] = None, **kwargs) -> QuerySet[WorkbookItem]:
diff --git a/tableauserverclient/server/filter.py b/tableauserverclient/server/filter.py
index fd90e281f..b936ceb92 100644
--- a/tableauserverclient/server/filter.py
+++ b/tableauserverclient/server/filter.py
@@ -1,7 +1,7 @@
from .request_options import RequestOptions
-class Filter:
+class Filter(object):
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 f"{self.field}:{self.operator}:{value_string}"
+ return "{0}:{1}:{2}".format(self.field, self.operator, value_string)
@property
def value(self):
diff --git a/tableauserverclient/server/pager.py b/tableauserverclient/server/pager.py
index e6d261b61..ca9d83872 100644
--- a/tableauserverclient/server/pager.py
+++ b/tableauserverclient/server/pager.py
@@ -1,7 +1,6 @@
import copy
from functools import partial
-from typing import Optional, Protocol, TypeVar, Union, runtime_checkable
-from collections.abc import Iterable, Iterator
+from typing import Iterable, Iterator, List, Optional, Protocol, Tuple, TypeVar, Union, runtime_checkable
from tableauserverclient.models.pagination_item import PaginationItem
from tableauserverclient.server.request_options import RequestOptions
@@ -12,12 +11,14 @@
@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]):
@@ -26,7 +27,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 801ad4a13..bbca612e9 100644
--- a/tableauserverclient/server/query.py
+++ b/tableauserverclient/server/query.py
@@ -1,10 +1,8 @@
-from collections.abc import Iterable, Iterator, Sized
+from collections.abc import Sized
from itertools import count
-from typing import Optional, Protocol, TYPE_CHECKING, TypeVar, overload
-import sys
+from typing import Iterable, Iterator, List, Optional, Protocol, Tuple, TYPE_CHECKING, TypeVar, overload
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
@@ -36,36 +34,10 @@ 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]:
@@ -77,30 +49,19 @@ def __iter__(self: Self) -> Iterator[T]:
for page in count(1):
self.request_options.pagenumber = page
self._result_cache = []
- self._pagination_item._page_number = None
- 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
- if len(self._result_cache) == 0:
- return
+ self._fetch_all()
yield from self._result_cache
- # 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:
+ # Set result_cache to empty so the fetch will populate
+ if (page * self.page_size) >= len(self):
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
@@ -142,7 +103,6 @@ def __getitem__(self, k):
elif k in range(self.total_available):
# Otherwise, check if k is even sensible to return
self._result_cache = []
- self._pagination_item._page_number = None
# Add one to k, otherwise it gets stuck at page boundaries, e.g. 100
self.request_options.pagenumber = max(1, math.ceil((k + 1) / size))
return self[k]
@@ -154,16 +114,11 @@ def _fetch_all(self: Self) -> None:
"""
Retrieve the data and store result and pagination item in cache
"""
- if not self._result_cache and self._pagination_item._page_number is None:
- 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()
+ if not self._result_cache:
+ self._result_cache, self._pagination_item = self.model.get(self.request_options)
def __len__(self: Self) -> int:
- return sys.maxsize if self.total_available is None else self.total_available
+ return self.total_available
@property
def total_available(self: Self) -> int:
@@ -173,16 +128,12 @@ def total_available(self: Self) -> int:
@property
def page_number(self: Self) -> int:
self._fetch_all()
- # 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
+ return self._pagination_item.page_number
@property
def page_size(self: Self) -> int:
self._fetch_all()
- # 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
+ return self._pagination_item.page_size
def filter(self: Self, *invalid, page_size: Optional[int] = None, **kwargs) -> Self:
if invalid:
@@ -209,22 +160,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(f"Operator `{operator}` is not valid.")
+ raise ValueError("Operator `{}` is not valid.".format(operator))
field = to_camel_case(tokens[0])
if field not in RequestOptions.Field.__dict__.values():
- raise ValueError(f"Field name `{field}` is not valid.")
+ raise ValueError("Field name `{}` is not valid.".format(field))
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 f7bd139d7..96fa14680 100644
--- a/tableauserverclient/server/request_factory.py
+++ b/tableauserverclient/server/request_factory.py
@@ -1,6 +1,5 @@
import xml.etree.ElementTree as ET
-from typing import Any, Callable, Optional, TypeVar, TYPE_CHECKING, Union
-from collections.abc import Iterable
+from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, TypeVar, TYPE_CHECKING, Union
from typing_extensions import ParamSpec
@@ -16,7 +15,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)
@@ -81,7 +80,7 @@ def _add_credentials_element(parent_element, connection_credentials):
credentials_element.attrib["oAuth"] = "true"
-class AuthRequest:
+class AuthRequest(object):
def signin_req(self, auth_item):
xml_request = ET.Element("tsRequest")
@@ -105,7 +104,7 @@ def switch_req(self, site_content_url):
return ET.tostring(xml_request)
-class ColumnRequest:
+class ColumnRequest(object):
def update_req(self, column_item):
xml_request = ET.Element("tsRequest")
column_element = ET.SubElement(xml_request, "column")
@@ -116,7 +115,7 @@ def update_req(self, column_item):
return ET.tostring(xml_request)
-class DataAlertRequest:
+class DataAlertRequest(object):
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")
@@ -141,7 +140,7 @@ def update_req(self, alert_item: "DataAlertItem") -> bytes:
return ET.tostring(xml_request)
-class DatabaseRequest:
+class DatabaseRequest(object):
def update_req(self, database_item):
xml_request = ET.Element("tsRequest")
database_element = ET.SubElement(xml_request, "database")
@@ -160,7 +159,7 @@ def update_req(self, database_item):
return ET.tostring(xml_request)
-class DatasourceRequest:
+class DatasourceRequest(object):
def _generate_xml(self, datasource_item: DatasourceItem, connection_credentials=None, connections=None):
xml_request = ET.Element("tsRequest")
datasource_element = ET.SubElement(xml_request, "datasource")
@@ -245,7 +244,7 @@ def publish_req_chunked(self, datasource_item, connection_credentials=None, conn
return _add_multipart(parts)
-class DQWRequest:
+class DQWRequest(object):
def add_req(self, dqw_item):
xml_request = ET.Element("tsRequest")
dqw_element = ET.SubElement(xml_request, "dataQualityWarning")
@@ -275,7 +274,7 @@ def update_req(self, dqw_item):
return ET.tostring(xml_request)
-class FavoriteRequest:
+class FavoriteRequest(object):
def add_request(self, id_: Optional[str], target_type: str, label: Optional[str]) -> bytes:
"""
@@ -330,7 +329,7 @@ def add_workbook_req(self, id_: Optional[str], name: Optional[str]) -> bytes:
return self.add_request(id_, Resource.Workbook, name)
-class FileuploadRequest:
+class FileuploadRequest(object):
def chunk_req(self, chunk):
parts = {
"request_payload": ("", "", "text/xml"),
@@ -339,8 +338,8 @@ def chunk_req(self, chunk):
return _add_multipart(parts)
-class FlowRequest:
- def _generate_xml(self, flow_item: "FlowItem", connections: Optional[list["ConnectionItem"]] = None) -> bytes:
+class FlowRequest(object):
+ 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:
@@ -371,8 +370,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 = {
@@ -381,14 +380,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:
+class GroupRequest(object):
def add_user_req(self, user_id: str) -> bytes:
xml_request = ET.Element("tsRequest")
user_element = ET.SubElement(xml_request, "user")
@@ -478,7 +477,7 @@ def update_req(
return ET.tostring(xml_request)
-class PermissionRequest:
+class PermissionRequest(object):
def add_req(self, rules: Iterable[PermissionsRule]) -> bytes:
xml_request = ET.Element("tsRequest")
permissions_element = ET.SubElement(xml_request, "permissions")
@@ -500,7 +499,7 @@ def _add_all_capabilities(self, capabilities_element, capabilities_map):
capability_element.attrib["mode"] = mode
-class ProjectRequest:
+class ProjectRequest(object):
def update_req(self, project_item: "ProjectItem") -> bytes:
xml_request = ET.Element("tsRequest")
project_element = ET.SubElement(xml_request, "project")
@@ -531,7 +530,7 @@ def create_req(self, project_item: "ProjectItem") -> bytes:
return ET.tostring(xml_request)
-class ScheduleRequest:
+class ScheduleRequest(object):
def create_req(self, schedule_item):
xml_request = ET.Element("tsRequest")
schedule_element = ET.SubElement(xml_request, "schedule")
@@ -610,7 +609,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:
+class SiteRequest(object):
def update_req(self, site_item: "SiteItem", parent_srv: Optional["Server"] = None):
xml_request = ET.Element("tsRequest")
site_element = ET.SubElement(xml_request, "site")
@@ -849,7 +848,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:
+class TableRequest(object):
def update_req(self, table_item):
xml_request = ET.Element("tsRequest")
table_element = ET.SubElement(xml_request, "table")
@@ -872,7 +871,7 @@ def update_req(self, table_item):
content_types = Iterable[Union["ColumnItem", "DatabaseItem", "DatasourceItem", "FlowItem", "TableItem", "WorkbookItem"]]
-class TagRequest:
+class TagRequest(object):
def add_req(self, tag_set):
xml_request = ET.Element("tsRequest")
tags_element = ET.SubElement(xml_request, "tags")
@@ -882,7 +881,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:
@@ -898,7 +897,7 @@ def batch_create(self, element: ET.Element, tags: set[str], content: content_typ
return ET.tostring(element)
-class UserRequest:
+class UserRequest(object):
def update_req(self, user_item: UserItem, password: Optional[str]) -> bytes:
xml_request = ET.Element("tsRequest")
user_element = ET.SubElement(xml_request, "user")
@@ -932,7 +931,7 @@ def add_req(self, user_item: UserItem) -> bytes:
return ET.tostring(xml_request)
-class WorkbookRequest:
+class WorkbookRequest(object):
def _generate_xml(
self,
workbook_item,
@@ -996,9 +995,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.")
@@ -1076,7 +1075,7 @@ def embedded_extract_req(
datasource_element.attrib["id"] = id_
-class Connection:
+class Connection(object):
@_tsrequest_wrapped
def update_req(self, xml_request: ET.Element, connection_item: "ConnectionItem") -> None:
connection_element = ET.SubElement(xml_request, "connection")
@@ -1099,7 +1098,7 @@ def update_req(self, xml_request: ET.Element, connection_item: "ConnectionItem")
connection_element.attrib["queryTaggingEnabled"] = str(connection_item.query_tagging).lower()
-class TaskRequest:
+class TaskRequest(object):
@_tsrequest_wrapped
def run_req(self, xml_request: ET.Element, task_item: Any) -> None:
# Send an empty tsRequest
@@ -1138,7 +1137,7 @@ def create_extract_req(self, xml_request: ET.Element, extract_item: "TaskItem")
return ET.tostring(xml_request)
-class FlowTaskRequest:
+class FlowTaskRequest(object):
@_tsrequest_wrapped
def create_flow_task_req(self, xml_request: ET.Element, flow_item: "TaskItem") -> bytes:
flow_element = ET.SubElement(xml_request, "runFlow")
@@ -1172,7 +1171,7 @@ def create_flow_task_req(self, xml_request: ET.Element, flow_item: "TaskItem") -
return ET.tostring(xml_request)
-class SubscriptionRequest:
+class SubscriptionRequest(object):
@_tsrequest_wrapped
def create_req(self, xml_request: ET.Element, subscription_item: "SubscriptionItem") -> bytes:
subscription_element = ET.SubElement(xml_request, "subscription")
@@ -1236,13 +1235,13 @@ def update_req(self, xml_request: ET.Element, subscription_item: "SubscriptionIt
return ET.tostring(xml_request)
-class EmptyRequest:
+class EmptyRequest(object):
@_tsrequest_wrapped
def empty_req(self, xml_request: ET.Element) -> None:
pass
-class WebhookRequest:
+class WebhookRequest(object):
@_tsrequest_wrapped
def create_req(self, xml_request: ET.Element, webhook_item: "WebhookItem") -> bytes:
webhook = ET.SubElement(xml_request, "webhook")
@@ -1288,7 +1287,7 @@ def update_req(self, xml_request: ET.Element, metric_item: MetricItem) -> bytes:
return ET.tostring(xml_request)
-class CustomViewRequest:
+class CustomViewRequest(object):
@_tsrequest_wrapped
def update_req(self, xml_request: ET.Element, custom_view_item: CustomViewItem):
updating_element = ET.SubElement(xml_request, "customView")
@@ -1416,7 +1415,7 @@ def publish(self, xml_request: ET.Element, virtual_connection: VirtualConnection
return ET.tostring(xml_request)
-class RequestFactory:
+class RequestFactory(object):
Auth = AuthRequest()
Connection = Connection()
Column = ColumnRequest()
diff --git a/tableauserverclient/server/request_options.py b/tableauserverclient/server/request_options.py
index d79ac7f73..ddb45834d 100644
--- a/tableauserverclient/server/request_options.py
+++ b/tableauserverclient/server/request_options.py
@@ -1,5 +1,4 @@
import sys
-from typing import Optional
from typing_extensions import Self
@@ -10,12 +9,12 @@
from tableauserverclient.helpers.logging import logger
-class RequestOptionsBase:
+class RequestOptionsBase(object):
# 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 = [f"{k}={v}" for (k, v) in params.items()]
+ params_list = ["{}={}".format(k, v) for (k, v) in params.items()]
logger.debug("Applying options to request: <%s(%s)>", self.__class__.__name__, ",".join(params_list))
@@ -23,52 +22,15 @@ def apply_query_params(self, url):
url, existing_params = url.split("?")
params_list.append(existing_params)
- return "{}?{}".format(url, "&".join(params_list))
+ return "{0}?{1}".format(url, "&".join(params_list))
except NotImplementedError:
raise
-
-# If it wasn't a breaking change, I'd rename it to QueryOptions
-"""
-This class manages options can be used when querying content on the server
-"""
+ def get_query_params(self):
+ raise NotImplementedError()
class RequestOptions(RequestOptionsBase):
- def __init__(self, pagenumber=1, pagesize=None):
- self.pagenumber = pagenumber
- self.pagesize = pagesize or config.PAGE_SIZE
- self.sort = set()
- self.filter = set()
- # This is private until we expand all of our parsers to handle the extra fields
- self._all_fields = False
-
- def get_query_params(self) -> dict:
- params = {}
- if self.sort and len(self.sort) > 0:
- sort_options = (str(sort_item) for sort_item in self.sort)
- ordered_sort_options = sorted(sort_options)
- params["sort"] = ",".join(ordered_sort_options)
- if len(self.filter) > 0:
- filter_options = (str(filter_item) for filter_item in self.filter)
- ordered_filter_options = sorted(filter_options)
- params["filter"] = ",".join(ordered_filter_options)
- if self._all_fields:
- params["fields"] = "_all_"
- if self.pagenumber:
- params["pageNumber"] = self.pagenumber
- if self.pagesize:
- params["pageSize"] = self.pagesize
- return params
-
- def page_size(self, page_size):
- self.pagesize = page_size
- return self
-
- def page_number(self, page_number):
- self.pagenumber = page_number
- return self
-
class Operator:
Equals = "eq"
GreaterThan = "gt"
@@ -79,7 +41,6 @@ class Operator:
Has = "has"
CaseInsensitiveEquals = "cieq"
- # These are fields in the REST API
class Field:
Args = "args"
AuthenticationType = "authenticationType"
@@ -156,53 +117,60 @@ class Direction:
Desc = "desc"
Asc = "asc"
+ def __init__(self, pagenumber=1, pagesize=None):
+ self.pagenumber = pagenumber
+ self.pagesize = pagesize or config.PAGE_SIZE
+ self.sort = set()
+ self.filter = set()
-"""
-These options can be used by methods that are fetching data exported from a specific content item
-"""
-
-
-class _DataExportOptions(RequestOptionsBase):
- def __init__(self, maxage: int = -1):
- super().__init__()
- self.view_filters: list[tuple[str, str]] = []
- self.view_parameters: list[tuple[str, str]] = []
- self.max_age: Optional[int] = maxage
- """
- This setting will affect the contents of the workbook as they are exported.
- Valid language values are tableau-supported languages like de, es, en
- If no locale is specified, the default locale for that language will be used
- """
- self.language: Optional[str] = None
+ # This is private until we expand all of our parsers to handle the extra fields
+ self._all_fields = False
- @property
- def max_age(self) -> int:
- return self._max_age
+ def page_size(self, page_size):
+ self.pagesize = page_size
+ return self
- @max_age.setter
- @property_is_int(range=(0, 240), allowed=[-1])
- def max_age(self, value):
- self._max_age = value
+ def page_number(self, page_number):
+ self.pagenumber = page_number
+ return self
def get_query_params(self):
params = {}
- if self.max_age != -1:
- params["maxAge"] = self.max_age
- if self.language:
- params["language"] = self.language
-
- self._append_view_filters(params)
+ if self.pagenumber:
+ params["pageNumber"] = self.pagenumber
+ if self.pagesize:
+ params["pageSize"] = self.pagesize
+ if len(self.sort) > 0:
+ sort_options = (str(sort_item) for sort_item in self.sort)
+ ordered_sort_options = sorted(sort_options)
+ params["sort"] = ",".join(ordered_sort_options)
+ if len(self.filter) > 0:
+ filter_options = (str(filter_item) for filter_item in self.filter)
+ ordered_filter_options = sorted(filter_options)
+ params["filter"] = ",".join(ordered_filter_options)
+ if self._all_fields:
+ params["fields"] = "_all_"
return params
+
+class _FilterOptionsBase(RequestOptionsBase):
+ """Provide a basic implementation of adding view filters to the url"""
+
+ def __init__(self):
+ self.view_filters = []
+ self.view_parameters = []
+
+ def get_query_params(self):
+ raise NotImplementedError()
+
def vf(self, name: str, value: str) -> Self:
- """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'"""
+ """Apply a filter to the view for a filter that is a normal column
+ within the view."""
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.
- Note that when filtering on a boolean type field, the only valid values are 'true' and 'false'"""
+ """Apply a filter based on a parameter within the workbook."""
self.view_parameters.append((name, value))
return self
@@ -213,73 +181,82 @@ def _append_view_filters(self, params) -> None:
params[name] = value
-class _ImagePDFCommonExportOptions(_DataExportOptions):
- def __init__(self, maxage=-1, viz_height=None, viz_width=None):
- super().__init__(maxage=maxage)
- self.viz_height = viz_height
- self.viz_width = viz_width
+class CSVRequestOptions(_FilterOptionsBase):
+ def __init__(self, maxage=-1):
+ super(CSVRequestOptions, self).__init__()
+ self.max_age = maxage
@property
- def viz_height(self):
- return self._viz_height
-
- @viz_height.setter
- @property_is_int(range=(0, sys.maxsize), allowed=(None,))
- def viz_height(self, value):
- self._viz_height = value
-
- @property
- def viz_width(self):
- return self._viz_width
-
- @viz_width.setter
- @property_is_int(range=(0, sys.maxsize), allowed=(None,))
- def viz_width(self, value):
- self._viz_width = value
-
- def get_query_params(self) -> dict:
- params = super().get_query_params()
-
- # XOR. Either both are None or both are not None.
- if (self.viz_height is None) ^ (self.viz_width is None):
- raise ValueError("viz_height and viz_width must be specified together")
+ def max_age(self):
+ return self._max_age
- if self.viz_height is not None:
- params["vizHeight"] = self.viz_height
+ @max_age.setter
+ @property_is_int(range=(0, 240), allowed=[-1])
+ def max_age(self, value):
+ self._max_age = value
- if self.viz_width is not None:
- params["vizWidth"] = self.viz_width
+ def get_query_params(self):
+ params = {}
+ if self.max_age != -1:
+ params["maxAge"] = self.max_age
+ self._append_view_filters(params)
return params
-class CSVRequestOptions(_DataExportOptions):
- extension = "csv"
+class ExcelRequestOptions(_FilterOptionsBase):
+ def __init__(self, maxage: int = -1) -> None:
+ super().__init__()
+ self.max_age = maxage
+ @property
+ def max_age(self) -> int:
+ return self._max_age
+
+ @max_age.setter
+ @property_is_int(range=(0, 240), allowed=[-1])
+ def max_age(self, value: int) -> None:
+ self._max_age = value
-class ExcelRequestOptions(_DataExportOptions):
- extension = "xlsx"
+ def get_query_params(self):
+ params = {}
+ if self.max_age != -1:
+ params["maxAge"] = self.max_age
+ self._append_view_filters(params)
+ return params
-class ImageRequestOptions(_ImagePDFCommonExportOptions):
- extension = "png"
+class ImageRequestOptions(_FilterOptionsBase):
# if 'high' isn't specified, the REST API endpoint returns an image with standard resolution
class Resolution:
High = "high"
- def __init__(self, imageresolution=None, maxage=-1, viz_height=None, viz_width=None):
- super().__init__(maxage=maxage, viz_height=viz_height, viz_width=viz_width)
+ def __init__(self, imageresolution=None, maxage=-1):
+ super(ImageRequestOptions, self).__init__()
self.image_resolution = imageresolution
+ self.max_age = maxage
+
+ @property
+ def max_age(self):
+ return self._max_age
+
+ @max_age.setter
+ @property_is_int(range=(0, 240), allowed=[-1])
+ def max_age(self, value):
+ self._max_age = value
def get_query_params(self):
- params = super().get_query_params()
+ params = {}
if self.image_resolution:
params["resolution"] = self.image_resolution
+ if self.max_age != -1:
+ params["maxAge"] = self.max_age
+ self._append_view_filters(params)
return params
-class PDFRequestOptions(_ImagePDFCommonExportOptions):
+class PDFRequestOptions(_FilterOptionsBase):
class PageType:
A3 = "a3"
A4 = "a4"
@@ -301,16 +278,61 @@ class Orientation:
Landscape = "landscape"
def __init__(self, page_type=None, orientation=None, maxage=-1, viz_height=None, viz_width=None):
- super().__init__(maxage=maxage, viz_height=viz_height, viz_width=viz_width)
+ super(PDFRequestOptions, self).__init__()
self.page_type = page_type
self.orientation = orientation
+ self.max_age = maxage
+ self.viz_height = viz_height
+ self.viz_width = viz_width
+
+ @property
+ def max_age(self):
+ return self._max_age
+
+ @max_age.setter
+ @property_is_int(range=(0, 240), allowed=[-1])
+ def max_age(self, value):
+ self._max_age = value
+
+ @property
+ def viz_height(self):
+ return self._viz_height
+
+ @viz_height.setter
+ @property_is_int(range=(0, sys.maxsize), allowed=(None,))
+ def viz_height(self, value):
+ self._viz_height = value
+
+ @property
+ def viz_width(self):
+ return self._viz_width
+
+ @viz_width.setter
+ @property_is_int(range=(0, sys.maxsize), allowed=(None,))
+ def viz_width(self, value):
+ self._viz_width = value
- def get_query_params(self) -> dict:
- params = super().get_query_params()
+ def get_query_params(self):
+ params = {}
if self.page_type:
params["type"] = self.page_type
if self.orientation:
params["orientation"] = self.orientation
+ if self.max_age != -1:
+ params["maxAge"] = self.max_age
+
+ # XOR. Either both are None or both are not None.
+ if (self.viz_height is None) ^ (self.viz_width is None):
+ raise ValueError("viz_height and viz_width must be specified together")
+
+ if self.viz_height is not None:
+ params["vizHeight"] = self.viz_height
+
+ if self.viz_width is not None:
+ params["vizWidth"] = self.viz_width
+
+ self._append_view_filters(params)
+
return params
diff --git a/tableauserverclient/server/server.py b/tableauserverclient/server/server.py
index 4eeefcaf9..e563a7138 100644
--- a/tableauserverclient/server/server.py
+++ b/tableauserverclient/server/server.py
@@ -58,64 +58,8 @@
default_server_version = "2.4" # first version that dropped the legacy auth endpoint
-class Server:
- """
- In the Tableau REST API, the server (https://MY-SERVER/) is the base or core
- of the URI that makes up the various endpoints or methods for accessing
- resources on the server (views, workbooks, sites, users, data sources, etc.)
- The TSC library provides a Server class that represents the server. You
- create a server instance to sign in to the server and to call the various
- methods for accessing resources.
-
- The Server class contains the attributes that represent the server on
- Tableau Server. After you create an instance of the Server class, you can
- sign in to the server and call methods to access all of the resources on the
- server.
-
- Parameters
- ----------
- server_address : str
- Specifies the address of the Tableau Server or Tableau Cloud (for
- example, https://MY-SERVER/).
-
- use_server_version : bool
- Specifies the version of the REST API to use (for example, '2.5'). When
- you use the TSC library to call methods that access Tableau Server, the
- version is passed to the endpoint as part of the URI
- (https://MY-SERVER/api/2.5/). Each release of Tableau Server supports
- specific versions of the REST API. New versions of the REST API are
- released with Tableau Server. By default, the value of version is set to
- '2.3', which corresponds to Tableau Server 10.0. You can view or set
- this value. You might need to set this to a different value, for
- example, if you want to access features that are supported by the server
- and a later version of the REST API. For more information, see REST API
- Versions.
-
- Examples
- --------
- >>> import tableauserverclient as TSC
-
- >>> # create a instance of server
- >>> server = TSC.Server('https://MY-SERVER')
-
- >>> # sign in, etc.
-
- >>> # change the REST API version to match the server
- >>> server.use_server_version()
-
- >>> # or change the REST API version to match a specific version
- >>> # for example, 2.8
- >>> # server.version = '2.8'
-
- """
-
+class Server(object):
class PublishMode:
- """
- Enumerates the options that specify what happens when you publish a
- workbook or data source. The options are Overwrite, Append, or
- CreateNew.
- """
-
Append = "Append"
Overwrite = "Overwrite"
CreateNew = "CreateNew"
@@ -186,7 +130,7 @@ def validate_connection_settings(self):
raise ValueError("Server connection settings not valid", req_ex)
def __repr__(self):
- return f""
+ return "".format(self.baseurl, self.server_info.serverInfo)
def add_http_options(self, options_dict: dict):
try:
@@ -198,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(f"Invalid http options given: {options_dict}")
+ raise ValueError("Invalid http options given: {}".format(options_dict))
def clear_http_options(self):
self._http_options = dict()
@@ -232,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(f"Could not get version info from server: {e.__class__}{e}")
+ logger.info("Could not get version info from server: {}{}".format(e.__class__, e))
version = self._get_legacy_version()
except EndpointUnavailableError as e:
- logger.info(f"Could not get version info from server: {e.__class__}{e}")
+ logger.info("Could not get version info from server: {}{}".format(e.__class__, e))
version = self._get_legacy_version()
except Exception as e:
- logger.info(f"Could not get version info from server: {e.__class__}{e}")
+ logger.info("Could not get version info from server: {}{}".format(e.__class__, e))
version = None
- logger.info(f"versions: {version}, {old_version}")
+ logger.info("versions: {}, {}".format(version, old_version))
return version or old_version
def use_server_version(self):
@@ -257,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 = f"{reason} is not available in API version {self.version}. Requires {comparison}"
+ error = "{} is not available in API version {}. Requires {}".format(reason, self.version, comparison)
raise EndpointUnavailableError(error)
@property
def baseurl(self):
- return f"{self._server_address}/api/{str(self.version)}"
+ return "{0}/api/{1}".format(self._server_address, str(self.version))
@property
def namespace(self):
diff --git a/tableauserverclient/server/sort.py b/tableauserverclient/server/sort.py
index 839a8c8db..2d6bc030a 100644
--- a/tableauserverclient/server/sort.py
+++ b/tableauserverclient/server/sort.py
@@ -1,7 +1,7 @@
-class Sort:
+class Sort(object):
def __init__(self, field, direction):
self.field = field
self.direction = direction
def __str__(self):
- return f"{self.field}:{self.direction}"
+ return "{0}:{1}".format(self.field, self.direction)
diff --git a/test/_utils.py b/test/_utils.py
index b4ee93bc3..8527aaf8c 100644
--- a/test/_utils.py
+++ b/test/_utils.py
@@ -1,6 +1,5 @@
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")
@@ -19,19 +18,6 @@ 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 489e8ac63..bdce4cdfb 100644
--- a/test/assets/flow_runs_get.xml
+++ b/test/assets/flow_runs_get.xml
@@ -1,4 +1,5 @@
+
-
+
\ No newline at end of file
diff --git a/test/assets/server_info_wrong_site.html b/test/assets/server_info_wrong_site.html
deleted file mode 100644
index e92daeb2d..000000000
--- a/test/assets/server_info_wrong_site.html
+++ /dev/null
@@ -1,56 +0,0 @@
-
-
-
-
-
- Example website
-
-
-
-
-
- | A |
- B |
- C |
- D |
- E |
-
-
- 1 |
- 2 |
- 3 |
- 4 |
- 5 |
-
-
- 2 |
- 3 |
- 4 |
- 5 |
- 6 |
-
-
- 3 |
- 4 |
- 5 |
- 6 |
- 7 |
-
-
- 4 |
- 5 |
- 6 |
- 7 |
- 8 |
-
-
- 5 |
- 6 |
- 7 |
- 8 |
- 9 |
-
-
-
-
-
\ No newline at end of file
diff --git a/test/test_auth.py b/test/test_auth.py
index 48100ad88..eaf13481e 100644
--- a/test/test_auth.py
+++ b/test/test_auth.py
@@ -63,7 +63,7 @@ def test_sign_in_error(self):
with requests_mock.mock() as m:
m.post(self.baseurl + "/signin", text=response_xml, status_code=401)
tableau_auth = TSC.TableauAuth("testuser", "wrongpassword")
- self.assertRaises(TSC.FailedSignInError, self.server.auth.sign_in, tableau_auth)
+ self.assertRaises(TSC.NotSignedInError, self.server.auth.sign_in, tableau_auth)
def test_sign_in_invalid_token(self):
with open(SIGN_IN_ERROR_XML, "rb") as f:
@@ -71,7 +71,7 @@ def test_sign_in_invalid_token(self):
with requests_mock.mock() as m:
m.post(self.baseurl + "/signin", text=response_xml, status_code=401)
tableau_auth = TSC.PersonalAccessTokenAuth(token_name="mytoken", personal_access_token="invalid")
- self.assertRaises(TSC.FailedSignInError, self.server.auth.sign_in, tableau_auth)
+ self.assertRaises(TSC.NotSignedInError, self.server.auth.sign_in, tableau_auth)
def test_sign_in_without_auth(self):
with open(SIGN_IN_ERROR_XML, "rb") as f:
@@ -79,7 +79,7 @@ def test_sign_in_without_auth(self):
with requests_mock.mock() as m:
m.post(self.baseurl + "/signin", text=response_xml, status_code=401)
tableau_auth = TSC.TableauAuth("", "")
- self.assertRaises(TSC.FailedSignInError, self.server.auth.sign_in, tableau_auth)
+ self.assertRaises(TSC.NotSignedInError, self.server.auth.sign_in, tableau_auth)
def test_sign_out(self):
with open(SIGN_IN_XML, "rb") as f:
diff --git a/test/test_custom_view.py b/test/test_custom_view.py
index 6e863a863..80800c86b 100644
--- a/test/test_custom_view.py
+++ b/test/test_custom_view.py
@@ -18,8 +18,6 @@
GET_XML_ID = os.path.join(TEST_ASSET_DIR, "custom_view_get_id.xml")
POPULATE_PREVIEW_IMAGE = os.path.join(TEST_ASSET_DIR, "Sample View Image.png")
CUSTOM_VIEW_UPDATE_XML = os.path.join(TEST_ASSET_DIR, "custom_view_update.xml")
-CUSTOM_VIEW_POPULATE_PDF = os.path.join(TEST_ASSET_DIR, "populate_pdf.pdf")
-CUSTOM_VIEW_POPULATE_CSV = os.path.join(TEST_ASSET_DIR, "populate_csv.csv")
CUSTOM_VIEW_DOWNLOAD = TEST_ASSET_DIR / "custom_view_download.json"
FILE_UPLOAD_INIT = TEST_ASSET_DIR / "fileupload_initialize.xml"
FILE_UPLOAD_APPEND = TEST_ASSET_DIR / "fileupload_append.xml"
@@ -248,73 +246,3 @@ def test_large_publish(self):
assert isinstance(view, TSC.CustomViewItem)
assert view.id is not None
assert view.name is not None
-
- def test_populate_pdf(self) -> None:
- self.server.version = "3.23"
- self.baseurl = self.server.custom_views.baseurl
- with open(CUSTOM_VIEW_POPULATE_PDF, "rb") as f:
- response = f.read()
- with requests_mock.mock() as m:
- m.get(
- self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/pdf?type=letter&orientation=portrait&maxAge=5",
- content=response,
- )
- custom_view = TSC.CustomViewItem()
- custom_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5"
-
- size = TSC.PDFRequestOptions.PageType.Letter
- orientation = TSC.PDFRequestOptions.Orientation.Portrait
- req_option = TSC.PDFRequestOptions(size, orientation, 5)
-
- self.server.custom_views.populate_pdf(custom_view, req_option)
- self.assertEqual(response, custom_view.pdf)
-
- def test_populate_csv(self) -> None:
- self.server.version = "3.23"
- self.baseurl = self.server.custom_views.baseurl
- with open(CUSTOM_VIEW_POPULATE_CSV, "rb") as f:
- response = f.read()
- with requests_mock.mock() as m:
- m.get(self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/data?maxAge=1", content=response)
- custom_view = TSC.CustomViewItem()
- custom_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5"
- request_option = TSC.CSVRequestOptions(maxage=1)
- self.server.custom_views.populate_csv(custom_view, request_option)
-
- csv_file = b"".join(custom_view.csv)
- self.assertEqual(response, csv_file)
-
- def test_populate_csv_default_maxage(self) -> None:
- self.server.version = "3.23"
- self.baseurl = self.server.custom_views.baseurl
- with open(CUSTOM_VIEW_POPULATE_CSV, "rb") as f:
- response = f.read()
- with requests_mock.mock() as m:
- m.get(self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/data", content=response)
- custom_view = TSC.CustomViewItem()
- custom_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5"
- self.server.custom_views.populate_csv(custom_view)
-
- csv_file = b"".join(custom_view.csv)
- self.assertEqual(response, csv_file)
-
- def test_pdf_height(self) -> None:
- self.server.version = "3.23"
- self.baseurl = self.server.custom_views.baseurl
- with open(CUSTOM_VIEW_POPULATE_PDF, "rb") as f:
- response = f.read()
- with requests_mock.mock() as m:
- m.get(
- self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/pdf?vizHeight=1080&vizWidth=1920",
- content=response,
- )
- custom_view = TSC.CustomViewItem()
- custom_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5"
-
- req_option = TSC.PDFRequestOptions(
- viz_height=1080,
- viz_width=1920,
- )
-
- self.server.custom_views.populate_pdf(custom_view, req_option)
- self.assertEqual(response, custom_view.pdf)
diff --git a/test/test_dataalert.py b/test/test_dataalert.py
index 6f6f1683c..d9e00a9db 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 + f"/{alert_id}/users/{user_id}", status_code=204)
+ m.delete(self.baseurl + "/{0}/users/{1}".format(alert_id, 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 45d9ba9c9..624eb93e1 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({"world", "indicators", "sample"}, all_datasources[1].tags)
+ self.assertEqual(set(["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({"world", "indicators", "sample"}, single_datasource.tags)
+ self.assertEqual(set(["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 = f'name="tableau_workbook"; filename="{filename}"'
+ disposition = 'name="tableau_workbook"; filename="{}"'.format(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(f"{self.baseurl}/{datasource.id}/revisions", text=response_xml)
+ m.get("{0}/{1}/revisions".format(self.baseurl, datasource.id), 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(f"{self.baseurl}/{datasource.id}/revisions/3")
+ m.delete("{0}/{1}/revisions/3".format(self.baseurl, datasource.id))
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 ff1ef0f72..8635af978 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:
+ class FakeResponse(object):
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 87332d70f..6f0be3b3c 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(f"{self.baseurl}/{self.user.id}", text=response_xml)
+ m.get("{0}/{1}".format(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(f"{self.baseurl}/{self.user.id}", text=response_xml)
+ m.put("{0}/{1}".format(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(f"{self.baseurl}/{self.user.id}", text=response_xml)
+ m.put("{0}/{1}".format(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(f"{self.baseurl}/{self.user.id}", text=response_xml)
+ m.put("{0}/{1}".format(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(f"{baseurl}/{self.user.id}", text=response_xml)
+ m.put("{0}/{1}".format(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(f"{self.baseurl}/{self.user.id}/workbooks/{workbook.id}")
+ m.delete("{0}/{1}/workbooks/{2}".format(self.baseurl, self.user.id, 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(f"{self.baseurl}/{self.user.id}/views/{view.id}")
+ m.delete("{0}/{1}/views/{2}".format(self.baseurl, self.user.id, 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(f"{self.baseurl}/{self.user.id}/datasources/{datasource.id}")
+ m.delete("{0}/{1}/datasources/{2}".format(self.baseurl, self.user.id, 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(f"{baseurl}/{self.user.id}/projects/{project.id}")
+ m.delete("{0}/{1}/projects/{2}".format(baseurl, self.user.id, 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 0f3234d5d..4c8fb0f9f 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(b"This is a zip file")
+ stream.write("This is a zip file".encode())
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 9567bc3ad..50a5ef48b 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 = f"{self.server.baseurl}/sites/{self.server.site_id}/fileUploads"
+ self.baseurl = "{}/sites/{}/fileUploads".format(self.server.baseurl, self.server.site_id)
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(f"{self.baseurl}/{upload_id}", text=append_response_xml)
+ m.put("{}/{}".format(self.baseurl, upload_id), text=append_response_xml)
actual = self.server.fileuploads.upload(file_path)
self.assertEqual(upload_id, actual)
@@ -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(f"{self.baseurl}/{upload_id}", text=append_response_xml)
+ m.put("{}/{}".format(self.baseurl, upload_id), text=append_response_xml)
actual = self.server.fileuploads.upload(file_content)
self.assertEqual(upload_id, actual)
diff --git a/test/test_flowruns.py b/test/test_flowruns.py
index 8af2540dc..864c0d3cd 100644
--- a/test/test_flowruns.py
+++ b/test/test_flowruns.py
@@ -1,4 +1,3 @@
-import sys
import unittest
import requests_mock
@@ -6,7 +5,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, server_response_error_factory
+from ._utils import read_xml_asset, mocked_time
GET_XML = "flow_runs_get.xml"
GET_BY_ID_XML = "flow_runs_get_by_id.xml"
@@ -29,8 +28,9 @@ 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 = self.server.flow_runs.get()
+ all_flow_runs, pagination_item = 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(f"{self.baseurl}/{flow_run_id}", text=response_xml)
+ m.get("{0}/{1}".format(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(f"{self.baseurl}/{flow_run_id}", text=response_xml)
+ m.get("{0}/{1}".format(self.baseurl, flow_run_id), text=response_xml)
with self.assertRaises(FlowRunFailedException):
self.server.flow_runs.wait_for_job(flow_run_id)
@@ -95,17 +95,6 @@ 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(f"{self.baseurl}/{flow_run_id}", text=response_xml)
+ m.get("{0}/{1}".format(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 2d9f7c7bd..034066e64 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(f"{self.baseurl}", text=response_xml)
+ m.post("{}".format(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 41b5992be..fc9c75a6d 100644
--- a/test/test_group.py
+++ b/test/test_group.py
@@ -1,3 +1,4 @@
+# encoding=utf-8
from pathlib import Path
import unittest
import os
diff --git a/test/test_job.py b/test/test_job.py
index 20b238764..d86397086 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(f"{self.baseurl}/{job_id}", text=response_xml)
+ m.get("{0}/{1}".format(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(f"{self.baseurl}/{job_id}", text=response_xml)
+ m.get("{0}/{1}".format(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(f"{self.baseurl}/{job_id}", text=response_xml)
+ m.get("{0}/{1}".format(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(f"{self.baseurl}/{job_id}", text=response_xml)
+ m.get("{0}/{1}".format(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_pager.py b/test/test_pager.py
index 1836095bb..c30352809 100644
--- a/test/test_pager.py
+++ b/test/test_pager.py
@@ -1,7 +1,6 @@
import contextlib
import os
import unittest
-import xml.etree.ElementTree as ET
import requests_mock
@@ -123,14 +122,3 @@ def test_pager_view(self) -> None:
m.get(self.server.views.baseurl, text=view_xml)
for view in TSC.Pager(self.server.views):
assert view.name is not None
-
- def test_queryset_no_matches(self) -> None:
- elem = ET.Element("tsResponse", xmlns="http://tableau.com/api")
- ET.SubElement(elem, "pagination", totalAvailable="0")
- ET.SubElement(elem, "groups")
- xml = ET.tostring(elem).decode("utf-8")
- with requests_mock.mock() as m:
- m.get(self.server.groups.baseurl, text=xml)
- all_groups = self.server.groups.all()
- groups = list(all_groups)
- assert len(groups) == 0
diff --git a/test/test_project.py b/test/test_project.py
index 430db84b2..e05785f86 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 = 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)
+ 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)
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 = 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)
+ 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)
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 62e301591..772704f69 100644
--- a/test/test_regression_tests.py
+++ b/test/test_regression_tests.py
@@ -1,5 +1,9 @@
import unittest
-from unittest import mock
+
+try:
+ from unittest import mock
+except ImportError:
+ import mock # type: ignore[no-redef]
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 7405189a3..e48f8510a 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 = f"{self.server.sites.baseurl}/{self.server._site_id}"
+ self.baseurl = "{0}/{1}".format(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({"weather"}, matching_workbooks[0].tags)
- self.assertEqual({"safari"}, matching_workbooks[1].tags)
- self.assertEqual({"sample"}, matching_workbooks[2].tags)
+ self.assertEqual(set(["weather"]), matching_workbooks[0].tags)
+ self.assertEqual(set(["safari"]), matching_workbooks[1].tags)
+ self.assertEqual(set(["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({"weather"}, matching_workbooks[0].tags)
- self.assertEqual({"safari"}, matching_workbooks[1].tags)
- self.assertEqual({"sample"}, matching_workbooks[2].tags)
+ self.assertEqual(set(["weather"]), matching_workbooks[0].tags)
+ self.assertEqual(set(["safari"]), matching_workbooks[1].tags)
+ self.assertEqual(set(["sample"]), matching_workbooks[2].tags)
def test_invalid_shorthand_option(self) -> None:
with self.assertRaises(ValueError):
@@ -358,13 +358,3 @@ def test_queryset_pagesize_filter(self) -> None:
queryset = self.server.views.all().filter(page_size=page_size)
assert queryset.request_options.pagesize == page_size
_ = list(queryset)
-
- def test_language_export(self) -> None:
- with requests_mock.mock() as m:
- m.get(requests_mock.ANY)
- url = self.baseurl + "/views/456/data"
- opts = TSC.PDFRequestOptions()
- opts.language = "en-US"
-
- resp = self.server.users.get_request(url, request_object=opts)
- self.assertTrue(re.search("language=en-us", resp.request.query))
diff --git a/test/test_schedule.py b/test/test_schedule.py
index b072522a4..0377295d7 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 = f"{self.server.baseurl}/schedules/{schedule_id}"
+ baseurl = "{}/schedules/{}".format(self.server.baseurl, 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 = f"{self.server.baseurl}/schedules/{schedule_id}"
+ baseurl = "{}/schedules/{}".format(self.server.baseurl, 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 = f"{self.server.baseurl}/schedules/{schedule_id}"
+ baseurl = "{}/schedules/{}".format(self.server.baseurl, 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 = f"{self.server.baseurl}/schedules/{schedule_id}"
+ baseurl = "{}/schedules/{}".format(self.server.baseurl, schedule_id)
m.get(baseurl, text=response_xml)
schedule = self.server.schedules.get_by_id(schedule_id)
self.assertIsNotNone(schedule)
@@ -165,7 +165,7 @@ def test_get_monthly_by_id_2(self) -> None:
response_xml = f.read().decode("utf-8")
with requests_mock.mock() as m:
schedule_id = "8c5caf33-6223-4724-83c3-ccdc1e730a07"
- baseurl = f"{self.server.baseurl}/schedules/{schedule_id}"
+ baseurl = "{}/schedules/{}".format(self.server.baseurl, 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 = f"{self.server.baseurl}/sites/{self.server.site_id}/schedules"
+ baseurl = "{}/sites/{}/schedules".format(self.server.baseurl, self.server.site_id)
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 = f"{self.server.baseurl}/sites/{self.server.site_id}/schedules"
+ baseurl = "{}/sites/{}/schedules".format(self.server.baseurl, self.server.site_id)
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 = f"{self.server.baseurl}/sites/{self.server.site_id}/schedules"
+ baseurl = "{}/sites/{}/schedules".format(self.server.baseurl, self.server.site_id)
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 = f"{self.server.baseurl}/sites/{self.server.site_id}/schedules"
+ baseurl = "{}/sites/{}/schedules".format(self.server.baseurl, self.server.site_id)
with open(FLOW_GET_BY_ID_XML, "rb") as f:
flow_response = f.read().decode("utf-8")
diff --git a/test/test_server_info.py b/test/test_server_info.py
index fa1472c9a..1cf190ecd 100644
--- a/test/test_server_info.py
+++ b/test/test_server_info.py
@@ -4,7 +4,6 @@
import requests_mock
import tableauserverclient as TSC
-from tableauserverclient.server.endpoint.exceptions import NonXMLResponseError
TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), "assets")
@@ -12,7 +11,6 @@
SERVER_INFO_25_XML = os.path.join(TEST_ASSET_DIR, "server_info_25.xml")
SERVER_INFO_404 = os.path.join(TEST_ASSET_DIR, "server_info_404.xml")
SERVER_INFO_AUTH_INFO_XML = os.path.join(TEST_ASSET_DIR, "server_info_auth_info.xml")
-SERVER_INFO_WRONG_SITE = os.path.join(TEST_ASSET_DIR, "server_info_wrong_site.html")
class ServerInfoTests(unittest.TestCase):
@@ -65,11 +63,3 @@ def test_server_use_server_version_flag(self):
m.get("http://test/api/2.4/serverInfo", text=si_response_xml)
server = TSC.Server("http://test", use_server_version=True)
self.assertEqual(server.version, "2.5")
-
- def test_server_wrong_site(self):
- with open(SERVER_INFO_WRONG_SITE, "rb") as f:
- response = f.read().decode("utf-8")
- with requests_mock.mock() as m:
- m.get(self.server.server_info.baseurl, text=response, status_code=404)
- with self.assertRaises(NonXMLResponseError):
- self.server.server_info.get()
diff --git a/test/test_site_model.py b/test/test_site_model.py
index 60ad9c5e5..f62eb66f0 100644
--- a/test/test_site_model.py
+++ b/test/test_site_model.py
@@ -1,3 +1,5 @@
+# coding=utf-8
+
import unittest
import tableauserverclient as TSC
diff --git a/test/test_tagging.py b/test/test_tagging.py
index 23dffebfb..0184af415 100644
--- a/test/test_tagging.py
+++ b/test/test_tagging.py
@@ -1,6 +1,6 @@
from contextlib import ExitStack
import re
-from collections.abc import Iterable
+from typing 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 = {"x", "y", "z"}
+ initial_tags = set(["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 2d724b879..53da7c160 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(f"{self.server.tasks.baseurl}/{TaskItem.Type.DataAcceleration}", text=response_xml)
+ m.get("{}/{}".format(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(f"{self.baseurl}/{task_id}", text=response_xml)
+ m.get("{}/{}".format(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(f"{self.baseurl}/{task_id}/runNow", text=response_xml)
+ m.post("{}/{}/runNow".format(self.baseurl, task_id), 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(f"{self.baseurl}", text=response_xml)
+ m.post("{}".format(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 a46624845..1f5eba57f 100644
--- a/test/test_user.py
+++ b/test/test_user.py
@@ -1,5 +1,8 @@
+import io
import os
import unittest
+from typing import List
+from unittest.mock import MagicMock
import requests_mock
@@ -160,7 +163,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({"Safari", "Sample"}, workbook_list[0].tags)
+ self.assertEqual(set(["Safari", "Sample"]), workbook_list[0].tags)
def test_populate_workbooks_missing_id(self) -> None:
single_user = TSC.UserItem("test", "Interactor")
@@ -173,7 +176,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(f"{baseurl}/{single_user.id}", text=response_xml)
+ m.get("{0}/{1}".format(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 a8a2c51cb..d0997b9ff 100644
--- a/test/test_user_model.py
+++ b/test/test_user_model.py
@@ -1,6 +1,7 @@
import logging
import unittest
from unittest.mock import *
+from typing import List
import io
import pytest
@@ -106,7 +107,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)
@@ -118,10 +119,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, f"Expected two lines to be parsed, got {valid}"
- assert invalid == [], f"Expected no failures, got {invalid}"
+ assert valid == 2, "Expected two lines to be parsed, got {}".format(valid)
+ assert invalid == [], "Expected no failures, got {}".format(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, f"Exactly 5 of the lines were valid, counted {valid + invalid}"
+ assert valid == 5, "Exactly 5 of the lines were valid, counted {}".format(valid + invalid)
diff --git a/test/test_view.py b/test/test_view.py
index a89a6d235..1c667a4c3 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({"tag1", "tag2"}, all_views[0].tags)
+ self.assertEqual(set(["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({"tag1", "tag2"}, view.tags)
+ self.assertEqual(set(["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({"tag1", "tag2"}, view.tags)
+ self.assertEqual(set(["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 766831b0a..6f94f0c10 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({"Safari", "Sample"}, single_workbook.tags)
+ self.assertEqual(set(["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 1a6b3192f..950118dc0 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({"Safari", "Sample"}, all_workbooks[1].tags)
+ self.assertEqual(set(["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({"Safari", "Sample"}, single_workbook.tags)
+ self.assertEqual(set(["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({"Safari", "Sample"}, single_workbook.tags)
+ self.assertEqual(set(["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 = f'name="tableau_workbook"; filename="{filename}"'
+ disposition = 'name="tableau_workbook"; filename="{}"'.format(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(f"{self.baseurl}/{workbook.id}/revisions", text=response_xml)
+ m.get("{0}/{1}/revisions".format(self.baseurl, workbook.id), 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(f"{self.baseurl}/{workbook.id}/revisions/3")
+ m.delete("{0}/{1}/revisions/3".format(self.baseurl, workbook.id))
self.server.workbooks.delete_revision(workbook.id, "3")
def test_download_revision(self) -> None:
diff --git a/versioneer.py b/versioneer.py
index cce899f58..86c240e13 100644
--- a/versioneer.py
+++ b/versioneer.py
@@ -276,6 +276,7 @@
"""
+from __future__ import print_function
try:
import configparser
@@ -327,7 +328,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(f"Warning: build in {os.path.dirname(me)} is using versioneer.py from {versioneer_py}")
+ print("Warning: build in %s is using versioneer.py from %s" % (os.path.dirname(me), versioneer_py))
except NameError:
pass
return root
@@ -341,7 +342,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) as f:
+ with open(setup_cfg, "r") as f:
parser.readfp(f)
VCS = parser.get("versioneer", "VCS") # mandatory
@@ -397,7 +398,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 OSError:
+ except EnvironmentError:
e = sys.exc_info()[1]
if e.errno == errno.ENOENT:
continue
@@ -407,7 +408,7 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=
return None, None
else:
if verbose:
- print(f"unable to find command, tried {commands}"
+ print("unable to find command, tried %s" % (commands,))
return None, None
stdout = p.communicate()[0].strip()
if sys.version_info[0] >= 3:
@@ -422,7 +423,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
@@ -954,7 +955,7 @@ def git_get_keywords(versionfile_abs):
# _version.py.
keywords = {}
try:
- f = open(versionfile_abs)
+ f = open(versionfile_abs, "r")
for line in f.readlines():
if line.strip().startswith("git_refnames ="):
mo = re.search(r'=\s*"(.*)"', line)
@@ -969,7 +970,7 @@ def git_get_keywords(versionfile_abs):
if mo:
keywords["date"] = mo.group(1)
f.close()
- except OSError:
+ except EnvironmentError:
pass
return keywords
@@ -993,11 +994,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 = {r.strip() for r in refnames.strip("()").split(",")}
+ refs = set([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 = {r[len(TAG) :] for r in refs if r.startswith(TAG)}
+ tags = set([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
@@ -1006,7 +1007,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 = {r for r in refs if re.search(r"\d", r)}
+ tags = set([r for r in refs if re.search(r"\d", r)])
if verbose:
print("discarding '%s', no digits" % ",".join(refs - tags))
if verbose:
@@ -1099,7 +1100,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"] = f"tag '{full_tag}' doesn't start with prefix '{tag_prefix}'"
+ pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % (full_tag, tag_prefix)
return pieces
pieces["closest-tag"] = full_tag[len(tag_prefix) :]
@@ -1144,13 +1145,13 @@ def do_vcs_install(manifest_in, versionfile_source, ipy):
files.append(versioneer_file)
present = False
try:
- f = open(".gitattributes")
+ f = open(".gitattributes", "r")
for line in f.readlines():
if line.strip().startswith(versionfile_source):
if "export-subst" in line.strip().split()[1:]:
present = True
f.close()
- except OSError:
+ except EnvironmentError:
pass
if not present:
f = open(".gitattributes", "a+")
@@ -1184,7 +1185,7 @@ def versions_from_parentdir(parentdir_prefix, root, verbose):
root = os.path.dirname(root) # up a level
if verbose:
- print(f"Tried directories {rootdirs!s} but none started with prefix {parentdir_prefix}")
+ print("Tried directories %s but none started with prefix %s" % (str(rootdirs), parentdir_prefix))
raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
@@ -1211,7 +1212,7 @@ def versions_from_file(filename):
try:
with open(filename) as f:
contents = f.read()
- except OSError:
+ except EnvironmentError:
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:
@@ -1228,7 +1229,7 @@ def write_to_version_file(filename, versions):
with open(filename, "w") as f:
f.write(SHORT_VERSION_PY % contents)
- print(f"set {filename} to '{versions['version']}'")
+ print("set %s to '%s'" % (filename, versions["version"]))
def plus_or_dot(pieces):
@@ -1451,7 +1452,7 @@ def get_versions(verbose=False):
try:
ver = versions_from_file(versionfile_abs)
if verbose:
- print(f"got version from file {versionfile_abs} {ver}")
+ print("got version from file %s %s" % (versionfile_abs, ver))
return ver
except NotThisMethod:
pass
@@ -1722,7 +1723,7 @@ def do_setup():
root = get_root()
try:
cfg = get_config_from_root(root)
- except (OSError, configparser.NoSectionError, configparser.NoOptionError) as e:
+ except (EnvironmentError, 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:
@@ -1747,9 +1748,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) as f:
+ with open(ipy, "r") as f:
old = f.read()
- except OSError:
+ except EnvironmentError:
old = ""
if INIT_PY_SNIPPET not in old:
print(" appending to %s" % ipy)
@@ -1768,12 +1769,12 @@ def do_setup():
manifest_in = os.path.join(root, "MANIFEST.in")
simple_includes = set()
try:
- with open(manifest_in) as f:
+ with open(manifest_in, "r") as f:
for line in f:
if line.startswith("include "):
for include in line.split()[1:]:
simple_includes.add(include)
- except OSError:
+ except EnvironmentError:
pass
# That doesn't cover everything MANIFEST.in can do
# (http://docs.python.org/2/distutils/sourcedist.html#commands), so
@@ -1804,7 +1805,7 @@ def scan_setup_py():
found = set()
setters = False
errors = 0
- with open("setup.py") as f:
+ with open("setup.py", "r") as f:
for line in f.readlines():
if "import versioneer" in line:
found.add("import")