snose

Artifact [2609a1b7ab]
Login

Artifact 2609a1b7abe39f610f08ac4c93fd1e0cfaf96f05bed784a2e5ff8c761b6d2d67:


#!/usr/bin/python
#snose - Simplenote Object Synchronisation (Explicit)

import sys
import json
import simplenote #Need to install this
import os.path, time
from optparse import OptionParser
import netrc
import re

def main():
    parser = OptionParser()
    parser.add_option("--snort", action="store_true", default=False, help="Import a new file to Simplenote")
    parser.add_option("--sniff", action="store", nargs=1, type="string", help="Link a file with an already existing note in Simplenote", metavar="<key>")
    parser.add_option("--sneeze", action="store", nargs=1, type="string", help="Export an existing file from Simplenote", metavar="<key>")
    parser.add_option("--blow", action="store", type="string", help="Roll back note key to previous version", metavar="<key>")
    parser.add_option("--sync", help="Sync files in index", default=False, action='store_true')
    parser.add_option("--hanky", help="Use with --sync to perform a dry run", default=False, action='store_true')
    parser.add_option("--snot", help="List notes available for export (tagged snose)", default=False, action='store_true')
    parser.add_option("--file", help="Filename for snort, sniff, sneeze", default=None, action='store')
    parser.add_option("--username", action="store", type="string", help="Your Simplenote email address")
    parser.add_option("--password", action="store", type="string", help="Your Simplenote password")
    (options, args) = parser.parse_args()

    if not options.username or not options.password:
        #Check to see if stored somewhere
        try:
            options.username = netrc.netrc().authenticators("simple-note.appspot.com")[0]
            options.password = netrc.netrc().authenticators("simple-note.appspot.com")[2]
        except IOError as e:
            print('Username and password must be supplied or exist .netrc file under domain of simple-note.appspot.com')
            sys.exit()
    snclient = simplenote.Simplenote(options.username, options.password)
    if options.snort:
        if options.file is None:
            print('--file required')
            sys.exit()
        snort(snclient, options.file)
    elif options.sniff:
        if options.file is None:
            print('--file required')
            sys.exit()
        sniff(snclient, options.sniff, options.file)
    elif options.sneeze:
        sneeze(snclient, options.sneeze, options.file)
    elif options.blow:
        blow(snclient, options.blow)
    elif options.snot:
        snot(snclient)
    elif options.sync and options.hanky:
        sync(snclient, True)
    elif options.sync:
        sync(snclient)
    else:
        print('No options supplied')


def snort(snclient, filename): 
    snose = load_or_new()
    #Add new file to Simplenote
    #Need to get file contents
    content = snread(filename)
    if content:
        try:
            returned = snclient.add_note({"content": content, "tags": ["snose"]})
            print("Imported %s into Simplenote with key %s" % (filename, returned[0]['key']))
        except IOError as e:
            print("Failed to add note to Simplenote")
            print(e)
        else:
            #Add mapping
            snose[filename] = snobject(returned[0], filename)
            write_index(snose, "But note was successfully imported to Simplenote with key %s. Try sniffing the file")


def sniff(snclient, key, filename): #How to ensure remote gets or has snose tag?
    # Add a new mapping only
    snose = load_or_new()
    #Get details about current Simplenote file
    remote = snremote(snclient, key)
    if remote:
        #Add mapping
        snose[filename] = snobject(remote[0], filename)
        write_index(snose)


def sneeze(snclient, key, filename):
    #place an existing note in current directory
    snose = load_or_new()
    #Get remote note
    remote = snremote(snclient, key)
    if remote:
        #Write file
        #If no filename supplied try to figure out from first three lines of file itself
        if filename is None:
            firstlines = remote[0]['content'].splitlines()[:3]
            for line in firstlines:
                try:
                    filename = re.search(r'file:(\S+)', line).group(1)
                except IndexError:
                    pass
                except AttributeError:
                    pass
            if filename is None:
                print("Failed to identify filename within note, please provide on command line")
                sys.exit()
        filename = os.path.expanduser(filename)
        ok = snwrite(filename, remote)
        if ok:
            #Update index
            snose[filename] = snobject(remote[0], filename)
            write_index(snose, "But note was created locally. Try sniffing the file to add it to the index.")


def blow(snclient, key):
    #With given key from .snose file, roll back to the previous version

    #1) Check exists in .snose index
    #2) Get previous version of remote
    #3) Write it out locally
    #4) Use that to update remote
    #5) Update index file with results

    #1) Check exists in .snose index
    try:
        snose = load_or_new()
        #Need to get filename of note, loop through, performance should be fine as .snose likely to be small
        sitems = snitems(snose)
        filename = [name for name, local in sitems if local['key'] == key][0]

        print("Attempting to rollback file %s" % filename)
    except IndexError as e:
        print("Note doesn't exist in local .snose index")
    else:
        #2) Get previous version of remote
        try:
            #fetch once to know version
            remote = snclient.get_note(key)
            rollback = snclient.get_note(key, remote[0]['version']-1)
        except IOError as e:
            print("Failed to fetch previous version")
        else:
            ok = snwrite(filename, rollback, msg="Failed to rollback local copy of that note")
            if ok:
                print("Rolled back local copy of that note")
                #Since rollback doesn't include full meta data, update remote accordingly
                try:
                    del remote[0]['version']
                except KeyError:
                    pass
                finally:
                    #Set modified date
                    sysmodifydate = float(os.path.getmtime(filename))
                    remote[0]['modifydate'] = sysmodifydate
                    remote[0]['content'] = rollback[0]['content']
                    #4) Use that to update remote
                    try:
                        returned = snclient.update_note(remote[0])
                        print("Rolled back remote version")
                    except IOError as e:
                        print("Failed to roll back remote version")
                    else:
                        #Get returned metadata
                        snose[filename]['version'] = returned[0]['version']
                        snose[filename]['modifydate'] = sysmodifydate
                        write_index(snose, "Try running a sync to get index corrected.")


