OmniZ3D
2/3/2013 - 3:58 PM

Script for use with Pythonista to allow Github Gists for script storage and retrieval. Copy script in full into a new script in Pythonista c

Script for use with Pythonista to allow Github Gists for script storage and retrieval. Copy script in full into a new script in Pythonista called "gitcheck". Run the script and it will create 4 scripts starting with "Gist". These can be added to the action menu. See comments for more details.

# Source: https://gist.github.com/4702275
#
# All-purpose gist tool for Pythonista.
# 
# When run directly, this script sets up four other scripts that call various
# functions within this file. Each of these sub-scripts are meant for use as
# action menu items. They are:
# 
#   Set Gist ID.py   - Set the gist id that the current file should be 
#                      associated with.
#
#   Download Gist.py - Download the gist from the URL on the clipboard, and
#                      automatically set the association for it (if possible).
#
#   Commit Gist.py   - Commits the currently open file to the associated gist.
#
#   Pull Gist.py     - Replaces the current file with the latest version from
#                      the associated gist.
#   Create Gist.py   - create a new gist with the current file
#
#
# Download Gist.py can also be run as a bookmarklet from Mobile Safari (to
# jump from browsing a gist directly to downloading it in Pythonista) by
# creating a bookmark to:
#
#   javascript:(function()%7Bif(document.location.href.indexOf('http')===0)document.location.href='pythonista://Gist%20Download?action=run&argv='+document.location.href;%7D)();
#
#
# Credits:
#
# Further building on the previous work of Westacular
# https://gist.github.com/4145515
#
# This combines the gist pull/commit/set id scripts from
# https://gist.github.com/4043334
#
# with the bookmarklet-compatible, multi-gist download script
# https://gist.github.com/5f3f2035d8aa46de42ad
#
# (which in turn is based on https://gist.github.com/4036200,
# which is based on https://gist.github.com/b0644f5ed1d94bd32805)
 
import clipboard
import console
import editor
import json
import keychain
import os
import re
import requests
import shelve

api_url = 'https://api.github.com/gists/'
 
class GistDownloadError (Exception): pass
class InvalidGistIDError (Exception): pass
 
#Perform authorization
def auth(username, password):
	data='{"scopes":["gist"],"note":"gistcheck"}'
	r = requests.post(
	    api_url.replace('gist','authorizations'),
	    data=data,auth=(username,password),
	    headers={'content-type':'application/json'})
	return r.json
	
#get auth data
def get_token():
	token = keychain.get_password('gistcheck','gistcheck')
	if token is None:
		u, p = console.login_alert('GitHub Login')
		token = auth(u, p)['token']
		keychain.set_password('gistcheck','gistcheck',token)
	return token
	
def commit_or_create(gist, files, token, message=None):
    payload = {"files":{}}
    if message is not None: payload['description'] = message
    for f, c in files.items():
        payload['files'][os.path.basename(f)] = {"content":c}
    headers = {
        'Content-Type':'application/json',
        'Authorization': 'token %s' % token,
        }
    if gist is None:
        # Create a new gist
        r = requests.post(api_url[:-1],
            data=json.dumps(payload),headers=headers).json
    else:
        # Commit to existing gist
        r = requests.post(api_url + gist,
            data=json.dumps(payload),headers=headers).json    
    return r
    
def fork(gist,token):
	headers = {
        'Content-Type':'application/json',
        'Authorization': 'token %s' % token,
        }
	return requests.post(api_url + gist + '/forks',
                       headers=headers).json
	
def get_gist_id():
		db = shelve.open('gistcheck.db')
		gist_id = db.get(editor.get_path(),None)
		db.close()
		return gist_id
	
def set_gist_id(gist_id):
		gist_id = extract_gist_id(gist_id)
		db = shelve.open('gistcheck.db')
		db[editor.get_path()] = gist_id
		db.close()
		
def del_gist_id():
	db = shelve.open('gistcheck.db')
	fpath = editor.get_path()
	if fpath in db:
		del db[fpath]
	db.close()
			
