Run Python on Elgato Stream Deck
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.
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.
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
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.
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 @markliederbachView on Threads