Welcome to this month’s basic DataDevQuest challenge! This challenge is designed to sharpen your skills in interacting with Tableau Server or Tableau Online programmatically. Whether you’re a seasoned developer or just starting out, this task will help you get familiar with the basics of the Tableau REST API or the Tableau Server Client (TSC) library for Python.
Challenge Overview
Objective:
Write a program that takes input from the user to search and retrieve information about a Tableau view by its name. The program should connect to Tableau Server or Tableau Online, search for the view, and display relevant details in an organized format.
Why This Challenge?
Working with Tableau’s APIs allows you to automate tasks, integrate Tableau with other systems, and extend its functionality. This challenge will give you hands-on experience with authentication, making API calls, and handling responses—essential skills for any Tableau developer.
Challenge Details
Requirements
- User Input:
- Prompt the user to enter the name of a Tableau view they wish to find.
- Authentication:
- Python Users:
- Use the Tableau Server Client (TSC) library for authentication.
- Other Languages:
- Use the Tableau REST API directly for authentication.
- Handle authentication securely and efficiently.
- Python Users:
- View Search:
- Search for the view on Tableau Server or Tableau Online that matches the user’s input.
- The search should be case-insensitive.
- Display Information:
- If the view is found, display the following details:
- View Name
- View ID
- Workbook Name
- Project Name
- View URL
- If multiple views match, display details for each.
- If no views are found, inform the user accordingly.
- If the view is found, display the following details:
- Error Handling:
- Gracefully handle errors such as authentication failures, network issues, or API errors.
- Provide meaningful messages to help the user understand what went wrong.
Constraints
- Programming Language:
- You may use any programming language.
- Python users are encouraged to use the Tableau Server Client (TSC) library.
- Users of other languages should utilize the Tableau REST API directly.
- Permissions:
- Assume the user has the necessary permissions to access the views on Tableau Server or Tableau Online.
- Code Quality:
- Write clean, maintainable code.
- Include comments to explain complex sections.
Optional Enhancements
- Pagination Handling:
- If the number of results is large, implement pagination to handle them efficiently.
- Command-Line Arguments:
- Allow the user to pass the view name as a command-line argument.
- Export View Image:
- Provide an option to download an image of the view.
- Partial matches
- Handle the users providing an incomplete name
Getting Started
For Python Users
- Tableau Server Client (TSC) Library:
- The TSC library simplifies interactions with the Tableau Server REST API.
- TSC GitHub Repository
- Install using pip:
pip install tableauserverclient
PythonFor Other Languages
- Tableau REST API Reference:
- Use the REST API to interact with Tableau Server programmatically.
- Tableau REST API Documentation
Sample Input/Output
Enter the name of the Tableau view to search for: Sales Dashboard
Searching for views...
Found the following view(s):
1)
- View Name: Sales Dashboard
- View ID: e8f2a1b4-1234-5678-9abc-def012345678
- Workbook Name: Annual Sales Reports
- Project Name: Corporate Reports
- View URL: https://your-tableau-server/views/AnnualSalesReports/SalesDashboard
2)
- View Name: Regional Sales Dashboard
- View ID: f9a3b2c5-2345-6789-0bcd-ef1234567890
- Workbook Name: Regional Reports
- Project Name: Sales Team
- View URL: https://your-tableau-server/views/RegionalReports/RegionalSalesDashboard
Process completed successfully.
YAMLSubmission Guidelines
- Source Code:
- Publish your project publically in Github profile
- README:
- Include setup instructions.
- Describe how to run the program.
- Video of Solution:
- Include a video of your solution in the README file, which you can publish on Youtube and embed the iframe. Or save the video file in the directory root inside the repository.
- Comments:
- Ensure your code is well-commented.
- Submission:
- Submit your challenge in the following **forms**
Additional Resources
- Tableau Server Client (Python):
- Tableau REST API:
- Filtering and Sorting
- Authentication
- Community Support:
- Tableau Community Forums
- Stack Overflow (
tableau-api
tag) - DataDev Slack Channel
Conclusion
This challenge is a great opportunity to delve into Tableau’s programmatic interfaces and enhance your automation skills. By completing this task, you’ll gain valuable experience that can be applied to a wide range of projects involving data visualization and analysis.
We look forward to seeing your innovative solutions! Happy coding!
Anti-patterns
# Antipattern 1: Only investigating the first page
import tableauserverclient as TSC
server = TSC.Server("http://localhost", True)
# Antipattern 1: Only investigating the first page
all_views, _ = server.views.get()
target_view = None
for view in all_views:
if view.name == "Sales Overview":
target_view = view
break
PythonWhat is wrong with this code?
– Hardcoded view name
– Filtering being done client side, rather than server side.
– The code only retrieves the first page of views from the server. If the view is not on the first page, the code will not find it.
– The code does not handle the case where the view is not found.
– It doesn’t handle the case where multiple views have the same name.
– Doesn’t allow user to specify a project/workbook containing the view
# Antipattern 2: Overusing list()
views = list(TSC.Pager(server.views))
views = list(filter(lambda view: view.name == "Sales Overview", views))
view = views[0]
PythonWhat is wrong with this code?
– Hardcoded view name.
– Filtering being done client side, rather than server side.
– The code creates an unnecessary list of views, which consumes memory.
– The code does not handle the case where the view is not found.
– a TSC.Pager object is iterable, so there is no need to convert it to a list.
– Doesn’t allow user to specify a project/workbook containing the view
– Potential for error if multiple views have the same name
– Many API calls, while still possibly giving an incorrect result
# Antipattern 3: Searching for workbooks and views separately
def find_workbook(server: TSC.Server, workbook_name: str) -> str:
for workbook in TSC.Pager(server.workbooks):
if workbook.name == workbook_name:
return workbook.id
def find_view(server: TSC.Server, workbook_name: str, view_name: str) -> str:
workbook_id = find_workbook(server, workbook_name)
workbook = server.workbooks.get_by_id(workbook_id)
for view in workbook.views:
if view.name == view_name:
return view
PythonWhat is wrong with this code?
– Filtering being done client side, rather than server side.
– The code retrieves all workbooks from the server, which can be slow and inefficient if there are many workbooks.
– The code then retrieves all views for each workbook, and if the name matches, it calls the API again to get the views for that workbook. This is inefficient and can result in many unnecessary API calls.
– Finds the objects, returns the ID, but doesn’t return the object itself, then searches for the object again. Redundant work being done.
– The code does not handle the case where the workbook or view is not found.
– May miss the intended workbook as it only searches for the first workbook with the matching name.
– Doesn’t allow user to specify a project containing the workbook and view.
Solution
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "tableauserverclient>=0.34",
# "python-dotenv",
# ]
# ///
import argparse
from collections.abc import Sequence
from dataclasses import dataclass
import os
import sys
from typing import overload
from dotenv import load_dotenv
import tableauserverclient as TSC
load_dotenv()
@dataclass
class SearchDetails:
name: str
server: str
workbook_name: str | None = None
project_name: str | None = None
site_url: str | None = None
auth: TSC.TableauAuth | TSC.PersonalAccessTokenAuth
@overload
def get_auth(
username: str, password: str, token_name: None, token_secret: None, site_url: str
) -> TSC.TableauAuth: ...
@overload
def get_auth(
username: None, password: None, token_name: str, token_secret: str, site_url: str
) -> TSC.PersonalAccessTokenAuth: ...
def get_auth(username, password, token_name, token_secret, site_url):
"""
From the provided credentials, return the object that will be used to
authenticate to Tableau Server or Tableau Cloud. Preference is given to
the provided credentials, but if they are not provided, the function will
attempt to read the credentials from the environment variables.
Parameters
----------
username: str | None
The username to use for authentication. Required if using username and
password authentication. Can be read from the TABLEAU_USERNAME
environment variable.
password: str | None
The password to use for authentication. Required if using username and
password authentication. Can be read from the TABLEAU_PASSWORD
environment variable.
token_name: str | None
The name of the personal access token to use for authentication. Required
if using personal access token authentication. Can be read from the
TABLEAU_TOKEN_NAME environment variable.
token_secret: str | None
The secret of the personal access token to use for authentication. Required
if using personal access token authentication. Can be read from the
TABLEAU_TOKEN_SECRET environment variable.
site_url: str
The URL of the site to authenticate to. Required. Can be read from the
TABLEAU_SITE_URL environment variable.
Returns
-------
TSC.TableauAuth | TSC.PersonalAccessTokenAuth
The object to use for authentication.
"""
if username and password:
return TSC.TableauAuth(username, password, site_url)
elif token_name and token_secret:
return TSC.PersonalAccessTokenAuth(token_name, token_secret, site_url)
else:
if (username := os.getenv("TABLEAU_USERNAME")) and (
password := os.getenv("TABLEAU_PASSWORD")
):
return TSC.TableauAuth(username, password, site_url)
elif (token_name := os.getenv("TABLEAU_TOKEN_NAME")) and (
token_secret := os.getenv("TABLEAU_TOKEN_SECRET")
):
return TSC.PersonalAccessTokenAuth(token_name, token_secret, site_url)
raise ValueError(
"Either username and password or token name and token secret must be provided."
)
def get_args(args: Sequence[str] | None = None) -> SearchDetails:
"""
Parse command line arguments.
"""
# If no arguments are provided, use sys.argv[1:] to read arguments from
# the command line. A str is also technically a sequence, so we need to
# check for that.
if isinstance(args, str):
raise ValueError("args must be a sequence of strings or None.")
args = args or sys.argv[1:]
# If the TABLEAU_SITE_URL environment variable is set, the site argument
# can default to that, otherwise it should be required.
if site_url := os.getenv("TABLEAU_SITE_URL"):
site_kwargs = {"default": site_url}
else:
site_kwargs = {"required": True}
if url := os.getenv("TABLEAU_URL"):
url_kwargs = {"default": url}
else:
url_kwargs = {"required": True}
parser = argparse.ArgumentParser(description="Locate a view by name.")
parser.add_argument(
"-n", "--name", required=True, help="Name of the view to locate."
)
parser.add_argument(
"-u",
"--url",
dest="server",
help="URL of the Tableau Server or Tableau Online instance to connect to.",
**url_kwargs,
)
parser.add_argument(
"-s", "--site", dest="site_url", help="Site ID of the site to.", **site_kwargs
)
parser.add_argument(
"-w",
"--workbook",
dest="workbook_name",
required=False,
help="Name of the workbook containing the view.",
)
parser.add_argument(
"--project",
dest="project_name",
required=False,
help="Name of the project containing the workbook.",
)
parser.add_argument(
"--username",
required=False,
help="Username to authenticate with. Required if using username and password authentication, or can be read from the TABLEAU_USERNAME environment variable.",
)
parser.add_argument(
"--password",
required=False,
help="Password to authenticate with. Required if using username and password authentication, or can be read from the TABLEAU_PASSWORD environment variable.",
)
parser.add_argument(
"--token-name",
required=False,
help="Name of the personal access token to authenticate with. Required if using personal access token authentication, or can be read from the TABLEAU_TOKEN_NAME environment variable.",
)
parser.add_argument(
"--token-secret",
required=False,
help="Secret of the personal access token to authenticate with. Required if using personal access token authentication, or can be read from the TABLEAU_TOKEN_SECRET environment variable.",
)
args = parser.parse_args(args)
auth = get_auth(
args.username, args.password, args.token_name, args.token_secret, args.site_url
)
return SearchDetails(
args.name, args.workbook_name, args.project_name, args.site_url, auth
)
def main() -> int:
"""
Main function for the script. Return value will be used as the exit code.
0 is considered a successful exit, any other value is considered an error.
"""
# Parse out command line arguments
search_details = get_args()
# Set "use_server_version" to True to use the server's version instead of
# the default version. This ensures that the client doesn't complain about
# version mismatches.
server = TSC.Server(search_details.server, use_server_version=True)
# From the command line arguments, construct what will be used to filter
# the objects on the server.
filter_options = {"name": search_details.name}
if search_details.workbook_name:
filter_options["workbook_name"] = search_details.workbook_name
if search_details.project_name:
filter_options["project_name"] = search_details.project_name
# Sign in using a context manager to ensure that the sign out is called
# when the block is exited.
with server.auth.sign_in(search_details.auth):
# Get the QuerySet of views that match the filter options.
views = server.views.filter(**filter_options)
# Check how many results are returned. If there are multiple views with
# the same name, we can't uniquely identify the view. Depending on the
# use case, this may not be an error, but exactly what you are after.
# Raise an error in this case. Raise an error if no views are found.
if len(views) > 1:
raise RuntimeError(f"Multiple views found with name {search_details.name}.")
elif len(views) == 0:
raise RuntimeError(f"No views found with name {search_details.name}.")
# If only one view is found, print out the name and ID of the view.
view = views[0]
print(f"View found: {view.name} ({view.id})")
return 0
if __name__ == "__main__":
sys.exit(main())
Python