def extract_gist_id(gist):
	if re.match(r'^([0-9a-f]+)$', gist): 
	    return gist
	m = re.match(r'^http(s?)://gist.github.com/([^/]+/)?([0-9a-f]*)', gist)
	if m: 
	    return m.group(3)
	m = re.match(r'^http(s?)://raw.github.com/gist/([0-9a-f]*)', gist)
	if m: 
	    return m.group(2) 
	raise InvalidGistIDError()
 
#load a file from a gist
def pull():
	gist_id = get_gist_id()
	if gist_id is None:
		console.alert('Error', 'There is no gist id set for this file')
	else:
		fname = os.path.basename(editor.get_path())
		gist_data = requests.get(api_url + gist_id).json
		try:
		    newtext = gist_data['files'][fname]['content']
		except:
		    console.alert('Pull Error', 'There was a problem with the pull',gist_data)
		if newtext is not None:
			editor.replace_text(0,len(editor.get_text()),newtext)
 
def commit():
	token = get_token()
	fpath = editor.get_path()
	fname = os.path.basename(fpath)
	m = console.input_alert('Edit Description','Enter a new description:','')
	if m == '': m = None
	gist_id = get_gist_id()
	res = commit_or_create(gist_id,{fpath:editor.get_text()},token,m)
	try:
		id = res['id']
	except KeyError:
		if gist_id:
			f = console.alert('Commit Failed',
		  	'Do you have permission to commit? Would you like to fork?','Fork')
			if f == 1:
				res = fork(gist_id,token)
				try:
					id = res['id']
				except KeyError:
					console.alert('Fork Error', 'There was a problem with the fork')
				else: 
					set_gist_id(id)
					res = commit_or_create(id,{fpath:editor.get_text()},token,m)
					try:
						id = res['id']
					except KeyError:
						console.alert('Commit Error',
						              'Commit still failed, maybe fork too')
	else:
		if gist_id is None:
			set_gist_id(id)

def set():
	gist = get_gist_id()
	if gist == None: gist = ''
	gist = console.input_alert('Assign Gist ID','Enter the gist id for this file',gist)
	if gist == '':
		del_gist_id()
	else:
		try:
			set_gist_id(gist)
		except InvalidGistIDError:
			console.alert('Invalid Gist ID', 'That does not appear to be a valid gist id')

def download_gist(gist_url):
	# Returns a 2-tuple of filename and content
    gist_id = extract_gist_id(gist_url)
    try:
        gist_info = requests.get(api_url + gist_id).json
        files = gist_info['files']
    except:
        raise GistDownloadError()
    for file_info in files.values():
        lang =  file_info.get('language', None)
        if lang != 'Python': 
            continue 
        yield file_info['filename'],gist_id,file_info['content']
    
def download(gist_url):
	try:
		for num, (filename, gist_id, content) in enumerate(download_gist(gist_url), start=1):
			if os.path.isfile(filename):
				i = console.alert('File exists', 'A file with the name ' + filename + 
								  ' already exists in your library.',
								  'Auto Rename', 'Skip')
				if i == 1:
					editor.make_new_file(filename, content)
			else:
				editor.make_new_file(filename, content)
				set_gist_id(gist_id)
			
	except InvalidGistIDError:
		console.alert('No Gist URL','Invalid Gist URL.','OK')
	except GistDownloadError:
		console.alert('Error', 'The Gist could not be downloaded.')
	if not num:
		console.alert('No Python Files', 'This Gist contains no Python files.')
		
def download_from_args(args):
	if len(args) == 2:
		url = args[1]
	else:
		url = clipboard.get()
	download(url)
	
def setup():
	script_map={
            'Gist Commit'  :'gistcheck.commit()',
            'Gist Pull'    :'gistcheck.pull()',
            'Gist Set ID'  :'gistcheck.set()',
            'Gist Download':'if __name__ == "__main__" : gistcheck.download_from_args( sys.argv )'
            }
	for s,c in script_map.items():
		with open(s+'.py','w') as f:
			f.writelines(['import gistcheck\n','%s\n'%c])

if __name__ == '__main__':
	setup()