1defa7309cd03d650186ea486ec45207696428ff8mikesamuel#!/usr/bin/env python 2defa7309cd03d650186ea486ec45207696428ff8mikesamuel# 3defa7309cd03d650186ea486ec45207696428ff8mikesamuel# Copyright 2006, 2007 Google Inc. All Rights Reserved. 4defa7309cd03d650186ea486ec45207696428ff8mikesamuel# Author: danderson@google.com (David Anderson) 5defa7309cd03d650186ea486ec45207696428ff8mikesamuel# 6defa7309cd03d650186ea486ec45207696428ff8mikesamuel# Script for uploading files to a Google Code project. 7defa7309cd03d650186ea486ec45207696428ff8mikesamuel# 8defa7309cd03d650186ea486ec45207696428ff8mikesamuel# This is intended to be both a useful script for people who want to 9defa7309cd03d650186ea486ec45207696428ff8mikesamuel# streamline project uploads and a reference implementation for 10defa7309cd03d650186ea486ec45207696428ff8mikesamuel# uploading files to Google Code projects. 11defa7309cd03d650186ea486ec45207696428ff8mikesamuel# 12defa7309cd03d650186ea486ec45207696428ff8mikesamuel# To upload a file to Google Code, you need to provide a path to the 13defa7309cd03d650186ea486ec45207696428ff8mikesamuel# file on your local machine, a small summary of what the file is, a 14defa7309cd03d650186ea486ec45207696428ff8mikesamuel# project name, and a valid account that is a member or owner of that 15defa7309cd03d650186ea486ec45207696428ff8mikesamuel# project. You can optionally provide a list of labels that apply to 16defa7309cd03d650186ea486ec45207696428ff8mikesamuel# the file. The file will be uploaded under the same name that it has 17defa7309cd03d650186ea486ec45207696428ff8mikesamuel# in your local filesystem (that is, the "basename" or last path 18defa7309cd03d650186ea486ec45207696428ff8mikesamuel# component). Run the script with '--help' to get the exact syntax 19defa7309cd03d650186ea486ec45207696428ff8mikesamuel# and available options. 20defa7309cd03d650186ea486ec45207696428ff8mikesamuel# 21defa7309cd03d650186ea486ec45207696428ff8mikesamuel# Note that the upload script requests that you enter your 22defa7309cd03d650186ea486ec45207696428ff8mikesamuel# googlecode.com password. This is NOT your Gmail account password! 23defa7309cd03d650186ea486ec45207696428ff8mikesamuel# This is the password you use on googlecode.com for committing to 24defa7309cd03d650186ea486ec45207696428ff8mikesamuel# Subversion and uploading files. You can find your password by going 25defa7309cd03d650186ea486ec45207696428ff8mikesamuel# to http://code.google.com/hosting/settings when logged in with your 26defa7309cd03d650186ea486ec45207696428ff8mikesamuel# Gmail account. If you have already committed to your project's 27defa7309cd03d650186ea486ec45207696428ff8mikesamuel# Subversion repository, the script will automatically retrieve your 28defa7309cd03d650186ea486ec45207696428ff8mikesamuel# credentials from there (unless disabled, see the output of '--help' 29defa7309cd03d650186ea486ec45207696428ff8mikesamuel# for details). 30defa7309cd03d650186ea486ec45207696428ff8mikesamuel# 31defa7309cd03d650186ea486ec45207696428ff8mikesamuel# If you are looking at this script as a reference for implementing 32defa7309cd03d650186ea486ec45207696428ff8mikesamuel# your own Google Code file uploader, then you should take a look at 33defa7309cd03d650186ea486ec45207696428ff8mikesamuel# the upload() function, which is the meat of the uploader. You 34defa7309cd03d650186ea486ec45207696428ff8mikesamuel# basically need to build a multipart/form-data POST request with the 35defa7309cd03d650186ea486ec45207696428ff8mikesamuel# right fields and send it to https://PROJECT.googlecode.com/files . 36defa7309cd03d650186ea486ec45207696428ff8mikesamuel# Authenticate the request using HTTP Basic authentication, as is 37defa7309cd03d650186ea486ec45207696428ff8mikesamuel# shown below. 38defa7309cd03d650186ea486ec45207696428ff8mikesamuel# 39defa7309cd03d650186ea486ec45207696428ff8mikesamuel# Licensed under the terms of the Apache Software License 2.0: 40defa7309cd03d650186ea486ec45207696428ff8mikesamuel# http://www.apache.org/licenses/LICENSE-2.0 41defa7309cd03d650186ea486ec45207696428ff8mikesamuel# 42defa7309cd03d650186ea486ec45207696428ff8mikesamuel# Questions, comments, feature requests and patches are most welcome. 43defa7309cd03d650186ea486ec45207696428ff8mikesamuel# Please direct all of these to the Google Code users group: 44defa7309cd03d650186ea486ec45207696428ff8mikesamuel# http://groups.google.com/group/google-code-hosting 45defa7309cd03d650186ea486ec45207696428ff8mikesamuel 46defa7309cd03d650186ea486ec45207696428ff8mikesamuel"""Google Code file uploader script. 47defa7309cd03d650186ea486ec45207696428ff8mikesamuel""" 48defa7309cd03d650186ea486ec45207696428ff8mikesamuel 49defa7309cd03d650186ea486ec45207696428ff8mikesamuel__author__ = 'danderson@google.com (David Anderson)' 50defa7309cd03d650186ea486ec45207696428ff8mikesamuel 51defa7309cd03d650186ea486ec45207696428ff8mikesamuelimport httplib 52defa7309cd03d650186ea486ec45207696428ff8mikesamuelimport os.path 53defa7309cd03d650186ea486ec45207696428ff8mikesamuelimport optparse 54defa7309cd03d650186ea486ec45207696428ff8mikesamuelimport getpass 55defa7309cd03d650186ea486ec45207696428ff8mikesamuelimport base64 56defa7309cd03d650186ea486ec45207696428ff8mikesamuelimport sys 57defa7309cd03d650186ea486ec45207696428ff8mikesamuel 58defa7309cd03d650186ea486ec45207696428ff8mikesamuel 59defa7309cd03d650186ea486ec45207696428ff8mikesamueldef upload(file, project_name, user_name, password, summary, labels=None): 60defa7309cd03d650186ea486ec45207696428ff8mikesamuel """Upload a file to a Google Code project's file server. 61defa7309cd03d650186ea486ec45207696428ff8mikesamuel 62defa7309cd03d650186ea486ec45207696428ff8mikesamuel Args: 63defa7309cd03d650186ea486ec45207696428ff8mikesamuel file: The local path to the file. 64defa7309cd03d650186ea486ec45207696428ff8mikesamuel project_name: The name of your project on Google Code. 65defa7309cd03d650186ea486ec45207696428ff8mikesamuel user_name: Your Google account name. 66defa7309cd03d650186ea486ec45207696428ff8mikesamuel password: The googlecode.com password for your account. 67defa7309cd03d650186ea486ec45207696428ff8mikesamuel Note that this is NOT your global Google Account password! 68defa7309cd03d650186ea486ec45207696428ff8mikesamuel summary: A small description for the file. 69defa7309cd03d650186ea486ec45207696428ff8mikesamuel labels: an optional list of label strings with which to tag the file. 70defa7309cd03d650186ea486ec45207696428ff8mikesamuel 71defa7309cd03d650186ea486ec45207696428ff8mikesamuel Returns: a tuple: 72defa7309cd03d650186ea486ec45207696428ff8mikesamuel http_status: 201 if the upload succeeded, something else if an 73defa7309cd03d650186ea486ec45207696428ff8mikesamuel error occured. 74defa7309cd03d650186ea486ec45207696428ff8mikesamuel http_reason: The human-readable string associated with http_status 75defa7309cd03d650186ea486ec45207696428ff8mikesamuel file_url: If the upload succeeded, the URL of the file on Google 76defa7309cd03d650186ea486ec45207696428ff8mikesamuel Code, None otherwise. 77defa7309cd03d650186ea486ec45207696428ff8mikesamuel """ 78defa7309cd03d650186ea486ec45207696428ff8mikesamuel # The login is the user part of user@gmail.com. If the login provided 79defa7309cd03d650186ea486ec45207696428ff8mikesamuel # is in the full user@domain form, strip it down. 80defa7309cd03d650186ea486ec45207696428ff8mikesamuel if user_name.endswith('@gmail.com'): 81defa7309cd03d650186ea486ec45207696428ff8mikesamuel user_name = user_name[:user_name.index('@gmail.com')] 82defa7309cd03d650186ea486ec45207696428ff8mikesamuel 83defa7309cd03d650186ea486ec45207696428ff8mikesamuel form_fields = [('summary', summary)] 84defa7309cd03d650186ea486ec45207696428ff8mikesamuel if labels is not None: 85defa7309cd03d650186ea486ec45207696428ff8mikesamuel form_fields.extend([('label', l.strip()) for l in labels]) 86defa7309cd03d650186ea486ec45207696428ff8mikesamuel 87defa7309cd03d650186ea486ec45207696428ff8mikesamuel content_type, body = encode_upload_request(form_fields, file) 88defa7309cd03d650186ea486ec45207696428ff8mikesamuel 89defa7309cd03d650186ea486ec45207696428ff8mikesamuel upload_host = '%s.googlecode.com' % project_name 90defa7309cd03d650186ea486ec45207696428ff8mikesamuel upload_uri = '/files' 91defa7309cd03d650186ea486ec45207696428ff8mikesamuel auth_token = base64.b64encode('%s:%s'% (user_name, password)) 92defa7309cd03d650186ea486ec45207696428ff8mikesamuel headers = { 93defa7309cd03d650186ea486ec45207696428ff8mikesamuel 'Authorization': 'Basic %s' % auth_token, 94defa7309cd03d650186ea486ec45207696428ff8mikesamuel 'User-Agent': 'Googlecode.com uploader v0.9.4', 95defa7309cd03d650186ea486ec45207696428ff8mikesamuel 'Content-Type': content_type, 96defa7309cd03d650186ea486ec45207696428ff8mikesamuel } 97defa7309cd03d650186ea486ec45207696428ff8mikesamuel 98defa7309cd03d650186ea486ec45207696428ff8mikesamuel server = httplib.HTTPSConnection(upload_host) 99defa7309cd03d650186ea486ec45207696428ff8mikesamuel server.request('POST', upload_uri, body, headers) 100defa7309cd03d650186ea486ec45207696428ff8mikesamuel resp = server.getresponse() 101defa7309cd03d650186ea486ec45207696428ff8mikesamuel server.close() 102defa7309cd03d650186ea486ec45207696428ff8mikesamuel 103defa7309cd03d650186ea486ec45207696428ff8mikesamuel if resp.status == 201: 104defa7309cd03d650186ea486ec45207696428ff8mikesamuel location = resp.getheader('Location', None) 105defa7309cd03d650186ea486ec45207696428ff8mikesamuel else: 106defa7309cd03d650186ea486ec45207696428ff8mikesamuel location = None 107defa7309cd03d650186ea486ec45207696428ff8mikesamuel return resp.status, resp.reason, location 108defa7309cd03d650186ea486ec45207696428ff8mikesamuel 109defa7309cd03d650186ea486ec45207696428ff8mikesamuel 110defa7309cd03d650186ea486ec45207696428ff8mikesamueldef encode_upload_request(fields, file_path): 111defa7309cd03d650186ea486ec45207696428ff8mikesamuel """Encode the given fields and file into a multipart form body. 112defa7309cd03d650186ea486ec45207696428ff8mikesamuel 113defa7309cd03d650186ea486ec45207696428ff8mikesamuel fields is a sequence of (name, value) pairs. file is the path of 114defa7309cd03d650186ea486ec45207696428ff8mikesamuel the file to upload. The file will be uploaded to Google Code with 115defa7309cd03d650186ea486ec45207696428ff8mikesamuel the same file name. 116defa7309cd03d650186ea486ec45207696428ff8mikesamuel 117defa7309cd03d650186ea486ec45207696428ff8mikesamuel Returns: (content_type, body) ready for httplib.HTTP instance 118defa7309cd03d650186ea486ec45207696428ff8mikesamuel """ 119defa7309cd03d650186ea486ec45207696428ff8mikesamuel BOUNDARY = '----------Googlecode_boundary_reindeer_flotilla' 120defa7309cd03d650186ea486ec45207696428ff8mikesamuel CRLF = '\r\n' 121defa7309cd03d650186ea486ec45207696428ff8mikesamuel 122defa7309cd03d650186ea486ec45207696428ff8mikesamuel body = [] 123defa7309cd03d650186ea486ec45207696428ff8mikesamuel 124defa7309cd03d650186ea486ec45207696428ff8mikesamuel # Add the metadata about the upload first 125defa7309cd03d650186ea486ec45207696428ff8mikesamuel for key, value in fields: 126defa7309cd03d650186ea486ec45207696428ff8mikesamuel body.extend( 127defa7309cd03d650186ea486ec45207696428ff8mikesamuel ['--' + BOUNDARY, 128defa7309cd03d650186ea486ec45207696428ff8mikesamuel 'Content-Disposition: form-data; name="%s"' % key, 129defa7309cd03d650186ea486ec45207696428ff8mikesamuel '', 130defa7309cd03d650186ea486ec45207696428ff8mikesamuel value, 131defa7309cd03d650186ea486ec45207696428ff8mikesamuel ]) 132defa7309cd03d650186ea486ec45207696428ff8mikesamuel 133defa7309cd03d650186ea486ec45207696428ff8mikesamuel # Now add the file itself 134defa7309cd03d650186ea486ec45207696428ff8mikesamuel file_name = os.path.basename(file_path) 135defa7309cd03d650186ea486ec45207696428ff8mikesamuel f = open(file_path, 'rb') 136defa7309cd03d650186ea486ec45207696428ff8mikesamuel file_content = f.read() 137defa7309cd03d650186ea486ec45207696428ff8mikesamuel f.close() 138defa7309cd03d650186ea486ec45207696428ff8mikesamuel 139defa7309cd03d650186ea486ec45207696428ff8mikesamuel body.extend( 140defa7309cd03d650186ea486ec45207696428ff8mikesamuel ['--' + BOUNDARY, 141defa7309cd03d650186ea486ec45207696428ff8mikesamuel 'Content-Disposition: form-data; name="filename"; filename="%s"' 142defa7309cd03d650186ea486ec45207696428ff8mikesamuel % file_name, 143defa7309cd03d650186ea486ec45207696428ff8mikesamuel # The upload server determines the mime-type, no need to set it. 144defa7309cd03d650186ea486ec45207696428ff8mikesamuel 'Content-Type: application/octet-stream', 145defa7309cd03d650186ea486ec45207696428ff8mikesamuel '', 146defa7309cd03d650186ea486ec45207696428ff8mikesamuel file_content, 147defa7309cd03d650186ea486ec45207696428ff8mikesamuel ]) 148defa7309cd03d650186ea486ec45207696428ff8mikesamuel 149defa7309cd03d650186ea486ec45207696428ff8mikesamuel # Finalize the form body 150defa7309cd03d650186ea486ec45207696428ff8mikesamuel body.extend(['--' + BOUNDARY + '--', '']) 151defa7309cd03d650186ea486ec45207696428ff8mikesamuel 152defa7309cd03d650186ea486ec45207696428ff8mikesamuel return 'multipart/form-data; boundary=%s' % BOUNDARY, CRLF.join(body) 153defa7309cd03d650186ea486ec45207696428ff8mikesamuel 154defa7309cd03d650186ea486ec45207696428ff8mikesamuel 155defa7309cd03d650186ea486ec45207696428ff8mikesamueldef upload_find_auth(file_path, project_name, summary, labels=None, 156defa7309cd03d650186ea486ec45207696428ff8mikesamuel user_name=None, password=None, tries=3): 157defa7309cd03d650186ea486ec45207696428ff8mikesamuel """Find credentials and upload a file to a Google Code project's file server. 158defa7309cd03d650186ea486ec45207696428ff8mikesamuel 159defa7309cd03d650186ea486ec45207696428ff8mikesamuel file_path, project_name, summary, and labels are passed as-is to upload. 160defa7309cd03d650186ea486ec45207696428ff8mikesamuel 161defa7309cd03d650186ea486ec45207696428ff8mikesamuel Args: 162defa7309cd03d650186ea486ec45207696428ff8mikesamuel file_path: The local path to the file. 163defa7309cd03d650186ea486ec45207696428ff8mikesamuel project_name: The name of your project on Google Code. 164defa7309cd03d650186ea486ec45207696428ff8mikesamuel summary: A small description for the file. 165defa7309cd03d650186ea486ec45207696428ff8mikesamuel labels: an optional list of label strings with which to tag the file. 166defa7309cd03d650186ea486ec45207696428ff8mikesamuel config_dir: Path to Subversion configuration directory, 'none', or None. 167defa7309cd03d650186ea486ec45207696428ff8mikesamuel user_name: Your Google account name. 168defa7309cd03d650186ea486ec45207696428ff8mikesamuel tries: How many attempts to make. 169defa7309cd03d650186ea486ec45207696428ff8mikesamuel """ 170defa7309cd03d650186ea486ec45207696428ff8mikesamuel if user_name is None or password is None: 171defa7309cd03d650186ea486ec45207696428ff8mikesamuel from netrc import netrc 172defa7309cd03d650186ea486ec45207696428ff8mikesamuel authenticators = netrc().authenticators("code.google.com") 173defa7309cd03d650186ea486ec45207696428ff8mikesamuel if authenticators: 174defa7309cd03d650186ea486ec45207696428ff8mikesamuel if user_name is None: 175defa7309cd03d650186ea486ec45207696428ff8mikesamuel user_name = authenticators[0] 176defa7309cd03d650186ea486ec45207696428ff8mikesamuel if password is None: 177defa7309cd03d650186ea486ec45207696428ff8mikesamuel password = authenticators[2] 178defa7309cd03d650186ea486ec45207696428ff8mikesamuel 179defa7309cd03d650186ea486ec45207696428ff8mikesamuel while tries > 0: 180defa7309cd03d650186ea486ec45207696428ff8mikesamuel if user_name is None: 181defa7309cd03d650186ea486ec45207696428ff8mikesamuel # Read username if not specified or loaded from svn config, or on 182defa7309cd03d650186ea486ec45207696428ff8mikesamuel # subsequent tries. 183defa7309cd03d650186ea486ec45207696428ff8mikesamuel sys.stdout.write('Please enter your googlecode.com username: ') 184defa7309cd03d650186ea486ec45207696428ff8mikesamuel sys.stdout.flush() 185defa7309cd03d650186ea486ec45207696428ff8mikesamuel user_name = sys.stdin.readline().rstrip() 186defa7309cd03d650186ea486ec45207696428ff8mikesamuel if password is None: 187defa7309cd03d650186ea486ec45207696428ff8mikesamuel # Read password if not loaded from svn config, or on subsequent tries. 188defa7309cd03d650186ea486ec45207696428ff8mikesamuel print 'Please enter your googlecode.com password.' 189defa7309cd03d650186ea486ec45207696428ff8mikesamuel print '** Note that this is NOT your Gmail account password! **' 190defa7309cd03d650186ea486ec45207696428ff8mikesamuel print 'It is the password you use to access Subversion repositories,' 191defa7309cd03d650186ea486ec45207696428ff8mikesamuel print 'and can be found here: http://code.google.com/hosting/settings' 192defa7309cd03d650186ea486ec45207696428ff8mikesamuel password = getpass.getpass() 193defa7309cd03d650186ea486ec45207696428ff8mikesamuel 194defa7309cd03d650186ea486ec45207696428ff8mikesamuel status, reason, url = upload(file_path, project_name, user_name, password, 195defa7309cd03d650186ea486ec45207696428ff8mikesamuel summary, labels) 196defa7309cd03d650186ea486ec45207696428ff8mikesamuel # Returns 403 Forbidden instead of 401 Unauthorized for bad 197defa7309cd03d650186ea486ec45207696428ff8mikesamuel # credentials as of 2007-07-17. 198defa7309cd03d650186ea486ec45207696428ff8mikesamuel if status in [httplib.FORBIDDEN, httplib.UNAUTHORIZED]: 199defa7309cd03d650186ea486ec45207696428ff8mikesamuel # Rest for another try. 200defa7309cd03d650186ea486ec45207696428ff8mikesamuel user_name = password = None 201defa7309cd03d650186ea486ec45207696428ff8mikesamuel tries = tries - 1 202defa7309cd03d650186ea486ec45207696428ff8mikesamuel else: 203defa7309cd03d650186ea486ec45207696428ff8mikesamuel # We're done. 204defa7309cd03d650186ea486ec45207696428ff8mikesamuel break 205defa7309cd03d650186ea486ec45207696428ff8mikesamuel 206defa7309cd03d650186ea486ec45207696428ff8mikesamuel return status, reason, url 207defa7309cd03d650186ea486ec45207696428ff8mikesamuel 208defa7309cd03d650186ea486ec45207696428ff8mikesamuel 209defa7309cd03d650186ea486ec45207696428ff8mikesamueldef main(): 210defa7309cd03d650186ea486ec45207696428ff8mikesamuel parser = optparse.OptionParser(usage='googlecode-upload.py -s SUMMARY ' 211defa7309cd03d650186ea486ec45207696428ff8mikesamuel '-p PROJECT [options] FILE') 212defa7309cd03d650186ea486ec45207696428ff8mikesamuel parser.add_option('-s', '--summary', dest='summary', 213defa7309cd03d650186ea486ec45207696428ff8mikesamuel help='Short description of the file') 214defa7309cd03d650186ea486ec45207696428ff8mikesamuel parser.add_option('-p', '--project', dest='project', 215defa7309cd03d650186ea486ec45207696428ff8mikesamuel help='Google Code project name') 216defa7309cd03d650186ea486ec45207696428ff8mikesamuel parser.add_option('-u', '--user', dest='user', 217defa7309cd03d650186ea486ec45207696428ff8mikesamuel help='Your Google Code username') 218defa7309cd03d650186ea486ec45207696428ff8mikesamuel parser.add_option('-w', '--password', dest='password', 219defa7309cd03d650186ea486ec45207696428ff8mikesamuel help='Your Google Code password') 220defa7309cd03d650186ea486ec45207696428ff8mikesamuel parser.add_option('-l', '--labels', dest='labels', 221defa7309cd03d650186ea486ec45207696428ff8mikesamuel help='An optional list of comma-separated labels to attach ' 222defa7309cd03d650186ea486ec45207696428ff8mikesamuel 'to the file') 223defa7309cd03d650186ea486ec45207696428ff8mikesamuel 224defa7309cd03d650186ea486ec45207696428ff8mikesamuel options, args = parser.parse_args() 225defa7309cd03d650186ea486ec45207696428ff8mikesamuel 226defa7309cd03d650186ea486ec45207696428ff8mikesamuel if not options.summary: 227defa7309cd03d650186ea486ec45207696428ff8mikesamuel parser.error('File summary is missing.') 228defa7309cd03d650186ea486ec45207696428ff8mikesamuel elif not options.project: 229defa7309cd03d650186ea486ec45207696428ff8mikesamuel parser.error('Project name is missing.') 230defa7309cd03d650186ea486ec45207696428ff8mikesamuel elif len(args) < 1: 231defa7309cd03d650186ea486ec45207696428ff8mikesamuel parser.error('File to upload not provided.') 232defa7309cd03d650186ea486ec45207696428ff8mikesamuel elif len(args) > 1: 233defa7309cd03d650186ea486ec45207696428ff8mikesamuel parser.error('Only one file may be specified.') 234defa7309cd03d650186ea486ec45207696428ff8mikesamuel 235defa7309cd03d650186ea486ec45207696428ff8mikesamuel file_path = args[0] 236defa7309cd03d650186ea486ec45207696428ff8mikesamuel 237defa7309cd03d650186ea486ec45207696428ff8mikesamuel if options.labels: 238defa7309cd03d650186ea486ec45207696428ff8mikesamuel labels = options.labels.split(',') 239defa7309cd03d650186ea486ec45207696428ff8mikesamuel else: 240defa7309cd03d650186ea486ec45207696428ff8mikesamuel labels = None 241defa7309cd03d650186ea486ec45207696428ff8mikesamuel 242defa7309cd03d650186ea486ec45207696428ff8mikesamuel status, reason, url = upload_find_auth(file_path, options.project, 243defa7309cd03d650186ea486ec45207696428ff8mikesamuel options.summary, labels, 244defa7309cd03d650186ea486ec45207696428ff8mikesamuel options.user, options.password) 245defa7309cd03d650186ea486ec45207696428ff8mikesamuel if url: 246defa7309cd03d650186ea486ec45207696428ff8mikesamuel print 'The file was uploaded successfully.' 247defa7309cd03d650186ea486ec45207696428ff8mikesamuel print 'URL: %s' % url 248defa7309cd03d650186ea486ec45207696428ff8mikesamuel return 0 249defa7309cd03d650186ea486ec45207696428ff8mikesamuel else: 250defa7309cd03d650186ea486ec45207696428ff8mikesamuel print 'An error occurred. Your file was not uploaded.' 251defa7309cd03d650186ea486ec45207696428ff8mikesamuel print 'Google Code upload server said: %s (%s)' % (reason, status) 252defa7309cd03d650186ea486ec45207696428ff8mikesamuel return 1 253defa7309cd03d650186ea486ec45207696428ff8mikesamuel 254defa7309cd03d650186ea486ec45207696428ff8mikesamuel 255defa7309cd03d650186ea486ec45207696428ff8mikesamuelif __name__ == '__main__': 256defa7309cd03d650186ea486ec45207696428ff8mikesamuel sys.exit(main()) 257