Alteryx IO Resources

Explore these resources to customize and extend the power of Alteryx with SDKs, APIs, and more.

Python Web App with Alteryx Schedules using AAC API

briancoombs
Alteryx
Alteryx
Created

Overview

Follow this guide if you are a workspace admin or developer and are interested in monitoring Alteryx Analytics Cloud (AAC) scheduling using the AAC API. With the AAC API, you can set up a web app that shows your AAC schedules, or similar assets. You can further analyze your schedules for easier use, customizing them to your company’s specific needs, visualizing AAC data, or automating actions or reports on the same data.

The guide also serves to provide a simple example of what you can do quickly with the API and to show the potential to build larger projects.

Guide

Video Demo

Create and Store Your API Token

To create an API token for you AAC workspace, follow this guide.

Make sure you can successfully refresh your token and save it somewhere secure before you move on to any more advanced tutorials like the one shown here.

Dependencies

  1. Make sure you have Python and pip (Python package installer) installed.

  2. Use pip to install flask and requests:

    1. pip install flask requests
  3. This example uses Chart.js from npm. It's easily imported in 1 line in our HTML example code so no need to install anything yourself.

Use the API to Get Your Schedules

  1. Write a Python function like this example to get the schedules. Note that you can use this process for other assets you’d like to audit or examine in a way other than the AAC UI offers today.

  2. It’s recommended to initially test this script on its own and confirm that it prints. This is a good time to check if you have issues with your access token.

def get_schedules(access_token):
    endpoint = f"/v4/schedules"
    url = base_url + endpoint
    headers = {
        "Accept": "application/json",
        "Authorization": f"Bearer {access_token}",
        "x-trifacta-workspace-id": f"{workspace_id}"
    }
    try:
        response = requests.get(url, headers=headers)
        if response.status_code == 200:
            return response.json()
        else:
            logging.error(f"Failed to get scheduled workflows, status code: {response.status_code}")
            logging.error(response.text)
            return None
    except requests.exceptions.RequestException as e:
        logging.exception("Request failed due to an exception.")
        return None

Use Flask to Display Your Schedules in a Table

  1. Flask is a microweb framework for Python. Use it here to render the schedules you just fetched in HTML.

  2. Create a home page route that renders an index.html file that contains our schedules.

  3. Create a main to initially fetch our tokens and then run the app

Use this Python script for the home page route. Note that it first runs the get_schedules function we just wrote and then passes it to the index.html file to render it.

@app.route('/')
def index():
    fetchedSchedules = get_schedules(access_token)
    print(fetchedSchedules)
   
    return render_template('index.html', schedules=fetchedSchedules)

Next is the main execution for the Flask web app. You need to add your own code to what we have below to set the access and refresh tokens from however you’re securely storing your keys.

if __name__ == '__main__':
    # fetch your tokens initially from your key store, you will need to add your own code here
    access_token = "" 
    refresh_token = ""
    app.run(debug=True)

Rendering the Table in HTML

  1. Now we will work on the HTML for our results. You won’t be able to see anything from your Flask App until you add HTML to the index.html file you just passed your schedules to.

  2. This HTML script draws a table that takes the JSON data from the /v4/schedules endpoint and puts the various values in different columns. You can change this yourself if you first try printing the JSON data from the Schedules endpoint and determine other parts of the Schedules endpoint’s response that you’d prefer to show in your table. You can also look at our API Specification to see the expected response here: https://us1.alteryxcloud.com/documentation/api#operation/listSchedules.

<!DOCTYPE html>
<html>
<head>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <title>Scheduled Workflows</title>
    <style>
        table {
            border-collapse: collapse;
            width: 100%;
        }
        th, td {
            border: 1px solid black;
            padding: 8px;
            text-align: left;
        }
    </style>
