Run Python on Elgato Stream Deck

Stay up to date

Get notified when I publish something new, and unsubscribe at any time.

I've been using the Elgato Stream Deck for long time, and it's been a great addition to my desk. I use it for everything from launching apps, muting microphones, clearing Slack notifications, and so much more. It's a fantastic tool for automating parts of my day.

There are many plugins available for the Stream Deck, which extend its functionality with new integrations. However, I recently found myself wanting to do some Github API automation. Specifically, I wanted a quick and easy way to hit a 'Thumbs Up' button on the Deck to approve a pull request. I couldn't find a plugin that did this, so I decided to tackle this the next best way, by writing a Python script.

Quick Note: I'm using the Arc Browser for this tutorial, but you could use any browser that supports AppleScript. This is also MacOS-specific, but I'm sure there are similar ways to accomplish this on Windows.

Elgato Stream Deck

How to get the pull request URL [MacOS]

The first challenge was figuring out how I could retrieve the currently-open browser tab's URL. I'm using MacOS, so this was a matter of leveraging the osascript command to run an AppleScript. Open up the Script Editor app, and test it out until you get the desired result.

The following command will return the URL of the currently-viewed tab in Arc (Chrome and other browsers will be similar).

tell application "Arc"
    set currentURL to URL of active tab of window 1
    return currentURL
end tell

Running that, you can see that the result is the URL from whatever tab is currently open in Arc. Now I just need a way to get this result in a python script.

animation of script editor test

Running AppleScript from Python

Thanks to the subprocess module, this is pretty easy. I can run the AppleScript from within Python, and then parse the result. For convenience, I've wrapped this in a function.

import sys
import subprocess

def get_current_arc_window_url():
    # Define the AppleScript command
    applescript = """
    tell application "Arc"
        set currentURL to URL of active tab of window 1
        return currentURL
    end tell
    """

    # Run the AppleScript
    proc = subprocess.run(
        ["osascript", "-e", applescript], capture_output=True, text=True
    )

    # Get the output
    url = proc.stdout.strip()
    if not url:
        sys.exit(1)

    return url

Extracting the pull request info

Things are looking good. But there's a few things to consider at this point. First, I need to ensure that the URL I'm getting is actually a Github pull request. And I also need to parse the URL to get the pull request number. I'll tackle these next.

This should be simple enough with the help of a bit of regex. I'll use the re module to match the URL against a pattern. If the pattern matches, I'll return the pull request number. Otherwise, terminate the script.

import re

def get_pull_request_info(url):
    # Get the repository name and pull request number from the url using regex
    regex = r"^https:\/\/github\.com\/(?P<repo>.*)\/pull\/(?P<number>\d+)\/?.*$"
    match = re.match(regex, url)

    # Check if matches were found
    if not match:
        print("Could not parse url for pull request info")
        sys.exit(1)

    repo = match.group("repo")
    number = match.group("number")
    return repo, number

Using the Github API

Perfect, we can now grab whatever pull request the user is looking at, parse the URL, and get the pull request number. To wrap up the script portion of this task, we'll just use the requests module to make an API call to Github to approve the pull request.

First, I need a Github API token accessible to my script. I'm not going to try setting this from the environment, because I won't be able to control that from within the next portion of this tutorial. Instead, I'm going to read the token from a file. Let's call this file ~/.github-token.

def get_github_api_token(file_path):
    with open(file_path, "r") as file:
        token = file.read().strip()
        if not token:
            sys.exit(1)
    return token

With that token in scope now, I can make the API call to approve the pull request.

import requests

def approve_pull_request(repo, number, token):
    url = f"https://api.github.com/repos/{repo}/pulls/{number}/reviews"
    headers = {
        "Authorization": f"token {token}",
        "Accept": "application/vnd.github.v3+json",
    }
    data = {"event": "APPROVE"}

    response = requests.post(url, headers=headers, json=data)
    response.raise_for_status()

Refreshing the browser

