Posted by virantha on Mon 28 November 2016

How to use Python to generate extensions with a GUI for Moneydance 2017

This post describes some of my experiences writing a python extension for Moneydance, a cross-platform personal finance manager (an alternative to Intuit Quicken). I was originally attracted to this product because it runs on a Mac (albeit using a Java look and feel), uses Direct Connect to download transactions from financial instituitions, has a reasonable price, and supports scripting to get at the entire transaction and account internals.

Most of this post is built-around using Python (via the built-in Jython 2.7 interface) to build an extension based on the information in the links below. Hopefully, this post will fill in some of the gaps. I was completely unfamiliar with Jython when I started, but I've been amazed at how easy it is to seamlessly integrate with any Java library, including Swing for building GUI's. You just import the Java library you want and you have complete access to all the APIs.

1   Pop up a simple frame with a button

Here's a simple script based off the Moneydance template that will pop-up a frame with a scrollable list of the names of the current accounts.

Popup

Keep in mind that in Moneydance developer terminology an account can be either a top-level user account like 'Checking' or a Category.

You can see the power and conciseness of Jython married with access to Moneydance internals in the few lines of code it took to accomplish the above:

To get this installed, simply open Moneydance 2017 (previous versions will not work) and select Python Scripting from the Windows menu. This will pop-up the Python interface where you'll do the following:

Installing the extension

If all goes well, in the Extensions menu you will see a new item called Test popup. If you select this, then the sample code will run and generate the popup.

2   Importing Python files

If you want to import code from another local file, you need to append the location of your script to sys.path before doing the import in your top-level script:

if __name__ == '__main__' and __package__ is None:
        search_path = os.path.dirname(os.path.abspath(__file__))
        if search_path not in sys.path:
                sys.path.append(search_path)

Caveat: It seems like Moneydance does not re-import any imports if you reload a script, making any updates to files other than your top-level script not visible except for the first time after a clean Moneydance startup. While not an issue when deploying a finished extension, this is a pain during development when you're changing code all the time, and the work-around I've found is to reload your module right after the import:

import my_module
reload(my_module)

This forces moneydance to re-import your local dependencies when you want to break your code into multiple files.

3   Import JAR files

This took me a long time to figure out. If you need to use a third-party Java library supplied as a JAR, I've so far found two ways to do this:

1. Copy the JAR into Moneydance's Java library path. On my Mac, this was inside the application bundle at /Applications/Moneydance.app/Contents/Java/Library. I suspect if you modify your system classpath and put it in there, that will work too, but I have not tried this. Regardless, if you use this method, then all you need is to add the import like so.

from net.miginfocom.swing import MigLayout

2. If you need a more portable way to distribute this without having the user install random jar files into their system libraries, I found I had to use a custom classloader to make sure all the introspection was supported (For example, I was trying to use a third-party Swing layout library called MigLayout). You can read more about this here, but here's the code you need to insert at the top of your script.

def loadJar(jarFile):
        '''load a jar at runtime using the system Classloader (needed for JDBC)
        adapted from http://forum.java.sun.com/thread.jspa?threadID=300557
        Author: Steve (SG) Langer Jan 2007 translated the above Java to Jython
        Reference: https://wiki.python.org/jython/JythonMonthly/Articles/January2007/3
        Author: seansummers@gmail.com simplified and updated for jython-2.5.3b3+
        '''
        from java import io, net, lang
        u = io.File(jarFile).toURL() if type(jarFile) <> net.URL else jarFile
        m = net.URLClassLoader.getDeclaredMethod('addURL', [net.URL])
        m.accessible = 1
        m.invoke(lang.ClassLoader.getSystemClassLoader(), [u])

if __name__ == '__main__' and __package__ is None:
        search_path = os.path.dirname(os.path.abspath(__file__))
        loadJar(os.path.join(search_path, 'miglayout.jar'))

from java import lang
MigLayout = lang.Class.forName('net.miginfocom.swing.MigLayout')

Now, the MigLayout class is available for use just like an import. Note that this assumes the .jar file is in the same location as your top-level script (search_path)

4   Persisting data in your secure moneydance file

If your extension needs to persist or save data to a file between sessions, there's an easy way to do it through Moneydance's LocalStorage API. This basically gives you programmatic access to the currently open and encrypted .moneydance file that stores your user information, providing a secure place to store things like passwords and pins.

Here are the steps to access it and persist a .pickle file to it:

from com.infinitekind.moneydance.model import *
from org.python.core.util import FileUtil
from java.io import FileNotFoundException
import pickle