def snot(snclient):
    #List simplenote notes tagged with "snose"
    notelist = snclient.get_note_list()
    #That gets list of keys. Then need to iterate through and get first line of text.
    #This is going to be slow.
    print("Key:                               \tNote")
    for note in notelist[0]:
        if ("snose" in note['tags']) & (int(note['deleted']) != 1):
            #get note itself
            remote = snclient.get_note(note['key'])
            print(remote[0]['key']  + " \t" + remote[0]['content'].splitlines()[0])


def sync(snclient, dry=False):
    #Need to read in mappings and sync those notes.
    dryremotes = []
    snose = load_or_new()
    #Need to iterate through list.
    sitems = snitems(snose)
    for name, local in sitems:
        #First of all check for local modifications
        sysmodifydate = float(os.path.getmtime(name))
        if sysmodifydate > float(local['modifydate']): #ensure full timestamp
            if not dry:
            #Update remote
                content = snread(name, "Skipping synchronisation for this note")
                if content:
                    try:
                        returned = snclient.update_note(snobject(local, name, tags=['snose'], content=content))
                        print("Updated remote version of %s" % name)
                    except IOError as e:
                        print("Failed to update remote verison of local note %s" % name)
                    else:
                        #Get returned metadata
                        snose[name]['version'] = returned[0]['version']
                        snose[name]['modifydate'] = sysmodifydate #Use local value to avoid differences in accuracy (decimal places. etc) between local and remote timestamps
                        #Update local file if merged content
                        if 'content' in returned[0]:
                            ok = snwrite(name, returned, msg="Failed to merge content locally for %s, therefore skipping updating the index for this note" % name)
                            if ok:
                                print("Merged local content for %s" % name)
                                #Override the returned value? As otherwise next sync will immediately update the remote version for no reason.
                                snose[name]['modifydate'] = os.path.getmtime(name) 
                        #Update the index file
                        write_index(snose, "But remote and local copy of the file itself have been updated.")
            elif dry:
                print("Updated remote version of %s" % name)
                #For dry run, collect list of "updated remotes" to ignore in local updates
                dryremotes.append(name)
        #Fetch details from Simplenote
        remote = snremote(snclient, local['key'], "Skipping synchronisation for this file")
        if remote:
            if remote[0]['version'] > local['version']:
                if not dry:
                    ok = snwrite(name, remote, msg="Failed to update local note %s with remote content. Will not update the .snose index file for this file" % name)
                    if ok:
                        print("Updated local version of %s" % name)
                        #Also update .snose index
                        snose[name]['version'] = remote[0]['version']
                        snose[name]['modifydate'] = os.path.getmtime(name) #As if set remote modify date, local file will immediately appear 'modified'
                        write_index(snose, "But local copy of the file %s has been updated with remote changes" % name)
                elif (dry and (not (name in dryremotes))):
                    print("Updated local version of %s" % name)

def load_or_new():
    try:
        with open('.snose', 'r') as f:
            snose = json.load(f)
    except IOError as e:
        #Doesn't exist so create new
        snose = {}
    return snose


def write_index(snose, msg=None):
    try:
        with open('.snose', 'w') as f:
            json.dump(snose, f, indent=2)
    except IOError as e:
        print("Failed to update index")
        if msg:
            print(msg)


def snobject(returned, filename, tags=None, content=None):
    sno = {'key': returned['key'], 'version': returned['version'], 'modifydate': float(os.path.getmtime(filename)) }
    if tags:
        sno['tags'] = tags
    if content:
        sno['content'] = content
    return sno


def snitems(snose):
    if sys.version_info < (3, 0):
        sitems = snose.iteritems()
    else:
        sitems = snose.items()
    return sitems


def snwrite(name, remote, msg=None):
    try:
        if sys.version_info < (3, 0):
            with open(name, 'w') as f:
                f.write(remote[0]['content'].encode("utf-8"))
        else:
            with open(name, 'w', encoding="utf-8") as f:
                f.write(remote[0]['content'])
    except IOError as e:
        if msg:
            print(msg)
        print("Failed to create local copy of that note")
        print(e)
    else:
        return True


def snread(filename, msg=None):
    try:
        with open(filename, 'r') as f:
            content = f.read()
    except IOError as e:
        print("Failed to read file %s" % filename)
        if msg:
            print(msg)
    else:
        return content


def snremote(snclient, key, msg=None):
    try:
        remote = snclient.get_note(key)
        #What if can't be found, need to abort...
    except IOError as e:
        print("Failed to find that note on Simplenote")
        print(e)
        if msg:
            print(msg)
    else:
        return remote


main()