</head>
<body>
    <h1>Scheduled Workflows</h1>

    {% if schedules %}
        <table>
            <thead>
                <tr>
                    <th>Schedule Name</th>
                    <th>Schedule</th>
                    <th>Last Run</th>
                    <th>Workflow/Plan Name</th>
                    <th>Owner</th>
                    <th>Updated At</th>
                </tr>
            </thead>
            <tbody>
                {% for schedule in schedules.data %}
                    <tr>
                        <td>{{ schedule.name }}</td>
                        <td>
                            {% if schedule.triggers[0].timeBased %}
                                {{schedule.triggers[0].timeBased}}
                            {% else %}
                                Triggered on file change
                            {% endif %}
                        </td>
                        <td>
                            {% if schedule.triggers[0].lastExecutionTime %}
                                {{schedule.triggers[0].lastExecutionTime}}
                            {% else %}
                                N/A
                            {% endif %}
                        </td>
                        <td>
                            {% if schedule.tasks[0].runFlow %}
                                {{ schedule.tasks[0].runFlow.name }}
                            {% elif schedule.tasks[0].runPlan %}
                                {{ schedule.tasks[0].runPlan.name }}
                            {% elif schedule.tasks[0].runWorkflow %}
                                {{ schedule.tasks[0].runWorkflow.name }}
                            {% else %}
                                N/A
                            {% endif %}
                        </td>
                        <td>{{ schedule.creator.name }}</td>
                        <td>{{ schedule.updatedAt }}</td>
                    </tr>
                {% endfor %}
            </tbody>
        </table>
    {% else %}
        <p>No scheduled workflows found.</p>
    {% endif %}
</body>
</html>

Making Chart JS Charts to Analyze the Scheduling Data

  1. Now add some charts to analyze the schedules data. Use Chart.js which we’ve already imported above in the HTML snippet.

  2. Add additional HTML and Javascript to calculate and render these Charts.

    1. The HTML creates <canvas> elements that we can then modify with the chart.js Javascript library.

  3. The Javascript counts the number of task types and owners and then graph this data.

    1. You can also do this with Python, but since we already are adding Javascript to use Chart.js we just added this here.

HTML Added

<div style="width: 800px;">
    <canvas id="schedules-by-owner-chart"></canvas>
</div>

<div style="width: 800px;">
    <canvas id="task-types-chart"></canvas>
</div>

Javascript Added (Inside index.html)

<script>
    const scheduleData = JSON.parse('{{ schedules|tojson|safe }}');

    var ownerCounts = {};
    var taskTypeCounts = {
        runFlow: 0,
        runWorkflow: 0,
        runPlan: 0
    };

    scheduleData.data.forEach(function(schedule) {
        var owner = schedule.creator.name;
        if (ownerCounts[owner]) {
            ownerCounts[owner]++;
        } else {
            ownerCounts[owner] = 1;
        }

        if (schedule.tasks[0].runFlow) {
            taskTypeCounts.runFlow++;
        } else if (schedule.tasks[0].runWorkflow) {
            taskTypeCounts.runWorkflow++;
        } else if (schedule.tasks[0].runPlan) {
            taskTypeCounts.runPlan++;
        }
    });

    var ownerCtx = document.getElementById('schedules-by-owner-chart').getContext('2d');
    new Chart(ownerCtx, {
        type: 'bar',
        data: {
            labels: Object.keys(ownerCounts),
            datasets: [{
                label: 'Schedules by Owner',
                data: Object.values(ownerCounts),
                backgroundColor: 'rgba(75, 192, 192, 0.6)',
                borderColor: 'rgba(75, 192, 192, 1)',
                borderWidth: 1
            }]
        },
        options: {
            scales: {
                y: {
                    beginAtZero: true,
                    stepSize: 1
                }
            }
        }
    });

    var taskTypeCtx = document.getElementById('task-types-chart').getContext('2d');
    new Chart(taskTypeCtx, {
        type: 'bar',
        data: {
            labels: Object.keys(taskTypeCounts),
            datasets: [{
                label: 'Task Types',
                data: Object.values(taskTypeCounts),
                backgroundColor: 'rgba(255, 99, 132, 0.6)',
                borderColor: 'rgba(255, 99, 132, 1)',
                borderWidth: 1
            }]
        },
        options: {
            scales: {
                y: {
                    beginAtZero: true,
                    stepSize: 1
                }
            }
        }
    });
</script>

Conclusion

