Automate iOS Contacts, Location Services, and Open In Menu with Pythonista 1.4

Pythonista 1.4

Pythonista is the app that changed my iOS workflow a year ago. A Python interpreter with native access to iOS system features like photos, URLs, and interface elements, Pythonista allowed me to convert the scripts and macros that I was using on OS X to the iPad, automating iOS in better and sometimes unexpected ways. Pythonista eventually led to Editorial, also developed by Ole Zorn, which changed the way I write and work on my iPad every day.

Pythonista 1.4, available today on the App Store, is the biggest update to Zorn’s app to date. It includes a new UI for iOS 7 (the app is also iOS 7-only starting today), new modules and enhancements to existing ones, and, more importantly, it doubles down on iOS integration by bringing native support for contacts, location, and Open In.

New UI

Pythonista primarily consists of a script editor and a console view that don’t look out of place on iOS 7, so Zorn overhauled the rest of the app’s interface to make it fit with Apple’s new aesthetic. In short: glossy toolbars are gone, the icons (including the app’s one) have been redesigned, and white dominates the UI with light blue accents for buttons and other interactive elements.

The layout of Pythonista has stayed the same, with scripts listed alongside folders in a sidebar on the left, the script editor in the middle, and the console panel on the right.

Open In

I’m not a fan of Apple’s Open In menu for iOS apps: it creates duplicate files, it’s redundant, and iOS’ inter-app communication needs go beyond a way to send a copy of a file across apps. However, Open In is (mostly) everything we have on iOS, and Zorn decided to properly support it in Pythonista 1.4. The result is intriguing.

The premise is that Pythonista can receive files sent through the Open In menu from other apps. Any app that can send an image (like Skitch or iPhoto), a text file (Byword), a PDF document (PDF Expert), or any other file with the native Open In menu can now send it to Pythonista. By default, Pythonista will add a received file to an Inbox folder in the sidebar and show it with QuickLook.

This is the first big addition. Non-Python files can be viewed in Pythonista with the native iOS QuickLook previewer (the same that is used in Apple apps like Mail and Messages) and plain text files can be created and edited directly in Pythonista. While it was possible to call local files in Pythonista scripts before, putting them into the app’s Documents was a cumbersome process, as it required downloading them from the web with a script or using utilities like iExplorer on a Mac to transfer files via USB. With Open In and a native preview, you can now easily send files to Pythonista without needing a computer. But that’s not the best part.

Pythonista 1.4

The Open In menu can be automated and scripted with Pythonista 1.4. In the app’s settings, you can choose a script to automatically run when a file is received through Open In, and, in the script, you can reference the file’s path and sender app’s bundle ID as command line arguments. This is a huge addition for automated iOS workflows and chaining apps together – it means that you no longer need to manually pick files because you can use Open In as a script handler in Pythonista. You can run specific scripts automatically depending on the app that sent a file while also using that file without ever needing to manually pick it.

I have enhanced several of my scripts and workflows with Open In integration, cutting down time required to choose scripts to run and files to use. Hence, rather than a bland textual description, let’s play by example.

Take, for instance, my “DropPics” script, which uses the dropbox module to put an image in your Public folder in Dropbox and create an old-style public URL that is set to the clipboard and opened in Safari:

from dropboxlogin import get_client
dropbox_client = get_client()
import keychain
import console
import time
import httplib
from io import BytesIO
import datetime
import webbrowser
import urllib
import clipboard

today = datetime.datetime.now()

console.clear()

def DropPic(img):
titles = console.input_alert('Image Upload', 'Enter your image name below')
console.show_activity()
buffer = BytesIO()
img.save(buffer, 'JPEG')
buffer.seek(0)
imgname = today.strftime("%Y-%m-%d-at-%H-%M-%S") + '-' + titles + '.jpeg'
response = dropbox_client.put_file('/Public/' + imgname, buffer)
console.hide_activity()
encoded = urllib.quote(imgname, safe='')
final = 'http://dl.dropbox.com/u/YOURDROPBOXUSERID/' + encoded
clipboard.set(final)
browser = 'safari-' + final
webbrowser.open(browser)

if __name__ == '__main__':
import photos
img = photos.pick_image()
console.clear()
DropPic(img)

The script’s main purpose – taking an image file and putting it in a Dropbox folder – is a function, which means it can be used by other scripts in Pythonista.

Now comes the meaty part. Here’s the script, called “Open In”, that I set to run automatically every time a file is received via Open In from Pythonista:

import console
import sys
import clipboard

sender = sys.argv[2]
#print sys.argv[2]
if 'com.evernote.Skitch' in sender:
from DropPics import DropPic
import Image
opened = Image.open(sys.argv[1], 'r')
DropPic(opened)
elif 'com.metaclassy.bywordios' in sender:
import markdown2
text = open(sys.argv[1], 'r')
clipboard.set(markdown2.markdown(text.read(), extras=["header-ids", "footnotes", "fenced-code-blocks"]))
elif 'com.getdropbox.Dropbox' in sender:
import editor
text = open(sys.argv[1], 'r')
title = console.input_alert('New Script Name', 'Choose a name for your new script file.')
editor.make_new_file(title, text.read())
editor.reload_files()

