From 32f639b34a34a153746215aa6d7945ea96950423 Mon Sep 17 00:00:00 2001 From: Aaron Carey <31550444+aaroncarey@users.noreply.github.com> Date: Thu, 26 Jul 2018 13:31:49 -0700 Subject: [PATCH 1/7] Updating REST API samples and readme files (#27) (#30) * Update README.md * Update README.md * Update version .py (2.8 -> 3.0) * Updated version numbers, changed the formatting used to refer to files and folders. * Updated version number from 2.8 to 3.0 --- java/README.md | 12 ++++++------ .../documentation/api/rest/util/RestApiUtils.java | 2 +- python/README.md | 4 ++-- python/version.py | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/java/README.md b/java/README.md index 5ee2898..1eba45d 100644 --- a/java/README.md +++ b/java/README.md @@ -14,11 +14,11 @@ ## Getting started 1. Install the tools listed in the "Requirements" section. -1. Download the REST API schema and save it in the "res" folder under the folder where this README file is. For more information about the schema, see the following documentation: +1. Download the REST API schema and save it in the `/res` folder under the folder where this README file is. For more information about the schema, see the following documentation: -1. In the "res" folder, open the "config.properties" file using a text editor. +1. In the `/res` folder, open the `config.properties` file using a text editor. 1. Modify the configurations as instructed in the file. A sample workbook is already provided with this sample, but you can use any packaged workbook that you want. 1. Open the following file in a text editor: @@ -31,13 +31,13 @@ For example, you might see the following URL: ``` - /api/2.8/ + /api/3.0/ ``` - If you want to use version 2.4 of the API, replace the URL with the following: + If you want to use version 2.8 of the API, replace the URL with the following: ``` - /api/2.4/ + /api/2.8/ ``` ## Running the sample @@ -50,7 +50,7 @@ ## Possible problems -When "ant" is run in a command prompt, it may respond with "ant is not recognized as an internal or external command..." +When `ant` is run in a command prompt, it may respond with "ant is not recognized as an internal or external command..." Make sure that the `ANT_HOME` and `JAVA_HOME` variables are set as described in the installation guide for Apache Ant. Paths should not include quotes. For more information, see diff --git a/java/src/com/tableausoftware/documentation/api/rest/util/RestApiUtils.java b/java/src/com/tableausoftware/documentation/api/rest/util/RestApiUtils.java index a30c9ec..6687a4f 100644 --- a/java/src/com/tableausoftware/documentation/api/rest/util/RestApiUtils.java +++ b/java/src/com/tableausoftware/documentation/api/rest/util/RestApiUtils.java @@ -110,7 +110,7 @@ public static RestApiUtils getInstance() { * @return the URI builder */ private static UriBuilder getApiUriBuilder() { - return UriBuilder.fromPath(m_properties.getProperty("server.host") + "/api/2.8"); + return UriBuilder.fromPath(m_properties.getProperty("server.host") + "/api/3.0"); } /** * Initializes the RestApiUtils. The initialize code loads values from the configuration diff --git a/python/README.md b/python/README.md index 1a26977..c965296 100644 --- a/python/README.md +++ b/python/README.md @@ -10,7 +10,7 @@ Running the samples * All samples require 2 arguments: server adress (without a trailing slash) and username * Run by executing ```python sample_file_name.py ``` * Specific information for each sample are included at the top of each file -* API version is set to 2.7 by default for Tableau server 10.4, but it can be changed in [version.py](./version.py) +* API version is set to 3.0 by default for Tableau Server 2018.1, but it can be changed in [version.py](./version.py) * For Tableau Server 9.0, the REST API namespace must be changed (refer to comment in each sample where namespace, xmlns, is defined) REST API Samples @@ -20,6 +20,6 @@ These are created and maintained by Tableau. Demo | Source Code | Description -------- | -------- | -------- Publish Workbook | [publish_workbook.py](./publish_workbook.py) | Shows how to upload a Tableau workbook using both a single request as well as chunking the upload. -Move Workbook | [move_workbook_projects.py](./move_workbook_projects.py)
[move_workbook_sites.py](./move_workbook_sites.py)
[move_workbook_server.py](./move_workbook_server.py) | Shows how to move a workbook from one project/site/server to another. Moving across different sites and servers require downloading the workbook. 2 methods of downloading are demonstrated in the sites and servers samples.

Moving to another project uses an API call to update workbook.
Moving to another site uses in-memory download method.
Moving to another server uses a temporary file to download workbook. +Move Workbook | [move_workbook_projects.py](./move_workbook_projects.py)
[move_workbook_sites.py](./move_workbook_sites.py)
[move_workbook_server.py](./move_workbook_server.py) | Shows how to move a workbook from one project/site/server to another. Moving across different sites and servers require downloading the workbook. Two methods of downloading are demonstrated in the sites and server samples.

Moving to another project uses an API call to update workbook.
Moving to another site uses in-memory download method.
Moving to another server uses a temporary file to download workbook. Add Permissions | [user_permission_audit.py](./user_permission_audit.py) | Shows how to add permissions for a given user to a given workbook. Global Workbook Permissions | [update_permission.py](./update_permission.py) | Shows how to add or update user permissions for every workbook on a given site or project. diff --git a/python/version.py b/python/version.py index 72efc6b..95da341 100644 --- a/python/version.py +++ b/python/version.py @@ -1 +1 @@ -VERSION = '2.8' +VERSION = '3.0' From 2324f1b4984fcff97a95f2f3342e6465f33cfccc Mon Sep 17 00:00:00 2001 From: Aaron Carey <31550444+aaroncarey@users.noreply.github.com> Date: Thu, 26 Jul 2018 13:33:45 -0700 Subject: [PATCH 2/7] Update README.md Updating version numbers from 3.0 to 3.1 --- java/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/java/README.md b/java/README.md index 1eba45d..7745f00 100644 --- a/java/README.md +++ b/java/README.md @@ -31,13 +31,13 @@ For example, you might see the following URL: ``` - /api/3.0/ + /api/3.1/ ``` - If you want to use version 2.8 of the API, replace the URL with the following: + If you want to use version 3.0 of the API, replace the URL with the following: ``` - /api/2.8/ + /api/3.0/ ``` ## Running the sample From ee6de4cb0c304d59493587f29bba4445ff73a9a6 Mon Sep 17 00:00:00 2001 From: Aaron Carey <31550444+aaroncarey@users.noreply.github.com> Date: Thu, 26 Jul 2018 13:35:16 -0700 Subject: [PATCH 3/7] Update RestApiUtils.java Updating version numbers from 3.0 to 3.1 --- .../documentation/api/rest/util/RestApiUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/src/com/tableausoftware/documentation/api/rest/util/RestApiUtils.java b/java/src/com/tableausoftware/documentation/api/rest/util/RestApiUtils.java index 6687a4f..7051687 100644 --- a/java/src/com/tableausoftware/documentation/api/rest/util/RestApiUtils.java +++ b/java/src/com/tableausoftware/documentation/api/rest/util/RestApiUtils.java @@ -110,7 +110,7 @@ public static RestApiUtils getInstance() { * @return the URI builder */ private static UriBuilder getApiUriBuilder() { - return UriBuilder.fromPath(m_properties.getProperty("server.host") + "/api/3.0"); + return UriBuilder.fromPath(m_properties.getProperty("server.host") + "/api/3.1"); } /** * Initializes the RestApiUtils. The initialize code loads values from the configuration From 859a6380ddaf4c13c2979a1879c1ffb4fb4e80c8 Mon Sep 17 00:00:00 2001 From: Aaron Carey <31550444+aaroncarey@users.noreply.github.com> Date: Thu, 26 Jul 2018 13:37:58 -0700 Subject: [PATCH 4/7] Update README.md Update to change version numbers from 3.0 to 3.1 --- python/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/README.md b/python/README.md index c965296..3fdf1ea 100644 --- a/python/README.md +++ b/python/README.md @@ -10,8 +10,8 @@ Running the samples * All samples require 2 arguments: server adress (without a trailing slash) and username * Run by executing ```python sample_file_name.py ``` * Specific information for each sample are included at the top of each file -* API version is set to 3.0 by default for Tableau Server 2018.1, but it can be changed in [version.py](./version.py) -* For Tableau Server 9.0, the REST API namespace must be changed (refer to comment in each sample where namespace, xmlns, is defined) +* API version is set to 3.1 by default for Tableau Server 2018.2, but it can be changed in [version.py](./version.py) +* For Tableau Server 9.0, the REST API namespace must be changed (refer to comment in each sample where the namespace, `xmlns`, is defined) REST API Samples --------------- From c95a695a2184a113462294cd1baf75bff60cd8ce Mon Sep 17 00:00:00 2001 From: Aaron Carey <31550444+aaroncarey@users.noreply.github.com> Date: Thu, 26 Jul 2018 13:38:47 -0700 Subject: [PATCH 5/7] Update version.py Update the version number from 3.0 to 3.1 --- python/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/version.py b/python/version.py index 95da341..05edb42 100644 --- a/python/version.py +++ b/python/version.py @@ -1 +1 @@ -VERSION = '3.0' +VERSION = '3.1' From 937905b66d1b90f9a0aa2e26fb4b3687da1fce5f Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Thu, 22 Dec 2022 22:26:07 -0800 Subject: [PATCH 6/7] Update log4j and jaxb - Also added some debug statements and separated out possible exceptions for easier debugging. - Updated the code to refer to config.properties instead of requiring the user to edit code before running. - Set a default API version so that first run is slightly simpler --- .gitignore | 4 + java/README.md | 51 +- java/build.xml | 13 +- java/ivy.xml | 8 +- java/res/config.properties | 5 +- java/res/ts-api_3_0.xsd | 834 ++++++++++++++++++ .../documentation/api/rest/Demo.java | 17 +- .../api/rest/util/RestApiUtils.java | 56 +- 8 files changed, 919 insertions(+), 69 deletions(-) create mode 100644 .gitignore create mode 100644 java/res/ts-api_3_0.xsd diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa86404 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.pyc +/java/build/* +/java/.idea/* +/java/lib/* diff --git a/java/README.md b/java/README.md index 7745f00..d30591f 100644 --- a/java/README.md +++ b/java/README.md @@ -1,7 +1,7 @@ ## Requirements -* Tableau Server +* Tableau Server, or an account on Tableau Cloud * Java SDK * Download: * Apache Ant @@ -14,39 +14,14 @@ ## Getting started 1. Install the tools listed in the "Requirements" section. -1. Download the REST API schema and save it in the `/res` folder under the folder where this README file is. For more information about the schema, see the following documentation: - - - -1. In the `/res` folder, open the `config.properties` file using a text editor. -1. Modify the configurations as instructed in the file. A sample workbook is already provided with this sample, but you can use any packaged workbook that you want. -1. Open the following file in a text editor: - - ``` - src\com\tableausoftware\documentation\api\rest\util\RestApiUtils.java - ``` - -1. Find the `getApiUriBuilder()` method, and replace the API URL with the correct version number. - - For example, you might see the following URL: - - ``` - /api/3.1/ - ``` - - If you want to use version 3.0 of the API, replace the URL with the following: - - ``` - /api/3.0/ - ``` - -## Running the sample - -1. Make sure that Tableau Server is running. + 1. Make sure that Tableau Server is running if you are using your own server. +1. In the /res folder, open the config.properties file using a text editor. + 1. Modify the configurations as instructed in the file. + 2. A sample workbook is already provided with this sample, but you can use any packaged workbook that you want. 1. Open a command prompt or terminal. -1. In the command prompt window, change directory to the sample code's parent folder. -1. Enter `ant` in the command prompt to compile the sample code and download dependencies. -1. Enter `ant run` in the command prompt to run the sample code after compilation. + 1. In the command prompt window, change directory to the sample code's parent folder. + 1. Enter `ant` in the command prompt to compile the sample code and download dependencies. + 1. Enter `ant run` in the command prompt to run the sample code after compilation. ## Possible problems @@ -54,3 +29,13 @@ When `ant` is run in a command prompt, it may respond with "ant is not recognize Make sure that the `ANT_HOME` and `JAVA_HOME` variables are set as described in the installation guide for Apache Ant. Paths should not include quotes. For more information, see +------ + +The example code uses version 3.0 of the REST API and schema. If you want to use a different version: +1. Download the REST API schema and save it in the `/res` folder under the folder where this README file is. For more information about the schema, see the following documentation: + + + +1. In the `/res` folder, open the `config.properties` file using a text editor. +1. Update the schema configuration properties to match the new file. +------ \ No newline at end of file diff --git a/java/build.xml b/java/build.xml index 321b58f..1bac0b4 100644 --- a/java/build.xml +++ b/java/build.xml @@ -1,18 +1,11 @@ + + + diff --git a/java/ivy.xml b/java/ivy.xml index ddbf39d..5473028 100644 --- a/java/ivy.xml +++ b/java/ivy.xml @@ -4,7 +4,8 @@ - + + @@ -12,7 +13,10 @@ - + + + + \ No newline at end of file diff --git a/java/res/config.properties b/java/res/config.properties index 9cc66c4..e7d8b85 100644 --- a/java/res/config.properties +++ b/java/res/config.properties @@ -1,10 +1,11 @@ # Set this to the name or IP address of the Tableau Server installation. server.host=http://YOUR-SERVER -# Set where the REST API schema is located +# Set where the REST API schema is located and the version to use # The latest schema can be downloaded from here: # http://onlinehelp.tableau.com/current/api/rest_api/en-us/help.htm#REST/rest_api_concepts_schema.htm -server.schema.location=res/ts-api_X_X.xsd +server.schema.location=res/ts-api_3_0.xsd +server.schema.version=3.0 # Set this to the content URL of the default site. # Not assigning a value to this configuration references the default site. diff --git a/java/res/ts-api_3_0.xsd b/java/res/ts-api_3_0.xsd new file mode 100644 index 0000000..832c323 --- /dev/null +++ b/java/res/ts-api_3_0.xsd @@ -0,0 +1,834 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Not a real value. Used to specify 'no limit' when site is created or updated. Never returned from server in response. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/src/com/tableausoftware/documentation/api/rest/Demo.java b/java/src/com/tableausoftware/documentation/api/rest/Demo.java index 77aacbd..6b30625 100644 --- a/java/src/com/tableausoftware/documentation/api/rest/Demo.java +++ b/java/src/com/tableausoftware/documentation/api/rest/Demo.java @@ -9,8 +9,8 @@ import java.util.Map; import java.util.Properties; -import org.apache.log4j.BasicConfigurator; -import org.apache.log4j.Logger; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.LogManager; import com.tableausoftware.documentation.api.rest.bindings.GranteeCapabilitiesType; import com.tableausoftware.documentation.api.rest.bindings.GroupType; @@ -38,15 +38,16 @@ */ public class Demo { - private static Logger s_logger = Logger.getLogger(Demo.class); + private static Logger s_logger = LogManager.getLogger(); private static Properties s_properties = new Properties(); private static final RestApiUtils s_restApiUtils = RestApiUtils.getInstance(); static { - // Configures the logger to log to stdout - BasicConfigurator.configure(); + org.apache.logging.log4j.core.config.Configurator.setAllLevels( + LogManager.getRootLogger().getName(), org.apache.logging.log4j.Level.ALL); + s_logger.info("Configuring..."); // Loads the values from configuration file into the Properties instance try { @@ -57,6 +58,7 @@ public class Demo { } public static void main(String[] args) { + s_logger.info("Running..."); // Sets the username, password, and content URL, which are all required // in the payload of a Sign In request String username = s_properties.getProperty("user.admin.name"); @@ -65,6 +67,11 @@ public static void main(String[] args) { // Signs in to server and saves the authentication token, site ID, and current user ID TableauCredentialsType credential = s_restApiUtils.invokeSignIn(username, password, contentUrl); + + if (credential == null || credential.getSite() == null || credential.getToken() == null){ + s_logger.error("Failed to sign in: null or invalid credential returned"); + return; + } String currentSiteId = credential.getSite().getId(); String currentUserId = credential.getUser().getId(); diff --git a/java/src/com/tableausoftware/documentation/api/rest/util/RestApiUtils.java b/java/src/com/tableausoftware/documentation/api/rest/util/RestApiUtils.java index 7051687..405961b 100644 --- a/java/src/com/tableausoftware/documentation/api/rest/util/RestApiUtils.java +++ b/java/src/com/tableausoftware/documentation/api/rest/util/RestApiUtils.java @@ -22,7 +22,8 @@ import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; -import org.apache.log4j.Logger; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.LogManager; import org.xml.sax.SAXException; import com.google.common.io.Files; @@ -110,32 +111,49 @@ public static RestApiUtils getInstance() { * @return the URI builder */ private static UriBuilder getApiUriBuilder() { - return UriBuilder.fromPath(m_properties.getProperty("server.host") + "/api/3.1"); + return UriBuilder.fromPath(m_properties.getProperty("server.host") + + "/api/" + + m_properties.getProperty("server.schema.version")); } /** * Initializes the RestApiUtils. The initialize code loads values from the configuration * file and initializes the JAXB marshaller and unmarshaller. */ private static void initialize() { + m_logger.info("Initializing..."); try { m_properties.load(new FileInputStream("res/config.properties")); - JAXBContext jaxbContext = JAXBContext.newInstance(TsRequest.class, TsResponse.class); + } catch (Exception ex) { + throw new IllegalStateException("Failed to read configuration properties"); + } + Schema schema; + try { + final String schemaLocation = m_properties.getProperty("server.schema.location"); + m_logger.info("Schema at " + schemaLocation); + File schemaFile = new File(schemaLocation); SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); - Schema schema = schemaFactory.newSchema(new File(m_properties.getProperty("server.schema.location"))); + schema = schemaFactory.newSchema(schemaFile); + m_logger.info("Schema factory complete"); + } catch (SAXException ex) { + throw new IllegalStateException("Failed to load schema file"); + } + try { + JAXBContext jaxbContext = JAXBContext.newInstance(TsRequest.class, TsResponse.class); s_jaxbMarshaller = jaxbContext.createMarshaller(); s_jaxbUnmarshaller = jaxbContext.createUnmarshaller(); s_jaxbUnmarshaller.setSchema(schema); s_jaxbMarshaller.setSchema(schema); - } catch (JAXBException | SAXException | IOException ex) { - throw new IllegalStateException("Failed to initialize the REST API"); + } catch (JAXBException ex) { + throw new IllegalStateException("Failed to initialize the REST API with schema", ex); } + m_logger.info("Schema initialization complete"); } private final String TABLEAU_AUTH_HEADER = "X-Tableau-Auth"; private final String TABLEAU_PAYLOAD_NAME = "request_payload"; - private Logger m_logger = Logger.getLogger(RestApiUtils.class); + private static Logger m_logger = LogManager.getLogger(); private ObjectFactory m_objectFactory = new ObjectFactory(); @@ -398,7 +416,7 @@ public TableauCredentialsType invokeSignIn(String username, String password, Str TsResponse response = post(url, null, payload); // Verifies that the response has a credentials element - if (response.getCredentials() != null) { + if (response != null && response.getCredentials() != null) { m_logger.info("Sign in is successful!"); return response.getCredentials(); @@ -795,21 +813,23 @@ private TsResponse post(String url, String authToken) { * @return the response from the request */ private TsResponse post(String url, String authToken, TsRequest requestPayload) { - // Creates an instance of StringWriter to hold the XML for the request - StringWriter writer = new StringWriter(); + String payload = ""; // Marshals the TsRequest object into XML format if it is not null if (requestPayload != null) { + // Creates an instance of StringWriter to hold the XML for the request + StringWriter writer = new StringWriter(); + try { s_jaxbMarshaller.marshal(requestPayload, writer); } catch (JAXBException ex) { - m_logger.error("There was a problem marshalling the payload"); + m_logger.error("There was a problem marshalling the payload: " + ex); + m_logger.error("Not posting to " + url); + throw new IllegalStateException(ex); } + // Converts the XML into a string + payload = writer.toString(); } - - // Converts the XML into a string - String payload = writer.toString(); - m_logger.debug("Input payload: \n" + payload); // Creates the HTTP client object and makes the HTTP request to the @@ -1018,8 +1038,10 @@ private TsResponse unmarshalResponse(String responseXML) { StringReader reader = new StringReader(responseXML); tsResponse = s_jaxbUnmarshaller.unmarshal(new StreamSource(reader), TsResponse.class).getValue(); } catch (JAXBException e) { - m_logger.error("Failed to parse response from server due to:"); - e.printStackTrace(); + m_logger.error("Failed to parse response from server due to:" + e.toString()); + // if more information is needed + // e.printStackTrace(); + m_logger.error(responseXML); } return tsResponse; From 408134bf86849b75f540ee7cbc57c1482899350c Mon Sep 17 00:00:00 2001 From: Dan Zucker <49076749+dzucker-tab@users.noreply.github.com> Date: Fri, 10 Jan 2025 11:17:28 -0800 Subject: [PATCH 7/7] Adding python/add_user_to_pulse_metric_subscription.py. (#90) --- .../add_user_to_pulse_metric_subscription.py | 259 ++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 python/add_user_to_pulse_metric_subscription.py diff --git a/python/add_user_to_pulse_metric_subscription.py b/python/add_user_to_pulse_metric_subscription.py new file mode 100644 index 0000000..01718b9 --- /dev/null +++ b/python/add_user_to_pulse_metric_subscription.py @@ -0,0 +1,259 @@ +# ========================================================================================================== +# Script Name: Tableau Pulse Metric Subscription Script +# Description: +# This script subscribes a user to a Pulse metric on Tableau Cloud Site. +# The script utilizes the Tableau Server Client (TSC) and requests libraries +# to authenticate with Tableau Cloud Site using a Personal Access Token (PAT), +# fetch the user ID based on email, and subscribe the user to a specified +# Pulse metric. +# +# Help: +# - https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_pulse.htm#PulseSubscriptionService_CreateSubscription +# - https://help.tableau.com/current/api/rest_api/en-us/REST/TAG/index.html#tag/Pulse-Methods/operation/PulseSubscriptionService_CreateSubscription +# +# Input Parameters: +# - server_url: URL of your Tableau POD (e.g., https://dub01.online.tableau.com/) +# - site_name: Tableau site name (e.g., darkplatypus) +# - pat_name: Name of the Personal Access Token for an admin user +# - pat_secret: Secret key of the Personal Access Token for an admin user +# - user_email: Email address of the user to subscribe to the metric +# - metric_id: The LUID of the Pulse metric +# +# LUID and pod of a Pulse Metric: +# The Tableau Cloud pod is the is the first part of the domain URL, for example 10AX in the metric URL below. +# The LUID is the last part of the metric URL, for example 5aa997e2-07ed-4c60-bda5-154ca9f8d013 for the URL below. +# - https://dub01.online.tableau.com/pulse/site/darkplatypus/metrics/5aa997e2-07ed-4c60-bda5-154ca9f8d013 +# +# Requirements: +# - Python 3.x +# - tableauserverclient (TSC) library (https://tableau.github.io/server-client-python/docs/) +# - requests library +# +# Usage: +# - Install the required dependencies: +# pip install tableauserverclient requests +# +# - Run the script to subscribe a user: +# python subscribe_to_pulse_metric.py +# ========================================================================================================== + + +import tableauserverclient as TSC +import requests +import traceback +import sys + +class TableauServerConnection: + """ + Class to manage the Tableau Cloud Site connection using a context manager. + Handles authentication and ensures proper sign-in/sign-out from the server. + """ + def __init__(self, server_url, site_name, pat_name, pat_secret): + self.server_url = server_url + self.site_name = site_name + self.pat_name = pat_name + self.pat_secret = pat_secret + + def __enter__(self): + """ + Establishes the Tableau connection using Personal Access Token (PAT) credentials. + Validates required fields, creates auth and server objects, and signs in. + """ + # Validate that all required fields are set + if not all([self.server_url, self.site_name, self.pat_name, self.pat_secret]): + print("Missing Tableau configuration parameters.") + sys.exit(1) + + try: + # Create a Tableau auth object using Personal Access Token credentials + self.tableau_auth = TSC.PersonalAccessTokenAuth( + token_name=self.pat_name, + personal_access_token=self.pat_secret, + site_id=self.site_name + ) + # Create a server object + self.server = TSC.Server(self.server_url, use_server_version=True) + # Sign in to the server + self.server.auth.sign_in(self.tableau_auth) + return self.server + except Exception as e: + print(f"Error during Tableau authentication: {str(e)}") + print(traceback.format_exc()) + sys.exit(2) + + def __exit__(self, exc_type, exc_val, exc_tb): + """ + Signs out from the Tableau Cloud Site once operations are complete. + Ensures clean disconnection from the server. + """ + self.server.auth.sign_out() + + +def make_tableau_request(method, url, auth_token, headers=None, data=None, content_type='application/json'): + """ + Sends a request to Tableau Cloud Site. + Parameters: + - method: HTTP method (GET, POST, etc.) + - url: Endpoint URL for the request + - auth_token: Authentication token for the request + - headers: Optional HTTP headers + - data: Optional request payload (usually for POST/PUT) + - content_type: Content-Type for the request, defaults to 'application/json' + Returns: + - The server response object if the request is successful, otherwise None. + """ + if headers is None: + headers = {} + + headers.update({ + 'X-Tableau-Auth': auth_token, + 'Accept': 'application/json', + 'Content-Type': content_type + }) + + try: + if data: + if content_type == 'application/json': + response = requests.request(method, url, headers=headers, json=data, timeout=10) + else: + response = requests.request(method, url, headers=headers, data=data, timeout=10) + else: + response = requests.request(method, url, headers=headers, timeout=10) + + response.raise_for_status() + return response + except requests.exceptions.RequestException as e: + print(f"RequestException for URL {url}: {e}") + print(traceback.format_exc()) + return None + + +def get_user_id(server_url, site_name, pat_name, pat_secret, user_email): + """ + Fetches the user ID based on the provided email using a filtered query. + This method is faster than iterating through all users, as it uses RequestOptions + to filter the result server-side. + + Parameters: + - server_url: Tableau Cloud Site URL + - site_name: Tableau site name + - pat_name: Name of the Personal Access Token (PAT) + - pat_secret: Secret of the Personal Access Token (PAT) + - user_email: Email of the user to search for + + Returns: + - User ID if found, otherwise None. + """ + req_option = TSC.RequestOptions() + req_option.filter.add(TSC.Filter(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Operator.Equals, user_email)) + + try: + with TableauServerConnection(server_url, site_name, pat_name, pat_secret) as server: + all_users, pagination_item = server.users.get(req_option) + if all_users: + return all_users[0].id + else: + print(f"No user found with email: {user_email}") + return None + except Exception as e: + print(f"Error while fetching user: {str(e)}") + print(traceback.format_exc()) + sys.exit(3) + +def is_user_already_a_follower(server_url, site_name, pat_name, pat_secret, user_id): + """ + Checks if the user is already a follower of any Pulse metric on Tableau Cloud Site. + Parameters: + - server_url: Tableau Cloud Site URL + - site_name: Tableau site name + - pat_name: Name of the Personal Access Token (PAT) + - pat_secret: Secret of the Personal Access Token (PAT) + - user_id: ID of the user to check for existing subscription + + Returns: + - True if the user is already a follower, otherwise False. + """ + try: + with TableauServerConnection(server_url, site_name, pat_name, pat_secret) as server: + headers = { + 'X-Tableau-Auth': server.auth_token, + 'Accept': 'application/json' + } + page_size = 1000 + next_page_token = '' + + while True: + if next_page_token: + url = f'{server_url}/api/-/pulse/subscriptions?page_size={page_size}&page_token={next_page_token}' + else: + url = f'{server_url}/api/-/pulse/subscriptions?page_size={page_size}' + + response = requests.get(url, headers=headers) + data = response.json() + + for subscription in data.get('subscriptions', []): + follower_user_id = subscription.get('follower', {}).get('user_id') + if follower_user_id == user_id: + return True # User is already subscribed + + next_page_token = data.get('next_page_token') + if not next_page_token: + return False # No more pages, user is not a follower + except Exception as e: + print(f"An error occurred: {str(e)}") + return False + + +def subscribe_to_metric(server_url, site_name, pat_name, pat_secret, metric_id, user_id): + """ + Subscribes a user to a specified Pulse metric on Tableau Cloud Site. + Parameters: + - server_url: Tableau Cloud Site URL + - site_name: Tableau site name + - pat_name: Name of the Personal Access Token (PAT) + - pat_secret: Secret of the Personal Access Token (PAT) + - metric_id: ID of the Pulse metric to subscribe to + - user_id: ID of the user to subscribe to the metric + """ + with TableauServerConnection(server_url, site_name, pat_name, pat_secret) as server: + auth_token = server.auth_token + url = f'{server_url}/api/-/pulse/subscriptions' + data = { + "metric_id": metric_id, + "follower": {"user_id": user_id} + } + + response = make_tableau_request("POST", url, auth_token, data=data) + if response and response.status_code == 201: + print(f"Subscription to {metric_id} successful.") + else: + print(f"Failed to subscribe. Status code: {response.status_code}" if response else "Failed to subscribe.") + + + + + +if __name__ == "__main__": + + + # Tableau Cloud Site details + server_url = 'https://{tableau_pod}.online.tableau.com/' #for example https://10ax.online.tableau.com/ + site_name = 'your_site_name' + pat_name = 'your_pat_name' + pat_secret = 'your_pat_secret' + + # User email + user_email = 'user@example.com' + # Metric Id + metric_id = '{LUID_of_the_Pulse_metric}' # for example: 5aa997e2-07ed-4c60-bda5-154ca9f8d013 + + # Fetch user ID + user_id = get_user_id(server_url, site_name, pat_name, pat_secret, user_email) + + # Proceed if user ID is found + if user_id: + already_follower = is_user_already_a_follower(server_url, site_name, pat_name, pat_secret, user_id) + if not already_follower: + subscribe_to_metric(server_url, site_name, pat_name, pat_secret, metric_id, user_id) + else: + print(f"User with ID {user_id} is already subscribed to the metric.")