Lastly, I like to see my approval in the browser, so I'm going to make one final AppleScript call to refresh the browser window.

def refresh_arc_window():
    # Define the AppleScript command
    applescript = """
    tell application "Arc"
        reload active tab of window 1
    end tell
    """

    # Run the AppleScript
    subprocess.run(["osascript", "-e", applescript])

Putting it all together

You can run this from the terminal to approve a pull request.

$ python approve_pull_request.py
#!/usr/bin/env python3
import re
import sys
import subprocess
import requests
import pathlib

TOKEN_FILE_PATH = pathlib.Path.home() / ".github-token"


def get_current_arc_window_url():
    # Define the AppleScript command
    applescript = """
    tell application "Arc"
        set currentURL to URL of active tab of window 1
        return currentURL
    end tell
    """

    # Run the AppleScript
    proc = subprocess.run(
        ["osascript", "-e", applescript], capture_output=True, text=True
    )

    # Get the output
    url = proc.stdout.strip()
    if not url:
        sys.exit(1)

    return url


def refresh_arc_window():
    # Define the AppleScript command
    applescript = """
    tell application "Arc"
        reload active tab of window 1
    end tell
    """

    # Run the AppleScript
    subprocess.run(["osascript", "-e", applescript])


def get_pull_request_info(url):
    # Get the repository name and pull request number from the url using regex
    regex = r"^https:\/\/github\.com\/(?P<repo>.*)\/pull\/(?P<number>\d+)\/?.*$"
    match = re.match(regex, url)

    # Check if matches were found
    if not match:
        print("Could not parse url for pull request info")
        sys.exit(1)

    repo = match.group("repo")
    number = match.group("number")
    return repo, number


def get_github_api_token(file_path):
    with open(file_path, "r") as file:
        token = file.read().strip()
        if not token:
            sys.exit(1)
    return token


def approve_pull_request(repo, number, token):
    url = f"https://api.github.com/repos/{repo}/pulls/{number}/reviews"
    headers = {
        "Authorization": f"token {token}",
        "Accept": "application/vnd.github.v3+json",
    }
    data = {"event": "APPROVE"}

    response = requests.post(url, headers=headers, json=data)
    response.raise_for_status()


def main():
    token = get_github_api_token(TOKEN_FILE_PATH)
    url = get_current_arc_window_url()
    repo, number = get_pull_request_info(url)
    approve_pull_request(repo, number, token)
    print(f"Approved https://github.com/{repo}/pull/{number}")
    refresh_arc_window()


if __name__ == "__main__":
    main()

If you don't care about using a Stream Deck or hooking this into anything else, you could stop here. But I want to be able to run this from my Stream Deck, so I'm going to take this a step further.

Running the script from the Stream Deck

Stream Deck does not currently offer a clean way to run any script you want. So my strategy for accomplishing this is to create an 'App' that can be launched. The App just wraps the shell command to run the script. This is a bit of a hack, but it works.

First, open up Automator and create a new 'Application'. Then add a 'Run Shell Script' action. Set the shell to /bin/bash and the input to no input. Then add the following script.

/path/to/python /path/to/approve_pull_request.py
Apple Automator

Save the app to your Applications folder. Give it one more test to ensure it's working by opening the app from Finder or Spotlight (ensure you are on a Github pull request page first).

Add a Stream Deck button

Now that we have an app that can run our script, we can add a button to the Stream Deck. Open up the Stream Deck app, and add a new button. Select the Open action under the System section, and then select the app we just created.

Screenshot of configuring the Stream Deck button

Conclusion

That's it! Now you can approve pull requests with the click of a button. There are a lot of learned lessons from getting this to work (using AppleScript for browser navigation, using Automator to run a script, etc). Hopefully, even if you don't have a Stream Deck, you can use some of these techniques to automate your own workflows.

If you have any questions or comments, feel free to reach out to me on Threads. I'm an active member of the Tech Threads community on there, and I'd love to hear from you.

Post by @markliederbach
View on Threads