This script is a handler for files and scripts: it doesn’t do anything on its own and it is, basically, a bridge for Open In.

When Pythonista receives a file via Open In, it saves two bits of information as command line arguments: the path to the new file (now inside the Inbox folder) and the bundle ID of the sender app; these will be, respectively, sys.argv[1] and sys.argv[2]. Bundle IDs, which should be unique to each iOS app, have a reverse-domain style name, such as “com.apple.mobilemail”, which is the bundle ID for Apple’s Mail app.

The Open In script does a series of checks to perform a different action or script depending on the app that sent a file. On line 5, the sender app’s bundle ID is saved as sender, which is the variable that three subsequent conditional blocks will check against.

The first one is run if an image was sent from Skitch. I like Skitch a lot, and I use it to annotate screenshots of beta apps that I’ll later send to developers for feedback. I was constantly saving and uploading screenshots from Skitch to Dropbox; Open In support in Pythonista allows me to make the entire process automated with just a few lines of code.

If “com.evernote.Skitch” is contained in the sender app’s name, Open In will hand off the received file to DropPics, which will upload it to Dropbox and automatically copy the shared URL and open the uploaded image in Safari (lines 10 and 11 open the image file and call the DropPic() function).

The process takes a few seconds to complete and everything is automated: once set up, I don’t have to fiddle with the script, I don’t have to save and pick the image – my handler script takes care of understanding where the file came from and giving it to another script. It almost seems unfair that this can be done on an iOS device.

The second conditional block is run if the sender app isn’t Skitch, but Byword. In that case, Pythonista will know that a text file has been sent, likely formatted in Markdown because that’s how I write in Byword. The markdown2 module is imported, the text file is opened, and the clipboard is automatically set to the HTML output of the text file. That’s four lines of code and it is only run if Byword was the sender app.

The last block runs if the sender app is Dropbox and I use it to create new scripts in Pythonista. Zorn’s app can’t sync its script library across the iPhone and iPad, and I was constantly saving scripts as text files to Dropbox with Drafts, opening them on a second device to copy the text, open Pythonista, and create a new script there as well.

With Open In, every time Pythonista sees that Dropbox sent a file it will read that file’s text and create a new script in the library with it. Lines 20 and 21 create a new file (with a title I manually type in an alert dialog) and reload Pythonista’s sidebar to show it. While I still need to manually upload scripts to Dropbox if I want to share them to a second device, Open In lets me remove the steps needed to manually copy text and create a file.

Here’s a video showing the workflow in action with Skitch and Dropbox.

A quick tip: if you want to know what an app’s bundle ID is, uncomment line 6 and send a file to Pythonista. The sender’s bundle ID will be printed in the console.

There are many possibilities opened by Pythonista’s integration with Open In, received files, and bundle IDs. You could offer multiple choices for the same app – e.g. do you want to upload this Skitch screenshot to Dropbox or manipulate it in Pythonista? You could check for the content or name of a file and run conditional blocks based on those variables – for Python scripts from Dropbox, do this, but for photos, perform this other action. You could build scripts to extract annotations from PDF documents and send them over email, or you could automate the process of saving photos to Evernote by using Open In and the (now better documented) evernote module. Within the limitations of iOS (namely, the Open In menu can’t send multiple files at once), Pythonista’s support for this feature is pretty fantastic and versatile.

Location Module

My readers know that I’ve longed wished for a scriptable iOS app with support for location data from iOS’ GPS module. Pythonista 1.4 gets just that with an aptly-named location module, which provides native access to geo-location services on iOS for addresses and coordinates.

Being able to fetch a device’s current location in a script is a powerful addition that comes with technical concerns: constantly firing up an iPhone’s GPS will consume battery. Thankfully, Zorn built the location module with functions aimed to fetch data and stop the location service soon after that data is saved; every time you’ll be using location, try to put location.get_location between location.start_updates() and location.stop_updates() so that Pythonista will trigger the GPS and turn it off after a few seconds. You’ll see that Pythonista is fetching your current location with the usual GPS icon in the iOS status and, of course, just like any other iOS app, Pythonista will ask for your permission before attempting to use any function of the location module.

Zorn’s implementation of location in Pythonista uses dictionaries for addresses and coordinates, which can be geo-coded (a readable address is turned into coordinates with latitute and longitude keys) or reverse geo-coded (coordinates are turned into a dictionary of possible readable addresses interpreted by iOS). The accuracy of the location module for your area depends entirely on iOS – Pythonista simply “Python-ifies” what iOS already does behind the scenes.

I’ve made an example script for what I’ve always wanted to be able to do in my scripts: fetch my location (coordinates) and present it as a human-readable address alongside a timestamp. The script, called “GetLocation”, can be used as a module in other scripts by calling the getLocation() function.

import console

