introduction
While Python is good for building many things, MacOS applications are certainly not one of them. I wonder if I can use Python to build a menu bar application for MacOS. I found it not only possible, but also "very simple".
In this tutorial, we will build a real-time MacOS application for stock prices -- all in Python.
This article requires the complete project source code to be obtained here
Let's start by installing dependencies.
rumps requests py2app
PyObjC
Dumps generate PyObjC applications (especially menubar applications) from simple python code. To test the dump module, run the following code:
import rumps def hello(sender): print(f"Hello from {sender.title}") app = rumps.App("Hello World") app.menu = [rumps.MenuItem("Weird Menu Item",callback=hello)] app.run()
This hello() function executes when a menu item is clicked.
>>> Hello from Weird Menu Item
To add more menu items, we just need to add more elements to the app.Menu list. The parameter sender indicates the MenuItem that sets the callback.
A cleaner way to get the same result is to use rumps.clicked decorator:
@rumps.clicked("Weird Menu Item") @rumps.clicked("Saner Menu Item") def hello(sender): print(f"Hello from {sender.title}") app = rumps.App("Hello World") app.run()
In the rest of this tutorial, we will stick to the decorator's method.
Stock price data
There are many sources to get stock quotes. We will use Finnhub's API (based on the auth key, for free).
GET https://finnhub.io/api/v1/quote?symbol=AAPL&token=YOUR_API_KEY
Sample responses:
{ "c":119.49, "h":119.6717, "l":117.87, "o":119.44, "pc":119.21, "t":1605436627 }
You can go to H ttps://finnhub.io/register A free API key. At the time of writing, the rate was limited to 60 requests per minute.
Build StockApp
Start a class named "StockApp" as a subclass of the Rumps.App class, and use the rops.Click decorator to add some menu items:
import rumps class StockApp(rumps.App): def __init__(self): super(StockApp, self).__init__(name="Stock") @rumps.clicked("MSFT") @rumps.clicked("AAPL") def getStock(self, sender): self.title = f"{sender.title}" if __name__ == '__main__': StockApp().run()
It's time to integrate StockApp with the FinnHub API. Let's adjust getStock() as follows:
import requests import rumps class StockApp(rumps.App): def __init__(self): super(StockApp, self).__init__(name="Stock") @rumps.clicked("MSFT") @rumps.clicked("AAPL") def getStock(self, sender): response = requests.get(f"https://finnhub.io/api/v1/quote?symbol={sender.title}&token=YOUR_API_KEY") stock_data = response.json()['c'] self.title = f"{sender.title}:{stock_data}" if __name__ == '__main__': StockApp().run()
This getStock() method updates the title when the stock symbol is selected from the menu.
However, we don't want to get the price by clicking on the event. We need a function to constantly update the price of the selected stock, for example, every few seconds.
There is a timer class for mumps. You can use rumps.timer() to set a timer on the function, do the following.
@rumps.timer(1) def do_something(self, sender): # this function is executed every 1 second
At startup, we can set some default menu items, such as "AAPL". This option can be changed by clicking the event, and the timer decoration function will continuously update the price of the currently selected menu item.
@rumps.clicked("AAPL") @rumps.clicked("MSFT") def changeStock(self, sender): self.stock = sender.title @rumps.timer(5) def updateStockPrice(self, sender): # fetch stock quote and update title
Don't complicate this, but since the application will send a network request, we need to process the API request on another thread so that the application UI can continue to run during the request processing.
import threading @rumps.timer(5) def updateStockPrice(self, sender): thread = threading.Thread(target=self.getStock) thread.start() def getStock(self): # code to send API request
Put everything together
Here are its views on full implementation. We added icons to make the title more attractive and added user input (using rops.window).
import threading import requests import rumps class StockApp(rumps.App): def __init__(self): super(StockApp, self).__init__(name="Stock") self.stock = "AAPL" self.icon = "icon.png" self.API_KEY = "YOUR_API_KEY" @rumps.clicked("Search...") @rumps.clicked("MSFT") @rumps.clicked("TSLA") @rumps.clicked("NFLX") @rumps.clicked("FB") @rumps.clicked("AAPL") def changeStock(self, sender): if sender.title!="Search...": self.title = f" :mag: {sender.title}" self.stock = sender.title else: # Launches a rumps window for user input window = rumps.Window(f"Current: {self.stock}","Search another stock") window.icon = self.icon response = window.run() self.stock = response.text @rumps.timer(5) def updateStockPrice(self, sender): thread = threading.Thread(target=self.getStock) thread.start() def getStock(self): response = requests.get(f"https://finnhub.io/api/v1/quote?symbol={self.stock}&token={self.API_KEY}") if response.status_code!=200: self.title = "API Error." return stock_data = response.json() current_price = stock_data['c'] previous_close = stock_data['pc'] change = current_price-previous_close try: changePercentage = abs(round(100*(change/previous_close), 2)) if change<0: marker = ":small_red_triangle_down:" else: marker = ":small_red_triangle:" self.title = f" {self.stock}: {str(response.json()['c'])} {marker}{changePercentage}%" # Finnhub returns 0 for non-existent symbols except ZeroDivisionError: self.title = "Invalid symbol, set to AAPL" self.stock = "AAPL" if __name__ == '__main__': StockApp().run()
To run the application, you need to have an "ic.png" file in the same directory. You can download it from the link below or remove the icon from the program. Also, don't forget to assign the FinnHub API key to the self.API_KEY .
Download icon.png
Download icns
Convert it to. app
Now that the application is ready, we just need to generate a portable Mac OS application. We can use py2app to accomplish this task.
You need to have a setup.py file in the same directory. In addition, you can add an icon to the application. The file type of the MacOS application icon is. icns
StockApp |__ app.py |__ setup.py |__ icon.png |__ icon.icns
Setup.py file:
from setuptools import setup APP = ['app.py'] DATA_FILES = ['icon.png'] OPTIONS = { 'argv_emulation': True, 'iconfile': 'icon.icns', 'plist': { 'CFBundleShortVersionString': '1.0.0', 'LSUIElement': True, }, 'packages': ['rumps','requests'] } setup( app=APP, name='Stock', data_files=DATA_FILES, options={'py2app': OPTIONS}, setup_requires=['py2app'], install_requires=['rumps','requests'] )
Finally run setup:
python setup.py py2app
You can create a new dist folder. Open the application and see its action!
If you receive an error at runtime, please start the application through the terminal so that you can track it.
open Stock.App/Contents/MacOS/Stock
epilogue
As we can see, it's easy to create such a simple menu bar application. There are many things you can build on this, because it allows you to easily trigger Python functions. We can use a music controller, a server monitor to see if a program is running, stopwatch, CPU timer, flight position tracker, Internet speed test, to name a few.