my_data_object = { 'anything': 'whatever'}
root_account = moneydance.getRootAccount()
local_storage = root_account.getBook().getLocalStorage()

# Save a file
ostr = local_storage.openFileForWriting('test.pickle')
save_file = FileUtil.wrap(ostr)
pickle.dump(my_data_object, save_file)
save_file.close()

# Open the file
try:
    istr = local_storage.openFileForReading('test.pickle')
    load_file = FileUtil.wrap(istr)
    my_data_object = pickle.load(load_file)
except FileNotFoundException as e:
    my_data_object =  None
except EOFError as e:
    my_data_object =  None

5   Bundling Python into a Moneydance Java Extension

One thing you may notice is that installing Python extensions in this way only keeps your extension around while Moneydance is running. If you quit and restart, then you have to reinstall the extension, which is probably a current limitation of the way Python extensions are supported in Moneydance.

However, after quite a bit of reading on the way Jython is integrated into Java (including this page), I've come up with the flow below to package up your Python extension as a standard Moneydance .mxt extension and deploy it through the regular (and persistent) extension manager.

5.1   Install the Developer Kit

These instructions assume you're able to get the Java extension development kit downloaded and running. The example found on the Developer page under 'Download Developer's Kit' is quite easy to get running. All you need is the JDK and the Ant build system (if you're on a Mac, just install Sun's JDK and use Homebrew to install ant).

You can test the sample extension supplied in this kit by going into the src sub-directory, which contains the build instructions in build.xml, and first generating your signing keys by running the following:

ant genkeys

Then, everytime you want to build the extension, you type

ant myextension

It will prompt you to enter the passphrase you used for the keys in the genkeys step, and build the file dist/myextension.mxt. Then, just go to the Moneydance menu Extensions -> Manage Extensions... and click on the button at the bottom that says Add from file.... Find the new myextension.mxt and install it, and then go to your Extensions menu and pick the new item Account list that will use the extension you just installed to pop up a list of your accounts.

Now that you have Moneydance's example extension building successfully, we can modify it slightly as discussed in the following to allow Python extensions.

5.2   Download the Jython Standalone .jar

In order to build extensions with Python, we need to add the Jython runtime to each extension (since it's currently not bundled with Moneydance). Go to the Jython downloads page and get the Standalone.jar for Jython 2.7.0. Move this file into your devkit's lib directory.

5.3   Modify build.xml

We're going to slightly modify the supplied build.xml as shown below:

First, around line 26 we add our jython.jar include, and then we add a python subdirectory to the code directory on line 46. The python subdirectory gets copied to the root of the .mxt file during the build, which is important for being able to import the extension in the next section.

Go ahead and replace your build.xml with this file.

5.4   Modify Main.java

Now, we'll modify the main Java file for the extension to support arbitrary Python extensions like so:

Here's what's going on:

  • First, to clean up, I'm taking out all references to the AccountList popup. We'll use our own Python extension from the earlier part of this blog post to replace it.

  • Then, I'm importing the Jython supplied PythonInterpreter, which is used to instatiate an interpreter from which we'll call our Python code.

  • In the init method, I register the invocation string as 'popup', which is the same string used in our Python extension.

  • Following that, I do the following on lines 35-46:
    1. Instantiate the interpreter.
    2. Get the path to the extension using the builtin getSourceFile. This is a key step, because we need this .mxt to point our sys.path at.
    3. Now, set the jarpath variable to this .mxt file, and execute some Python statements to import the Python extension (I'm calling it pyextension.py).
    4. The reload is very important to make sure any changes to your Python extension during deveoplment are updated every time the extension is reinstalled.
    5. I then instantiate the extension and manually instantiate it using the Moneydance context.
  • Then, in the invoke method on line 75, I merely passthrough whatever invocation command Moneydance sends me (in this case, only popup from the registration command) to our Python extension.

That's it!

5.5   Python extension

The Python extension is hardly changed from the original that we discussed above:

The only change is I added a parameter to the initialize method called from_java, which if set to True, will prevent the Python extension from also registering a menu item. I also changed the reference to get the Context to using a member variable (not sure if this is needed or not, but it seemed cleaner).

And that's it! Now, the nice thing is that you can test the Python extension unchanged through the Python interface in Moneydance, and when you're ready, you can just compile the java extension for deployment.

Just put this file in src/com/moneydance/modules/features/myextension/python (new python subdirectory).

5.6   Compiling

Just run ant myextension again from your src directory and this should rebuild the extension. And there you go, a Python extension bundled into a .mxt file that can be installed inside Moneydance permanently!

© Virantha Ekanayake. Built using Pelican. Modified svbhack theme, based on theme by Carey Metcalfe