David Hutchison

Downloading files with Pythonista

Pythonista Logo

I have been trying to broaden the programming languages that I am familiar with and I am giving Python a shot. I have been trying out Pythonista as my IDE. Pythonista is a universal app for iPad & iPhone (iTunes link). This is working out quite well for me, as it means I can read a book on my iPad, then just change apps to try out the things I have learned without needing to sit in front of my computer. It also allows me to do some more powerful things with my iOS devices.

The first script I want to share solves a little problem I have. While browsing the internet during the day on my iPhone, when I am away from my main computer, there will be files that I want to download later. Up to now the process has been to add a reminder with the link to come back to it later.

Now I can use Pythonista to download the file and upload it to my Dropbox folder. Later on, when I am at home, this will sync to my desktop and Hazel can process it appropriately.

Why Pythonista?

Pythonista is by the same developer as Editorial, my iPad text editor of choice. The workflow system in Editorial is powered by the same Python backend, so the modules available in the two applications are mostly the same. This means that any scripts I develop in Pythonista can be used as part of a text workflow. Eventually I would like to integrate the PyGitHub library into something so I can commit updates to this site without needing my desktop, but I appreciate this is not going to be an easy task.

One of the annoyances I have had with this application is there is not an easy way to transport scripts between the two platforms. The current solutions appear to revolve around using the GitHub Gist service. An example, which I could not get to work, is called gist check.

I am working on a solution to this problem that will use the Dropbox API to sync files. I am currently testing this, but it seems to still have a few bugs that need ironed out before it is released to the wild.

If you are looking for a complete review of the application, this is not the place. I would suggest you check out Federico Viticci’s excellent article: Automating iOS: How Pythonista Changed My Workflow.

Setting Up a Dropbox Developer Account

In order to use the Dropbox API you will require your own developer account. You will need to create an app and plug in your own APP_KEY and APP_SECRET values into the script below. I set up my script to use a seperate application directory, but you can choose to use the root of your Dropbox by changing the ACCESS_TYPE variable.

#### App folder

A dedicated folder named after your app is created within the Apps folder of a user's Dropbox. Your app gets read and write access to this folder only and users can provide content to your app by moving files into this folder. Your app can also read and write datastores using the Datastore API.

#### Full Dropbox

You get full access to all the files and folders in a user's Dropbox, as well as permission to read and write datastores using the Datastore API.

Your app should use the least privileged permission it can. When applying for production, we'll review that your app doesn't request an unnecessarily broad permission.

The Script

This script takes in a single argument, the URL to download. This script is not complicated, and really should perform validation, but I am new to Python and still learning.

You can either set this argument by holding the Run button in Pythonista, which will display a “Run With Arguments” dialog, or by use of a bookmarklet. In Chrome I have a bookmarklet that will take the current page URL and call the script via the pythonista:// URL scheme.

javascript:window.location='pythonista://FileDownloader.py?action=run&argv='+ encodeURIComponent(location.href)

The first time the script is run it will ask to be authenticated with Dropbox.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# Script for downloading a URL to Dropbox
import sys
import urllib2
import urllib
import dropbox
import os
import console

# Configuration
DOWNLOAD_FOLDER = 'downloads'
# Get your app key and secret from the Dropbox developer website
APP_KEY = '<your app key>'
APP_SECRET = '<your app secret>'
# ACCESS_TYPE should be 'dropbox' or 'app_folder' as configured for your app
ACCESS_TYPE = 'app_folder'

### Main program below ###
PYTHONISTA_DOC_DIR = os.path.expanduser('~/Documents')
SYNC_STATE_FOLDER = os.path.join(PYTHONISTA_DOC_DIR, 'dropbox_sync')
TOKEN_FILEPATH = os.path.join(SYNC_STATE_FOLDER, TOKEN_FILENAME)
 
def transfer_file(a_url):

    # Configure Dropbox
    sess = dropbox.session.DropboxSession(APP_KEY, APP_SECRET, ACCESS_TYPE)
    configure_token(sess)
    client = dropbox.client.DropboxClient(sess)
    
    print "Attempting to download %s" % a_url
    
    file_name = a_url.split('/')[-1]
    file_name = urllib.unquote(file_name).decode('utf8') 

    
    if not os.path.exists(DOWNLOAD_FOLDER):
        os.makedirs(DOWNLOAD_FOLDER)
        
    download_file = os.path.join(DOWNLOAD_FOLDER, file_name)
    
    u = urllib2.urlopen(a_url)
    f = open(download_file, 'wb')
    meta = u.info()
    file_size = int(meta.getheaders("Content-Length")[0])
    print "Downloading: %s Bytes: %s" % (file_name, file_size)
    
    file_size_dl = 0
    block_sz = 8192
    while True:
        buffer = u.read(block_sz)
        if not buffer:
            break

        file_size_dl += len(buffer)
        f.write(buffer)
        status = r"%10d  [%3.2f%%]" % (file_size_dl, file_size_dl * 100. / file_size)
        status = status + chr(8)*(len(status)+1)
        print status,
        
    f.close()
    
    print "Uploading to dropbox"
    upload(download_file, client)
    
    # Delete the local file
    os.remove(download_file)
    
    print "DONE !"

def upload(file, client):
    print "Trying to upload %s" % file

    response = client.put_file(file, open(file, 'r'), True)
    
    print "File %s uploaded to Dropbox" % file
    
 
def configure_token(dropbox_session):
    if os.path.exists(TOKEN_FILEPATH):
        token_file = open(TOKEN_FILEPATH)
        token_key, token_secret = token_file.read().split('|')
        token_file.close()
        dropbox_session.set_token(token_key,token_secret)
    else:
        setup_new_auth_token(dropbox_session)
    pass

def setup_new_auth_token(sess):
    request_token = sess.obtain_request_token()
    url = sess.build_authorize_url(request_token)
    
    # Make the user sign in and authorize this token
    print "url:", url
    print "Please visit this website and press the 'Allow' button, then hit 'Enter' here."
    webbrowser.open(url)
    raw_input()
    # This will fail if the user didn't visit the above URL and hit 'Allow'
    access_token = sess.obtain_access_token(request_token)
    #save token file
    token_file = open(TOKEN_FILEPATH,'w')
    token_file.write("%s|%s" % (access_token.key,access_token.secret) )
    token_file.close()
    pass

def main():

    # Attempt to take a URL from the arguments
    the_url = None
    try:
        the_url = sys.argv[1]
    except IndexError:
        # no arguments, use the clipboard contents
        the_url = clipboard.get()

    if not the_url:
        print repr(sys.argv)
        return

    console.clear()
    transfer_file(the_url)
 
if __name__ == '__main__':
    main()

This script is also available as a gist.

I would appriciate any feedback, as my coding experience these days is mostly Java and there may be a better way to do this in Python that I have missed.

Next?

After my last few Python projects are complete, I think the next language I want to try my hand at will be JavaScript. I did my final year project at University in JavaScript and have barely touched it since. I have a handful of projects in mind that shouldn’t be too difficult to achieve, but will still give me a good understanding of the language.