You’ve completed these steps…

  1. Pulled schedules from your workspace.

  2. Rendered those schedules in a table.

  3. Added the schedule data to 2 graphs.

You’ve built a proof of concept that you can visualize AAC data in customizable ways for a variety of audit use cases. You can also make interactive web applications that let non-Alteryx experts interact with Alteryx resources.

To go further, a developer could improve this project to serve a variety of use cases…

  1. Run plans mentioned in these schedules through buttons in the rows of the table.

  2. Provide daily emailed reports or call out anomalous schedules, workflows, or schedule failures.

  3. Provide a larger audit log for all types of assets not just schedules.

Thank you for reading along and please reach out to brian.coombs@alteryx.com if you have issues or questions!

Full Python Script

This full script also includes some refresh token code. If you already have your own you can delete it. Just included it in case it would help.

from flask import Flask, render_template, request, redirect, url_for
import requests
import logging

app = Flask(__name__)

# Setup logging
logging.basicConfig(level=logging.INFO)

## Variables to adjust based on the environment
BASE_URL = "https://us1.alteryxcloud.com"

# Initial tokens (set these yourself securely from a secure key management system and refresh them by following our refresh token guide)
access_token = ""
refresh_token = ""
ISS = "https://pingauth.alteryxcloud.com/as/token"
CLIENT_ID = "YOUR_CLIENT_ID_HERE"

# get your workspace id using the v4/workspaces/current endpoint or find it by inspecting using developer tools
workspace_id = "your_workspace_id_here"

# refresh token code from our other guide, just to show you how it would fit in with the refresh token button
def refresh_tokens(refresh_token, file_path):
    print("refreshing tokens")
    refresh_url = iss
    headers = {
        "Content-Type": "application/x-www-form-urlencoded",
    }
    body = {
        "grant_type": "refresh_token",
        "refresh_token": f"{refresh_token}",
        "client_id": f"{client_id}"
    }
    try:
        response = requests.post(refresh_url, data=body, headers=headers)
        response.raise_for_status()  # Raises an HTTPError for bad responses
        new_token_info = response.json()
        new_access_token = new_token_info['access_token']  # Update the access token
        new_refresh_token = new_token_info['refresh_token'] # Update the refresh token
        #once returned, you'll need to save them wherever you need to
        return new_access_token, new_refresh_token
    except requests.exceptions.HTTPError as err:
        logging.error(f"Failed to refresh token: {err}")
        raise Exception("Could not refresh the tokens.") from err

# Function to make a GET request
def get_schedules(access_token):
    endpoint = f"/v4/schedules"
    url = base_url + endpoint
    headers = {
        "Accept": "application/json",
        "Authorization": f"Bearer {access_token}",
        "x-trifacta-workspace-id": f"{workspace_id}"
    }
    try:
        response = requests.get(url, headers=headers)
        if response.status_code == 200:
            return response.json()
        else:
            logging.error(f"Failed to get scheduled workflows, status code: {response.status_code}")
            logging.error(response.text)
            return None
    except requests.exceptions.RequestException as e:
        logging.exception("Request failed due to an exception.")
        return None

#### main exectution 

@app.route('/')
def index():
    fetchedSchedules = get_schedules(access_token)
    print(fetchedSchedules)
   
    return render_template('index.html', schedules=fetchedSchedules)

# This route below supports a button at the bottom where you can refresh the token manually. 
# You could also make this refresh automatically every time the page is opened by moving it up to the route above
@app.route('/refresh_token', methods=['POST'])
def refresh_token():
    global access_token, refresh_token
    access_token, refresh_token = refresh_tokens(refresh_token, file_path)
    return redirect(url_for('index'))

if __name__ == '__main__':
    # fetch your tokens initially from your key store, you will need to add your own code here
    access_token = "" 
    refresh_token = ""
    app.run(debug=True)

 

HTML

This full index.html also includes a refresh token button. This could also be done on refresh of the page or otherwise, so it could be removed in favor of your chosen method of token refreshing.

<!DOCTYPE html>
<html>
<head>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <title>Scheduled Workflows</title>
    <style>
        table {
            border-collapse: collapse;
            width: 100%;
        }
        th, td {
            border: 1px solid black;
            padding: 8px;
            text-align: left;
        }
    </style>
