Create an Albert plugin using Python

Bharat Kalluri / 2018-06-23

Albert is a desktop agnostic launcher for linux. The functionality of the launcher can be extended by adding plugins.

It has a lot of positives

  • It is written in C++. So, it is blazing fast.
  • It is written in Qt, So it'll work on all desktops and linux distros.
  • It has a great set of awesome themes installed by default.

The best part is that, it can be extended using python!

Let us develop an extension which just shows the weather of a city with a trigger word like weather.

weather bangalore

should return weather of bangalore(high and low) and humidity. We will get data from openweathermap api. So, signup for an account and get a key from openweathermap api. If I say <API_KEY>, replace it with your own API key.

Understanding the structure of the plugin

Albert gives you a library called albert. To use it, the script needs to be inside the path of default python extension.

Some basic concepts to understand are that, the python library gives you a function called handleQuery which takes in a argument, which will be the query you type after the trigger. The metadata of the program will contain the trigger word, name and author. Finally, everything which pops under albert is an Item object. An Item object takes in a couple of arguments. It'll take an Id, icons which can take system icons using a helper function called iconLookup, text which is the heading for each item, and a subtext which will be the subtitle.

Let's start off by importing albert library and since we are using python 3, urllib3 will be great for making API requests. We should set up some metadata before starting to program.

from albert import *

import urllib3
import json

__iid__ = "PythonInterface/v0.4"
__title__ = "Weather"
__prettyname__ = "Weather"
__version__ = "0.1"
__triggers__ = "weather "
__authors__ = "Bharat Kalluri"
__dependencies__ = []

The initial function is handleQuery(query). Check if query is triggered, else display the default item.

def handleQuery(query):
    if query.isTriggered:
        if query.string.strip():
            return showWeather(query)
        else:
            return Item(
                id=__prettyname__,
                icon=iconLookup("weather-overcast"),
                text=__prettyname__,
                subtext="Type in the city name",
                completion=query.rawString,
            )

Now, showWeather contains the actual logic of calling the API and returning Items.

http = urllib3.PoolManager()
def showWeather(query):
    qurl = (
        "https://api.openweathermap.org/data/2.5/weather?appid=<API_KEY>&type=accurate&units=metric&q="
        + query.string.strip()
    )

    try:
        res = http.request("GET", qurl)
        data = json.loads(res.data)
    except:
        critical("No Internet!")
        return [
            Item(
                id=__prettyname__,
                icon=iconLookup("dialog-warning"),
                text="Is internet working?",
                subtext="We could not query, check your internet connection",
                completion=query.rawString,
            )
        ]

    if data["cod"] == "404":
        return [
            Item(
                id=__prettyname__,
                icon=iconLookup("weather-overcast"),
                text=__prettyname__,
                subtext="The city name does not exist in the database",
                completion=query.rawString,
            )
        ]
    else:
        critical(weatherDict.get(data["weather"][0]["description"], "weather-overcast"))
        return [
            Item(
                id=__prettyname__,
                icon=iconLookup(
                    weatherDict.get(
                        data["weather"][0]["description"], "weather-overcast"
                    )
                ),
                text="Weather in {}: {}".format(
                    data["name"], data["weather"][0]["main"]
                ),
                subtext="High: {}°C ({}°F) Low: {}°C ({}°F) Humidity: {}%".format(
                    data["main"]["temp_min"],
                    fahrenheitConverter(data["main"]["temp_min"]),
                    data["main"]["temp_max"],
                    fahrenheitConverter(data["main"]["temp_max"]),
                    data["main"]["humidity"],
                ),
                completion=query.rawString,
            )
        ]

Let me quickly summarize what's happening in there. Since the function already has a query of a city name, append the city name to the url. Before making the request, an instance of pool manager must be created which will make the requests. Then, It tries to make a request, if it fails, it returns an item saying 'Check your internet connection'.

Now, since we have the API response. Check if the status code is 200, if not the city does not exist in the database. So, show an item saying the city does not exist in the database. If it does, then the code will be 200. Now parse the json for relevant data and format the data into an item and return it.

One thing to note is that, extensions can be debugged by starting albert in terminal and putting critical() or debug() to test the response. Another thing to note is, all default linux icon sets have weather icons already. So, we take the description from the API and find the corresponding icon. Normally, switch works well in this case, since we do not have switch statements in python. Create a dictionary of conditions as such.

weatherDict = {
    "clear sky": "weather-clear",
    "few clouds": "weather-few-clouds",
    "scattared clouds": "weather-overcast",
    "broken clouds": "weather-overcast",
    "shower rain": "weather-showers",
    "rain": "weather-showers",
    "thunderstorm": "weather-storm",
    "snow": "weather-snow",
    "mist": "weather-fog",
}

Use the dictionary to get the icon name for the value in the API. By the way, since fahrenheit is also necessary for some people. Let us make a simple converter from celsius.

def fahrenheitConverter(celsius):
    return 9 / 5 * celsius + 32

In less than 100 lines of code, a simple albert extension was made! In the future, I will add 5-day forecast and let the user input their API key and preferred default city.

Many more extensions can be found at Albert hub.

Hand crafted by Bharat Kalluri