Welcome to this month’s intermediate DataDevQuest challenge! This challenge is designed to sharpen your skills in interacting with Tableau Server or Tableau Online programmatically. Taking in more complicated input, transforming it into a format needed, and retrieving the REST API objects allows for other solutions to rapidly find and adjust settings on specific views, workbooks, and projects.

Challenge Overview

Objective

Write a program that takes input from the user to search and retrieve information about a Tableau view by its URL. 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

  1. User Input:
    • Prompt the user to enter the URL of a Tableau view they wish to find. The URL may be complete or incomplete. For the server and site you use as default, or another site for which you have credentials.
  2. 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.
  3. View Search:
    • Search for the view on Tableau Server or Tableau Online that matches the user’s input.
  4. 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.
  5. 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

Optional Enhancements


Getting Started

For Python Users

pip install tableauserverclient
Python

For Other Languages


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.
YAML

Submission Guidelines


Additional Resources


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!


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 re
import sys
from typing import overload
from urllib.parse import urlparse

from dotenv import load_dotenv
import tableauserverclient as TSC

load_dotenv()


@dataclass
class SearchDetails:
    server: str
    site_url: str | None = None
    content_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 parse_url(url: str) -> SearchDetails:
    """
    Parse the user provided URL to extract the server URL, site URL,
    and the view object path to search for.

    Parameters
    ----------
    url: str
        The URL to parse. The URL should be in the format of
        'https://<server>/#/<site>/views/<workbook>/<view>'.

    Returns
    -------
    SearchDetails
        A dataclass containing the server URL, site URL, and the view object
        path to search for.
    """
    # The URL may have a "query" string that needs to be removed before
    # parsing. URL standard expects the query string to be before the fragment.
    # Tableau Server URLs use the fragment to handle navigation to a site, and
    # then to a specific object within that site.
    parsed_url = urlparse(url.split("?")[0])
    server = f"{parsed_url.scheme}://{parsed_url.netloc}"
    # Create a regular expression pattern with named capture groups to extract
    # relevant parts of the URL.
    pattern = re.compile(
        r"""
    ^\/?                          # Start of string and optional leading slash
    (site\/(?P<site>\w+))?        # Optional site URL. If not present, then the "default" site
    ((?P<view>views)\/            # Fixed string "views" followed by a slash
    (?P<view_path>[\w-]+\/[\w-]+) # View path is Workbook/View
    )
    $ # End of string
    """,
        flags=re.VERBOSE,
    )

    if (match := pattern.fullmatch(parsed_url.fragment)):
        # The default site URL is represented by an empty string.
        site = match.groupdict().get("site", "")
        # Content URL has the format "workbook/sheets/view".
        view_path = match.groupdict().get("view_path", "")
        workbook, view = view_path.split("/")
        content_url = f"{workbook}/sheets/{view}"

    else:
        raise ValueError(
            "URL must be in the format 'https://<server>/#/<site>/views/<workbook>/<view>'."
        )

    return SearchDetails(server, site, content_url, None)


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:]

    parser = argparse.ArgumentParser(description="Locate a view by name.")
    parser.add_argument(
        "-u", "--url", dest="url", help="URL of the View to locate.", required=True
    )
    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)

    details = parse_url(args.url)
    auth = get_auth(
        args.username,
        args.password,
        args.token_name,
        args.token_secret,
        details.site_url,
    )
    details.auth = auth

    return details


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)

    # 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(content_url=search_details.content_url)
        # Check how many results are returned. Raise an error if no views are found.
        # URL is unique, so only one view at most should be returned.
        if 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