#!/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()