def getLocation():
import location
import datetime
import time
"Returns current location and timestamp in readable format"
location.start_updates()
time.sleep(3)
current = location.get_location()
location.stop_updates()
address = location.reverse_geocode({'latitude': current['latitude'], 'longitude': current['longitude']})
loctime = address[0]['Street'] + ', ' + address[0]['City'] + ', ' + address[0]['Country'] + ' on ' + datetime.datetime.fromtimestamp(int(current['timestamp'])).strftime('%A, %d %B %Y at %-I:%M %p')
return loctime

if __name__ == '__main__':
print getLocation()

Line 8 turns on the GPS, and line 9 pauses the script for three seconds to allow Pythonista to fetch the current location; line 10 saves the current coordinates to a variable, and the GPS is turned off on line 11. Lines 12 and 13 take care of reverse geo-coding the location and constructing a variable that contains the street name, city, country and timestamp based on strftime. The end result is printed in the console.

To demonstrate the usefulness of getLocation, I tweaked a script that Zorn made to showcase Pythonista’s integration with the Camera Roll and Evernote to create a new note with a picture and location + timestamp information in Evernote. As with previous Evernote scripts, you’ll need a developer token to use this in Pythonista.

Pythonista 1.4

Mine is a simple tweak that imports getLocation (from the “GetLocation” script) and that inserts it as a string in a new Evernote note. What’s cool about Zorn’s script is that it properly attaches photos as Evernote resources to a note, which means that they’ll get a thumbnail preview in the app’s note list and they will be tappable in the note editor (for markup and other options).

Like Open In, support for iOS’ geo-location services creates interesting opportunities for Pythonista users. Whether you want to append a location string to a text log, send coordinates to other apps, or email readable addresses to a contact without having to copy and paste anything, Pythonista’s location module is capable and easy to use.

Other Improvements

There are several other changes and enhancements in Pythonista 1.4 worth mentioning.

On the iPad, the app supports external keyboard shortcuts. They are:

⌘R to run a script;

⌘Space to toggle the console;

⌘F to toggle search;

⌘K to clear the console output.

As with other third-party iPad apps, shortcuts are only available in areas of the app where the software keyboard would otherwise be shown.

The webbrowser module has received new functions to add a URL to Reading List and check whether a URL can be launched. The latter, webbrowser.can_open() is especially handy for checking if a third-party app with a registered URL scheme can be opened by Pythonista.

Here’s a simple script called “ReadLater” that, given a URL in the system clipboard, will add it to Safari’s Reading List and that can be easily triggered from a Launch Center Pro action.

import webbrowser
import clipboard

URL = clipboard.get()
webbrowser.add_to_reading_list(URL)

print 'URL saved to Reading List'

I don’t use contact management apps on my iOS devices and most of my contact-related shortcuts live in Launch Center Pro, but Pythonista 1.4 also comes with full support for iOS Contacts. I’m mentioning it in this section because I haven’t found a particular use for it, but this is a terrific addition for users who deal with contacts on their devices and wanted a way to automate the Address Book: you can add and remove individual contacts and groups, do searches, read notes and labels, and even add contacts to existing groups without ever using the Contacts app. Zorn created Person objects in Python to represent people in your contacts list, giving you access to each field associated with them like email, address, birthday, names, and more. It’s a truly impressive implementation and you can read more in the app’s docs.

If you’ve ever wished for a script that talked to you, there is a new text to speech module you can play with.

Pythonista 1.4

I’m happy to tell Dr. Drang that photos.save_image() now has the same behavior as the tap & hold menu on images shown in the console: it’ll save screenshots losslessly. I’ll (finally) write about my new scripts to handle screenshot generation and status bar cleaning in the next few weeks.

The app’s documentation and release notes mention every other minor improvement or fix in Pythonista 1.4. Notably: local sound effects can be played with parameters for pitch and volume; there’s a new option in the Settings to always clear the console before running a script; the auto-completion bubble can be long-tapped on the iPad to view documentation before accepting a suggestion; and, the action menu is directly editable without opening the Settings.

Automating iOS

The whole idea of being able to script and automate iOS still makes some people uneasy, but the truth is – Pythonista users have been able to speed up their iOS workflows for over a year without making too much of a fuss about it. Like it or not, parts of the same Python automation that’s been possible on OS X for years with the Terminal or apps like Keyboard Maestro are now available on iOS. Several aspects are different; others are more limited because of iOS; and yet there are features, like a unified image picker or GPS module, that make automating iOS easier and more fun. For a power user, it’s comforting to know that Python scripts and workflows can be used on iOS when an iPad is the only device available.

Pythonista has always been about automating iOS, leveraging Python to access system functionalities like the Camera, UIKit, web views, and more. With version 1.4, Zorn continues down the road of iOS automation with powerful new integrations for Contacts, Location, and Open In – all while making the app ready for iOS 7 and fixing annoyances and problems of the old Pythonista.

Support for Open In is a particularly handy enhancement in that, in spite of its flaws, it makes dealing with apps and files a much easier and streamlined process. I’m still tweaking my Open In script, trying to understand how I can make tedious tasks faster and invisible to me; it’ll be interesting to see what the Pythonista community will come up with.

Pythonista 1.4 is a great update and it’s available on the App Store.