</head>
<body>
    <h1>Scheduled Workflows</h1>

    {% if schedules %}
        <table>
            <thead>
                <tr>
                    <th>Schedule Name</th>
                    <th>Schedule</th>
                    <th>Last Run</th>
                    <th>Workflow/Plan Name</th>
                    <th>Owner</th>
                    <th>Updated At</th>
                </tr>
            </thead>
            <tbody>
                {% for schedule in schedules.data %}
                    <tr>
                        <td>{{ schedule.name }}</td>
                        <td>
                            {% if schedule.triggers[0].timeBased %}
                                {{schedule.triggers[0].timeBased}}
                            {% else %}
                                Triggered on file change
                            {% endif %}
                        </td>
                        <td>
                            {% if schedule.triggers[0].lastExecutionTime %}
                                {{schedule.triggers[0].lastExecutionTime}}
                            {% else %}
                                N/A
                            {% endif %}
                        </td>
                        <td>
                            {% if schedule.tasks[0].runFlow %}
                                {{ schedule.tasks[0].runFlow.name }}
                            {% elif schedule.tasks[0].runPlan %}
                                {{ schedule.tasks[0].runPlan.name }}
                            {% elif schedule.tasks[0].runWorkflow %}
                                {{ schedule.tasks[0].runWorkflow.name }}
                            {% else %}
                                N/A
                            {% endif %}
                        </td>
                        <td>{{ schedule.creator.name }}</td>
                        <td>{{ schedule.updatedAt }}</td>
                    </tr>
                {% endfor %}
            </tbody>
        </table>
    {% else %}
        <p>No scheduled workflows found.</p>
    {% endif %}

    <div style="width: 800px;">
        <canvas id="schedules-by-owner-chart"></canvas>
    </div>

    <div style="width: 800px;">
        <canvas id="task-types-chart"></canvas>
    </div>

    <form action="{{ url_for('refresh_token') }}" method="POST">
        <button type="submit">Refresh Token</button>
    </form>

    <script>
        const scheduleData = JSON.parse('{{ schedules|tojson|safe }}');


        var ownerCounts = {};
        var taskTypeCounts = {
            runFlow: 0,
            runWorkflow: 0,
            runPlan: 0
        };

        scheduleData.data.forEach(function(schedule) {
            var owner = schedule.creator.name;
            if (ownerCounts[owner]) {
                ownerCounts[owner]++;
            } else {
                ownerCounts[owner] = 1;
            }

            if (schedule.tasks[0].runFlow) {
                taskTypeCounts.runFlow++;
            } else if (schedule.tasks[0].runWorkflow) {
                taskTypeCounts.runWorkflow++;
            } else if (schedule.tasks[0].runPlan) {
                taskTypeCounts.runPlan++;
            }
        });

        var ownerCtx = document.getElementById('schedules-by-owner-chart').getContext('2d');
        new Chart(ownerCtx, {
            type: 'bar',
            data: {
                labels: Object.keys(ownerCounts),
                datasets: [{
                    label: 'Schedules by Owner',
                    data: Object.values(ownerCounts),
                    backgroundColor: 'rgba(75, 192, 192, 0.6)',
                    borderColor: 'rgba(75, 192, 192, 1)',
                    borderWidth: 1
                }]
            },
            options: {
                scales: {
                    y: {
                        beginAtZero: true,
                        stepSize: 1
                    }
                }
            }
        });

        var taskTypeCtx = document.getElementById('task-types-chart').getContext('2d');
        new Chart(taskTypeCtx, {
            type: 'bar',
            data: {
                labels: Object.keys(taskTypeCounts),
                datasets: [{
                    label: 'Task Types',
                    data: Object.values(taskTypeCounts),
                    backgroundColor: 'rgba(255, 99, 132, 0.6)',
                    borderColor: 'rgba(255, 99, 132, 1)',
                    borderWidth: 1
                }]
            },
            options: {
                scales: {
                    y: {
                        beginAtZero: true,
                        stepSize: 1
                    }
                }
            }
        });
    </script>

</body>
</html>