nncli

NextCloud Notes Command Line Interface
git clone git://git.danielmoch.com/nncli.git
Log | Files | Refs | LICENSE

commit 7c31503b0177d8f4473483f8d472fbd28dc76b3f
parent 2b9a20dffeddbb02298838ca46b3da774a6a806d
Author: Samuel Walladge <swalladge@users.noreply.github.com>
Date:   Fri, 27 May 2016 10:12:16 +0930

Merge pull request #22 from swalladge/master

add alternate host config option, support for custom config file, upgrade to python3, fix unicode handling problems, refactor to use requests library
Diffstat:
MREADME.md | 5+++--
Msetup.py | 4++--
Msimplenote_cli/config.py | 13+++++++++----
Msimplenote_cli/notes_db.py | 31++++++++++++++++---------------
Msimplenote_cli/simplenote.py | 172++++++++++++++++++++++++++++++++++++++-----------------------------------------
Msimplenote_cli/sncli.py | 184+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Msimplenote_cli/temp.py | 4++--
Msimplenote_cli/utils.py | 39++++++++++++++++-----------------------
Msimplenote_cli/view_help.py | 38+++++++++++++++++++-------------------
Msimplenote_cli/view_log.py | 6+++---
Msimplenote_cli/view_note.py | 44++++++++++++++++++++++----------------------
Msimplenote_cli/view_titles.py | 14+++++++-------
Msncli | 2+-
13 files changed, 278 insertions(+), 278 deletions(-)

diff --git a/README.md b/README.md @@ -18,8 +18,9 @@ Check your OS distribution for installation packages. ### Requirements -* [Python 2](http://python.org) -* [Urwid](http://urwid.org) Python 2 module +* [Python 3](http://python.org) +* [Urwid](http://urwid.org) Python 3 module +* [Requests](https://requests.readthedocs.org/en/master/) Python 3 module * A love for the command line! ### Features diff --git a/setup.py b/setup.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 # Copyright (c) 2014 Eric Davis # Licensed under the MIT License @@ -13,7 +13,7 @@ author=simplenote_cli.__author__, author_email=simplenote_cli.__author_email__, url=simplenote_cli.__url__, - requires=[ 'urwid' ], + requires=[ 'urwid', 'requests' ], packages=[ 'simplenote_cli' ], scripts=[ 'sncli' ] ) diff --git a/simplenote_cli/config.py b/simplenote_cli/config.py @@ -2,11 +2,11 @@ # Copyright (c) 2014 Eric Davis # Licensed under the MIT License -import os, urwid, collections, ConfigParser +import os, urwid, collections, configparser class Config: - def __init__(self): + def __init__(self, custom_file=None): self.home = os.path.abspath(os.path.expanduser('~')) defaults = \ { @@ -26,6 +26,7 @@ def __init__(self): 'cfg_max_logs' : '5', 'cfg_log_timeout' : '5', 'cfg_log_reversed' : 'yes', + 'cfg_sn_host' : 'simple-note.appspot.com', 'kb_help' : 'h', 'kb_quit' : 'q', @@ -118,8 +119,11 @@ def __init__(self): 'clr_help_descr_bg' : 'default' } - cp = ConfigParser.SafeConfigParser(defaults) - self.configs_read = cp.read([os.path.join(self.home, '.snclirc')]) + cp = configparser.SafeConfigParser(defaults) + if custom_file is not None: + self.configs_read = cp.read([custom_file]) + else: + self.configs_read = cp.read([os.path.join(self.home, '.snclirc')]) cfg_sec = 'sncli' @@ -131,6 +135,7 @@ def __init__(self): self.configs = collections.OrderedDict() self.configs['sn_username'] = [ cp.get(cfg_sec, 'cfg_sn_username', raw=True), 'Simplenote Username' ] self.configs['sn_password'] = [ cp.get(cfg_sec, 'cfg_sn_password', raw=True), 'Simplenote Password' ] + self.configs['sn_host'] = [ cp.get(cfg_sec, 'cfg_sn_host', raw=True), 'Simplenote server hostname' ] self.configs['db_path'] = [ cp.get(cfg_sec, 'cfg_db_path'), 'Note storage path' ] self.configs['search_tags'] = [ cp.get(cfg_sec, 'cfg_search_tags'), 'Search tags as well' ] self.configs['sort_mode'] = [ cp.get(cfg_sec, 'cfg_sort_mode'), 'Sort mode' ] diff --git a/simplenote_cli/notes_db.py b/simplenote_cli/notes_db.py @@ -7,10 +7,10 @@ # new BSD license import os, time, re, glob, json, copy, threading -import utils -import simplenote +from . import utils +from . import simplenote simplenote.NOTE_FETCH_LENGTH=100 -from simplenote import Simplenote +from .simplenote import Simplenote class ReadError(RuntimeError): pass @@ -42,10 +42,10 @@ def __init__(self, config, log, update_view): for fn in fnlist: try: - n = json.load(open(fn, 'rb')) - except IOError, e: + n = json.load(open(fn, 'r')) + except IOError as e: raise ReadError ('Error opening {0}: {1}'.format(fn, str(e))) - except ValueError, e: + except ValueError as e: raise ReadError ('Error reading {0}: {1}'.format(fn, str(e))) else: # we always have a localkey, also when we don't have a note['key'] yet (no sync) @@ -59,7 +59,8 @@ def __init__(self, config, log, update_view): # initialise the simplenote instance we're going to use # this does not yet need network access self.simplenote = Simplenote(self.config.get_config('sn_username'), - self.config.get_config('sn_password')) + self.config.get_config('sn_password'), + self.config.get_config('sn_host')) # we'll use this to store which notes are currently being synced by # the background thread, so we don't add them anew if they're still @@ -69,12 +70,12 @@ def __init__(self, config, log, update_view): def filtered_notes_sort(self, filtered_notes, sort_mode='date'): if sort_mode == 'date': if self.config.get_config('pinned_ontop') == 'yes': - filtered_notes.sort(utils.sort_by_modify_date_pinned, reverse=True) + filtered_notes.sort(key=utils.sort_by_modify_date_pinned, reverse=True) else: filtered_notes.sort(key=lambda o: -float(o.note.get('modifydate', 0))) else: if self.config.get_config('pinned_ontop') == 'yes': - filtered_notes.sort(utils.sort_by_title_pinned) + filtered_notes.sort(key=utils.sort_by_title_pinned) else: filtered_notes.sort(key=lambda o: utils.get_note_title(o.note)) @@ -370,7 +371,7 @@ def helper_key_to_fname(self, k): def helper_save_note(self, k, note): # Save a single note to disc. fn = self.helper_key_to_fname(k) - json.dump(note, open(fn, 'wb'), indent=2) + json.dump(note, open(fn, 'w'), indent=2) # record that we saved this to disc. note['savedate'] = time.time() @@ -540,21 +541,21 @@ def sync_notes(self, server_sync=True, full_sync=True): # PERMANENT DELETE, remove note from local store # Only do this when a full sync (i.e. entire index) is performed! if server_sync and full_sync and not skip_remote_syncing: - for local_key in self.notes.keys(): + for local_key in list(self.notes.keys()): if local_key not in server_keys: del self.notes[local_key] local_deletes[local_key] = True # sync done, now write changes to db_path - for k in local_updates.keys(): + for k in list(local_updates.keys()): try: self.helper_save_note(k, self.notes[k]) - except WriteError, e: + except WriteError as e: raise WriteError (str(e)) self.log("Saved note to disk (key={0})".format(k)) - for k in local_deletes.keys(): + for k in list(local_deletes.keys()): fn = self.helper_key_to_fname(k) if os.path.exists(fn): os.unlink(fn) @@ -596,7 +597,7 @@ def get_note_status(self, key): def verify_all_saved(self): all_saved = True self.sync_lock.acquire() - for k in self.notes.keys(): + for k in list(self.notes.keys()): o = self.get_note_status(k) if not o.saved: all_saved = False diff --git a/simplenote_cli/simplenote.py b/simplenote_cli/simplenote.py @@ -13,13 +13,13 @@ :license: MIT, see LICENSE for more details. """ -import urllib -import urllib2 -from urllib2 import HTTPError +import urllib.parse +from urllib.error import HTTPError import base64 import time import datetime import logging +import requests try: import json @@ -30,9 +30,6 @@ # For Google AppEngine from django.utils import simplejson as json -AUTH_URL = 'https://simple-note.appspot.com/api/login' -DATA_URL = 'https://simple-note.appspot.com/api2/data' -INDX_URL = 'https://simple-note.appspot.com/api2/index?' NOTE_FETCH_LENGTH = 100 class SimplenoteLoginFailed(Exception): @@ -41,10 +38,13 @@ class SimplenoteLoginFailed(Exception): class Simplenote(object): """ Class for interacting with the simplenote web service """ - def __init__(self, username, password): + def __init__(self, username, password, host): """ object constructor """ - self.username = urllib2.quote(username) - self.password = urllib2.quote(password) + self.username = urllib.parse.quote(username) + self.password = urllib.parse.quote(password) + self.AUTH_URL = 'https://{0}/api/login'.format(host) + self.DATA_URL = 'https://{0}/api2/data'.format(host) + self.INDX_URL = 'https://{0}/api2/index?'.format(host) self.token = None def authenticate(self, user, password): @@ -59,15 +59,18 @@ def authenticate(self, user, password): """ auth_params = "email=%s&password=%s" % (user, password) - values = base64.encodestring(auth_params) - request = Request(AUTH_URL, values) + values = base64.encodestring(auth_params.encode()) try: - res = urllib2.urlopen(request).read() - token = urllib2.quote(res) - except HTTPError: + res = requests.post(self.AUTH_URL, data=values) + token = res.text + if res.status_code != 200: + raise SimplenoteLoginFailed( + 'Login to Simplenote API failed! statuscode: {}'.format(res.status_code)) + except HTTPError as e: raise SimplenoteLoginFailed('Login to Simplenote API failed!') except IOError: # no connection exception token = None + return token def get_token(self): @@ -104,23 +107,25 @@ def get_note(self, noteid, version=None): if version is not None: params_version = '/' + str(version) - params = '/%s%s?auth=%s&email=%s' % (str(noteid), params_version, self.get_token(), self.username) - #logging.debug('REQUEST: ' + DATA_URL+params) - request = Request(DATA_URL+params) + params = {'auth': self.get_token(), + 'email': self.username } + url = '{}/{}{}'.format(self.DATA_URL, str(noteid), params_version) + #logging.debug('REQUEST: ' + self.DATA_URL+params) try: - response = urllib2.urlopen(request) - except HTTPError, e: + res = requests.get(url, params=params) + except HTTPError as e: #logging.debug('RESPONSE ERROR: ' + str(e)) return e, -1 - except IOError, e: + except IOError as e: #logging.debug('RESPONSE ERROR: ' + str(e)) return e, -1 - note = json.loads(response.read()) - # use UTF-8 encoding - note["content"] = note["content"].encode('utf-8') - # For early versions of notes, tags not always available - if note.has_key("tags"): - note["tags"] = [t.encode('utf-8') for t in note["tags"]] + note = res.json() + + # # use UTF-8 encoding + # note["content"] = note["content"].encode('utf-8') + # # For early versions of notes, tags not always available + # if "tags" in note: + # note["tags"] = [t.encode('utf-8') for t in note["tags"]] #logging.debug('RESPONSE OK: ' + str(note)) return note, 0 @@ -138,43 +143,42 @@ def update_note(self, note): - status (int): 0 on sucesss and -1 otherwise """ - # use UTF-8 encoding - # cpbotha: in both cases check if it's not unicode already - # otherwise you get "TypeError: decoding Unicode is not supported" - if note.has_key("content"): - if isinstance(note["content"], str): - note["content"] = unicode(note["content"], 'utf-8') - - if note.has_key("tags"): - # if a tag is a string, unicode it, otherwise pass it through - # unchanged (it's unicode already) - # using the ternary operator, because I like it: a if test else b - note["tags"] = [unicode(t, 'utf-8') if isinstance(t, str) else t for t in note["tags"]] + # # use UTF-8 encoding + # # cpbotha: in both cases check if it's not unicode already + # # otherwise you get "TypeError: decoding Unicode is not supported" + # if "content" in note: + # if isinstance(note["content"], str): + # note["content"] = str(note["content"], 'utf-8') + # + # if "tags" in note: + # # if a tag is a string, unicode it, otherwise pass it through + # # unchanged (it's unicode already) + # # using the ternary operator, because I like it: a if test else b + # note["tags"] = [str(t, 'utf-8') if isinstance(t, str) else t for t in note["tags"]] # determine whether to create a new note or updated an existing one + params = {'auth': self.get_token(), + 'email': self.username} if "key" in note: # set modification timestamp if not set by client if 'modifydate' not in note: note["modifydate"] = time.time() - url = '%s/%s?auth=%s&email=%s' % (DATA_URL, note["key"], - self.get_token(), self.username) + url = '%s/%s' % (self.DATA_URL, note["key"]) else: - url = '%s?auth=%s&email=%s' % (DATA_URL, self.get_token(), self.username) + url = self.DATA_URL #logging.debug('REQUEST: ' + url + ' - ' + str(note)) - request = Request(url, urllib.quote(json.dumps(note))) - response = "" try: - response = urllib2.urlopen(request) - except IOError, e: + res = requests.post(url, data=json.dumps(note), params=params) + except IOError as e: #logging.debug('RESPONSE ERROR: ' + str(e)) return e, -1 - note = json.loads(response.read()) - if note.has_key("content"): - # use UTF-8 encoding - note["content"] = note["content"].encode('utf-8') - if note.has_key("tags"): - note["tags"] = [t.encode('utf-8') for t in note["tags"]] + note = res.json() + # if "content" in note: + # # use UTF-8 encoding + # note["content"] = note["content"].encode('utf-8') + # if "tags" in note: + # note["tags"] = [t.encode('utf-8') for t in note["tags"]] #logging.debug('RESPONSE OK: ' + str(note)) return note, 0 @@ -232,35 +236,38 @@ def get_note_list(self, since=None, tags=[]): notes = { "data" : [] } # get the note index - params = 'auth=%s&email=%s&length=%s' % (self.get_token(), self.username, - NOTE_FETCH_LENGTH) + params = {'auth': self.get_token(), + 'email': self.username, + 'length': NOTE_FETCH_LENGTH + } if since is not None: - params += '&since=%s' % since + params['since'] = since # perform initial HTTP request try: - #logging.debug('REQUEST: ' + INDX_URL+params) - request = Request(INDX_URL+params) - response = json.loads(urllib2.urlopen(request).read()) - #logging.debug('RESPONSE OK: ' + str(response)) - notes["data"].extend(response["data"]) + #logging.debug('REQUEST: ' + self.INDX_URL+params) + res = requests.get(self.INDX_URL, params=params) + #logging.debug('RESPONSE OK: ' + str(res)) + notes["data"].extend(res.json()["data"]) except IOError: status = -1 # get additional notes if bookmark was set in response while "mark" in response: - vals = (self.get_token(), self.username, response["mark"], NOTE_FETCH_LENGTH) - params = 'auth=%s&email=%s&mark=%s&length=%s' % vals + params = {'auth': self.get_token(), + 'email': self.username, + 'mark': response['mark'], + 'length': NOTE_FETCH_LENGTH + } if since is not None: - params += '&since=%s' % since + params['since'] = since # perform the actual HTTP request try: - #logging.debug('REQUEST: ' + INDX_URL+params) - request = Request(INDX_URL+params) - response = json.loads(urllib2.urlopen(request).read()) + #logging.debug('REQUEST: ' + self.INDX_URL+params) + res = requests.get(self.INDX_URL, params=params) #logging.debug('RESPONSE OK: ' + str(response)) - notes["data"].extend(response["data"]) + notes["data"].extend(res.json()["data"]) except IOError: status = -1 @@ -314,30 +321,17 @@ def delete_note(self, note_id): if (status == -1): return note, status - params = '/%s?auth=%s&email=%s' % (str(note_id), self.get_token(), - self.username) - #logging.debug('REQUEST DELETE: ' + DATA_URL+params) - request = Request(url=DATA_URL+params, method='DELETE') + params = {'auth': self.get_token(), + 'email': self.username } + url = '{}/{}'.format(self.DATA_URL, str(note_id)) + + #logging.debug('REQUEST DELETE: ' + self.DATA_URL+params) + request = Request(url=self.DATA_URL+params, method='DELETE') try: - urllib2.urlopen(request) - except IOError, e: + res = requests.delete(url, params=params) + # TODO: check error handling stuff - probably use res.status_code to check if was able to delete, etc. + # (same must be done for other note actions) + except IOError as e: return e, -1 return {}, 0 - -class Request(urllib2.Request): - """ monkey patched version of urllib2's Request to support HTTP DELETE - Taken from http://python-requests.org, thanks @kennethreitz - """ - - def __init__(self, url, data=None, headers={}, origin_req_host=None, - unverifiable=False, method=None): - urllib2.Request.__init__(self, url, data, headers, origin_req_host, unverifiable) - self.method = method - - def get_method(self): - if self.method: - return self.method - - return urllib2.Request.get_method(self) - diff --git a/simplenote_cli/sncli.py b/simplenote_cli/sncli.py @@ -2,20 +2,20 @@ # Copyright (c) 2014 Eric Davis # Licensed under the MIT License -import os, sys, getopt, re, signal, time, datetime, shlex, md5 -import subprocess, thread, threading, logging +import os, sys, getopt, re, signal, time, datetime, shlex, hashlib +import subprocess, threading, logging import copy, json, urwid, datetime -import view_titles, view_note, view_help, view_log, user_input -import utils, temp -from config import Config -from simplenote import Simplenote -from notes_db import NotesDB, ReadError, WriteError +from . import view_titles, view_note, view_help, view_log, user_input +from . import utils, temp +from .config import Config +from .simplenote import Simplenote +from .notes_db import NotesDB, ReadError, WriteError from logging.handlers import RotatingFileHandler class sncli: - def __init__(self, do_server_sync, verbose=False): - self.config = Config() + def __init__(self, do_server_sync, verbose=False, config_file=None): + self.config = Config(config_file) self.do_server_sync = do_server_sync self.verbose = verbose self.do_gui = False @@ -41,7 +41,7 @@ def __init__(self, do_server_sync, verbose=False): try: self.ndb = NotesDB(self.config, self.log, self.gui_update_view) - except Exception, e: + except Exception as e: self.log(str(e)) sys.exit(1) @@ -51,7 +51,7 @@ def __init__(self, do_server_sync, verbose=False): # with hundreds of notes will cause a recursion panic under # urwid. This simple workaround gets the job done. :-) self.verbose = True - self.log(u'sncli database doesn\'t exist, forcing full sync...') + self.log('sncli database doesn\'t exist, forcing full sync...') self.sync_notes() self.verbose = verbose @@ -60,26 +60,26 @@ def sync_notes(self): def get_editor(self): editor = self.config.get_config('editor') - if os.environ.has_key('EDITOR'): + if 'EDITOR' in os.environ: editor = os.environ['EDITOR'] if not editor: - self.log(u'No editor configured!') + self.log('No editor configured!') return None return editor def get_pager(self): pager = self.config.get_config('pager') - if os.environ.has_key('PAGER'): + if 'PAGER' in os.environ: pager = os.environ['PAGER'] if not pager: - self.log(u'No pager configured!') + self.log('No pager configured!') return None return pager def get_diff(self): diff = self.config.get_config('diff') if not diff: - self.log(u'No diff command configured!') + self.log('No diff command configured!') return None return diff @@ -93,16 +93,16 @@ def exec_cmd_on_note(self, note, cmd=None, raw=False): tf = temp.tempfile_create(note if note else None, raw=raw) try: - subprocess.check_call(cmd + u' ' + temp.tempfile_name(tf), shell=True) - except Exception, e: - self.log(u'Command error: ' + str(e)) + subprocess.check_call(cmd + ' ' + temp.tempfile_name(tf), shell=True) + except Exception as e: + self.log('Command error: ' + str(e)) temp.tempfile_delete(tf) return None content = None if not raw: content = ''.join(temp.tempfile_content(tf)) - if not content or content == u'\n': + if not content or content == '\n': content = None temp.tempfile_delete(tf) @@ -123,16 +123,16 @@ def exec_diff_on_note(self, note, old_note): out = temp.tempfile_create(None) try: - subprocess.call(diff + u' ' + - temp.tempfile_name(ltf) + u' ' + - temp.tempfile_name(otf) + u' > ' + + subprocess.call(diff + ' ' + + temp.tempfile_name(ltf) + ' ' + + temp.tempfile_name(otf) + ' > ' + temp.tempfile_name(out), shell=True) - subprocess.check_call(pager + u' ' + + subprocess.check_call(pager + ' ' + temp.tempfile_name(out), shell=True) - except Exception, e: - self.log(u'Command error: ' + str(e)) + except Exception as e: + self.log('Command error: ' + str(e)) temp.tempfile_delete(ltf) temp.tempfile_delete(otf) temp.tempfile_delete(out) @@ -215,7 +215,9 @@ def log_timeout(self, loop, arg): self.gui_footer_log_clear() self.logs = [] else: - self.logs.pop(0) + # for some reason having problems with this being empty? + if len(self.logs) > 0: + self.logs.pop(0) log_pile = [] @@ -232,7 +234,7 @@ def log(self, msg): if not self.do_gui: if self.verbose: - print msg + print(msg) return self.log_lock.acquire() @@ -240,7 +242,7 @@ def log(self, msg): self.log_alarms += 1 self.logs.append(msg) - if len(self.logs) > self.config.get_config('max_logs'): + if len(self.logs) > int(self.config.get_config('max_logs')): self.log_alarms -= 1 self.logs.pop(0) @@ -263,7 +265,7 @@ def gui_update_view(self): try: cur_key = self.view_titles.note_list[self.view_titles.focus_position].note['key'] - except IndexError, e: + except IndexError as e: cur_key = None pass self.view_titles.update_note_list(self.view_titles.search_string) @@ -312,7 +314,7 @@ def restore_note_callback(self, key, yes): return # restore the contents of the old_note - self.log(u'Restoring version v{0} (key={1})'. + self.log('Restoring version v{0} (key={1})'. format(self.view_note.old_note['version'], key)) self.ndb.set_note_content(key, self.view_note.old_note['content']) @@ -348,8 +350,8 @@ def gui_version_input(self, args, version): try: # verify input is a number int(version) - except ValueError, e: - self.log(u'ERROR: Invalid version value') + except ValueError as e: + self.log('ERROR: Invalid version value') return self.view_note.update_note_view(version=version) self.gui_update_status_bar() @@ -391,8 +393,8 @@ def gui_pipe_input(self, args, cmd): pipe.communicate(note['content']) pipe.stdin.close() pipe.wait() - except OSError, e: - self.log(u'Pipe error: ' + str(e)) + except OSError as e: + self.log('Pipe error: ' + str(e)) finally: self.gui_reset() @@ -540,7 +542,7 @@ def gui_frame_keypress(self, size, key): return key if not self.view_note.old_note: - self.log(u'Already at latest version (key={0})'. + self.log('Already at latest version (key={0})'. format(self.view_note.key)) return None @@ -554,7 +556,7 @@ def gui_frame_keypress(self, size, key): return key if not self.view_note.old_note: - self.log(u'Already at latest version (key={0})'. + self.log('Already at latest version (key={0})'. format(self.view_note.key)) return None @@ -607,7 +609,7 @@ def gui_frame_keypress(self, size, key): self.gui_reset() if content: - self.log(u'New note created') + self.log('New note created') self.ndb.create_note(content) self.gui_update_view() self.ndb.sync_worker_go() @@ -642,11 +644,11 @@ def gui_frame_keypress(self, size, key): if not content: return None - md5_old = md5.new(self.encode_utf_8(note['content'])).digest() - md5_new = md5.new(self.encode_utf_8(content)).digest() + md5_old = hashlib.md5(self.encode_utf_8(note['content'])).digest() + md5_new = hashlib.md5(self.encode_utf_8(content)).digest() if md5_old != md5_new: - self.log(u'Note updated') + self.log('Note updated') self.ndb.set_note_content(note['key'], content) if self.gui_body_get().__class__ == view_titles.ViewTitles: lb.update_note_title() @@ -654,7 +656,7 @@ def gui_frame_keypress(self, size, key): lb.update_note_view() self.ndb.sync_worker_go() else: - self.log(u'Note unchanged') + self.log('Note unchanged') elif key == self.config.get_keybind('view_note'): if self.gui_body_get().__class__ != view_titles.ViewTitles: @@ -863,7 +865,7 @@ def gui_frame_keypress(self, size, key): def encode_utf_8(self, string): # This code also exists in temp.py. Move into an encoding or utility class if other areas need encoding. - return string.encode("utf-8") if isinstance(string, unicode) else string + return string.encode("utf-8") if isinstance(string, str) else string def gui_init_view(self, loop, view_note): self.master_frame.keypress = self.gui_frame_keypress @@ -876,7 +878,7 @@ def gui_init_view(self, loop, view_note): self.thread_sync.start() def gui_clear(self): - self.sncli_loop.widget = urwid.Filler(urwid.Text(u'')) + self.sncli_loop.widget = urwid.Filler(urwid.Text('')) self.sncli_loop.draw_screen() def gui_reset(self): @@ -1000,7 +1002,7 @@ def gui(self, key): self.config.get_color('help_descr_bg') ) ] - self.master_frame = urwid.Frame(body=urwid.Filler(urwid.Text(u'')), + self.master_frame = urwid.Frame(body=urwid.Filler(urwid.Text('')), header=None, footer=urwid.Pile([ urwid.Pile([]), urwid.Pile([]) ]), @@ -1023,38 +1025,38 @@ def cli_list_notes(self, regex, search_string): search_mode='regex' if regex else 'gstyle') for n in note_list: flags = utils.get_note_flags(n.note) - print n.key + \ - u' [' + flags + u'] ' + \ - utils.get_note_title(n.note) + print((n.key + \ + ' [' + flags + '] ' + \ + utils.get_note_title(n.note))) def cli_note_dump(self, key): note = self.ndb.get_note(key) if not note: - self.log(u'ERROR: Key does not exist') + self.log('ERROR: Key does not exist') return w = 60 - sep = u'+' + u'-'*(w+2) + u'+' + sep = '+' + '-'*(w+2) + '+' t = time.localtime(float(note['modifydate'])) mod_time = time.strftime('%a, %d %b %Y %H:%M:%S', t) title = utils.get_note_title(note) flags = utils.get_note_flags(note) tags = utils.get_note_tags(note) - print sep - print (u'| {:<' + str(w) + u'} |').format((u' Title: ' + title)[:w]) - print (u'| {:<' + str(w) + u'} |').format((u' Key: ' + note['key'])[:w]) - print (u'| {:<' + str(w) + u'} |').format((u' Date: ' + mod_time)[:w]) - print (u'| {:<' + str(w) + u'} |').format((u' Tags: ' + tags)[:w]) - print (u'| {:<' + str(w) + u'} |').format((u' Version: v' + str(note['version']))[:w]) - print (u'| {:<' + str(w) + u'} |').format((u' Flags: [' + flags + u']')[:w]) + print(sep) + print(('| {:<' + str(w) + '} |').format((' Title: ' + title)[:w])) + print(('| {:<' + str(w) + '} |').format((' Key: ' + note['key'])[:w])) + print(('| {:<' + str(w) + '} |').format((' Date: ' + mod_time)[:w])) + print(('| {:<' + str(w) + '} |').format((' Tags: ' + tags)[:w])) + print(('| {:<' + str(w) + '} |').format((' Version: v' + str(note['version']))[:w])) + print(('| {:<' + str(w) + '} |').format((' Flags: [' + flags + ']')[:w])) if utils.note_published(note) and 'publishkey' in note: - print (u'| {:<' + str(w) + u'} |').format((u'Published: http://simp.ly/publish/' + note['publishkey'])[:w]) + print(('| {:<' + str(w) + '} |').format(('Published: http://simp.ly/publish/' + note['publishkey'])[:w])) else: - print (u'| {:<' + str(w) + u'} |').format((u'Published: n/a')[:w]) - print sep - print note['content'] + print(('| {:<' + str(w) + '} |').format(('Published: n/a')[:w])) + print(sep) + print((note['content'])) def cli_dump_notes(self, regex, search_string): @@ -1073,10 +1075,10 @@ def cli_note_create(self, from_stdin, title): content = self.exec_cmd_on_note(None) if title: - content = title + '\n\n' + content if content else u'' + content = title + '\n\n' + content if content else '' if content: - self.log(u'New note created') + self.log('New note created') self.ndb.create_note(content) self.sync_notes() @@ -1084,28 +1086,28 @@ def cli_note_edit(self, key): note = self.ndb.get_note(key) if not note: - self.log(u'ERROR: Key does not exist') + self.log('ERROR: Key does not exist') return content = self.exec_cmd_on_note(note) if not content: return - md5_old = md5.new(self.encode_utf_8(note['content'])).digest() - md5_new = md5.new(self.encode_utf_8(content)).digest() + md5_old = hashlib.md5(self.encode_utf_8(note['content'])).digest() + md5_new = hashlib.md5(self.encode_utf_8(content)).digest() if md5_old != md5_new: - self.log(u'Note updated') + self.log('Note updated') self.ndb.set_note_content(note['key'], content) self.sync_notes() else: - self.log(u'Note unchanged') + self.log('Note unchanged') def cli_note_trash(self, key, trash): note = self.ndb.get_note(key) if not note: - self.log(u'ERROR: Key does not exist') + self.log('ERROR: Key does not exist') return self.ndb.set_note_deleted(key, trash) @@ -1115,7 +1117,7 @@ def cli_note_pin(self, key, pin): note = self.ndb.get_note(key) if not note: - self.log(u'ERROR: Key does not exist') + self.log('ERROR: Key does not exist') return self.ndb.set_note_pinned(key, pin) @@ -1125,7 +1127,7 @@ def cli_note_markdown(self, key, markdown): note = self.ndb.get_note(key) if not note: - self.log(u'ERROR: Key does not exist') + self.log('ERROR: Key does not exist') return self.ndb.set_note_markdown(key, markdown) @@ -1133,13 +1135,13 @@ def cli_note_markdown(self, key, markdown): def SIGINT_handler(signum, frame): - print u'\nSignal caught, bye!' + print('\nSignal caught, bye!') sys.exit(1) signal.signal(signal.SIGINT, SIGINT_handler) def usage(): - print u''' + print (''' Usage: sncli [OPTIONS] [COMMAND] [COMMAND_ARGS] @@ -1150,6 +1152,7 @@ def usage(): -r, --regex - search string is a regular expression -k <key>, --key=<key> - note key -t <title>, --title=<title> - title of note for create (cli mode) + -c <file>, --config=<file> - config file to read from (defaults to ~/.snclirc) COMMANDS: <none> - console gui mode when no command specified @@ -1162,7 +1165,7 @@ def usage(): < trash | untrash > - trash/untrash a note (specified by <key>) < pin | unpin > - pin/unpin a note (specified by <key>) < markdown | unmarkdown > - markdown/unmarkdown a note (specified by <key>) -''' +''') sys.exit(0) def main(argv): @@ -1171,11 +1174,12 @@ def main(argv): regex = False key = None title = None + config = None try: opts, args = getopt.getopt(argv, - 'hvnrk:t:', - [ 'help', 'verbose', 'nosync', 'regex', 'key=', 'title=' ]) + 'hvnrk:t:c:', + [ 'help', 'verbose', 'nosync', 'regex', 'key=', 'title=', 'config=' ]) except: usage() @@ -1192,30 +1196,32 @@ def main(argv): key = arg elif opt in [ '-t', '--title']: title = arg + elif opt in [ '-c', '--config']: + config = arg else: - print u'ERROR: Unhandled option' + print('ERROR: Unhandled option') usage() if not args: - sncli(sync).gui(key) + sncli(sync, verbose, config).gui(key) return - def sncli_start(sync, verbose): - sn = sncli(sync, verbose) + def sncli_start(sync=sync, verbose=verbose, config=config): + sn = sncli(sync, verbose, config) if sync: sn.sync_notes() return sn if args[0] == 'sync': - sn = sncli_start(True, verbose) + sn = sncli_start(True) elif args[0] == 'list': - sn = sncli_start(sync, verbose) + sn = sncli_start() sn.cli_list_notes(regex, ' '.join(args[1:])) elif args[0] == 'dump': - sn = sncli_start(sync, verbose) + sn = sncli_start() if key: sn.cli_note_dump(key) else: @@ -1224,10 +1230,10 @@ def sncli_start(sync, verbose): elif args[0] == 'create': if len(args) == 1: - sn = sncli_start(sync, verbose) + sn = sncli_start() sn.cli_note_create(False, title) elif len(args) == 2 and args[1] == '-': - sn = sncli_start(sync, verbose) + sn = sncli_start() sn.cli_note_create(True, title) else: usage() @@ -1237,7 +1243,7 @@ def sncli_start(sync, verbose): if not key: usage() - sn = sncli_start(sync, verbose) + sn = sncli_start() sn.cli_note_edit(key) elif args[0] == 'trash' or args[0] == 'untrash': @@ -1245,7 +1251,7 @@ def sncli_start(sync, verbose): if not key: usage() - sn = sncli_start(sync, verbose) + sn = sncli_start() sn.cli_note_trash(key, 1 if args[0] == 'trash' else 0) elif args[0] == 'pin' or args[0] == 'unpin': @@ -1253,7 +1259,7 @@ def sncli_start(sync, verbose): if not key: usage() - sn = sncli_start(sync, verbose) + sn = sncli_start() sn.cli_note_pin(key, 1 if args[0] == 'pin' else 0) elif args[0] == 'markdown' or args[0] == 'unmarkdown': @@ -1261,7 +1267,7 @@ def sncli_start(sync, verbose): if not key: usage() - sn = sncli_start(sync, verbose) + sn = sncli_start() sn.cli_note_markdown(key, 1 if args[0] == 'markdown' else 0) else: diff --git a/simplenote_cli/temp.py b/simplenote_cli/temp.py @@ -25,7 +25,7 @@ def tempfile_create(note, raw=False): def encode_utf_8(string): # This code also exists in sncli.py. Move into an encoding or utility class if other areas need encoding. - return string.encode("utf-8") if isinstance(string, unicode) else string + return string.encode("utf-8") if isinstance(string, str) else string def tempfile_delete(tf): if tf: @@ -40,5 +40,5 @@ def tempfile_content(tf): # This seems like a hack. When editing with Gedit, tf file contents weren't getting # updated in memory, even though it successfully saved on disk. updated_tf_contents = open(tf.name, 'r').read() - tf.write(updated_tf_contents) + tf.write(updated_tf_contents.encode("utf-8")) return updated_tf_contents diff --git a/simplenote_cli/utils.py b/simplenote_cli/utils.py @@ -22,10 +22,10 @@ def get_note_tags(note): if 'tags' in note: tags = '%s' % ','.join(note['tags']) if 'deleted' in note and note['deleted']: - if tags: tags += u',trash' - else: tags = u'trash' + if tags: tags += ',trash' + else: tags = 'trash' else: - tags = u'' + tags = '' return tags # Returns a fixed length string: @@ -36,12 +36,12 @@ def get_note_tags(note): # 'm' - markdown def get_note_flags(note): flags = '' - flags += u'X' if float(note['modifydate']) > float(note['syncdate']) else u' ' - flags += u'T' if 'deleted' in note and note['deleted'] else u' ' + flags += 'X' if float(note['modifydate']) > float(note['syncdate']) else ' ' + flags += 'T' if 'deleted' in note and note['deleted'] else ' ' if 'systemtags' in note: - flags += u'*' if 'pinned' in note['systemtags'] else u' ' - flags += u'S' if 'published' in note['systemtags'] else u' ' - flags += u'm' if 'markdown' in note['systemtags'] else u' ' + flags += '*' if 'pinned' in note['systemtags'] else ' ' + flags += 'S' if 'published' in note['systemtags'] else ' ' + flags += 'm' if 'markdown' in note['systemtags'] else ' ' else: flags += ' ' return flags @@ -63,9 +63,9 @@ def get_note_title_file(note): return '' if isinstance(fn, str): - fn = unicode(fn, 'utf-8') + fn = str(fn, 'utf-8') else: - fn = unicode(fn) + fn = str(fn) if note_markdown(note): fn += '.mkdn' @@ -145,21 +145,14 @@ def sanitise_tags(tags): else: return illegals_removed.split(',') -def sort_by_title_pinned(a, b): - if note_pinned(a.note) and not note_pinned(b.note): - return -1 - elif not note_pinned(a.note) and note_pinned(b.note): - return 1 - else: - return cmp(get_note_title(a.note), get_note_title(b.note)) +def sort_by_title_pinned(a): + return (not note_pinned(a.note), get_note_title(a.note)) -def sort_by_modify_date_pinned(a, b): - if note_pinned(a.note) and not note_pinned(b.note): - return 1 - elif not note_pinned(a.note) and note_pinned(b.note): - return -1 +def sort_by_modify_date_pinned(a): + if note_pinned(a.note): + return 100.0 * float(a.note.get('modifydate', 0)) else: - return cmp(float(a.note.get('modifydate', 0)), float(b.note.get('modifydate', 0))) + return float(a.note.get('modifydate', 0)) class KeyValueObject: """Store key=value pairs in this object and retrieve with o.key. diff --git a/simplenote_cli/view_help.py b/simplenote_cli/view_help.py @@ -13,12 +13,12 @@ def __init__(self, config): self.config_width = 29 lines = [] - lines.extend(self.create_kb_help_lines(u'Keybinds Common', 'common')) - lines.extend(self.create_kb_help_lines(u'Keybinds Note List', 'titles')) - lines.extend(self.create_kb_help_lines(u'Keybinds Note Content', 'notes')) + lines.extend(self.create_kb_help_lines('Keybinds Common', 'common')) + lines.extend(self.create_kb_help_lines('Keybinds Note List', 'titles')) + lines.extend(self.create_kb_help_lines('Keybinds Note Content', 'notes')) lines.extend(self.create_config_help_lines()) lines.extend(self.create_color_help_lines()) - lines.append(urwid.Text(('help_header', u''))) + lines.append(urwid.Text(('help_header', ''))) super(ViewHelp, self).__init__(urwid.SimpleFocusListWalker(lines)) @@ -30,13 +30,13 @@ def get_status_bar(self): total = len(self.body.positions()) status_title = \ - urwid.AttrMap(urwid.Text(u'Help', + urwid.AttrMap(urwid.Text('Help', wrap='clip'), 'status_bar') status_index = \ - ('pack', urwid.AttrMap(urwid.Text(u' ' + + ('pack', urwid.AttrMap(urwid.Text(' ' + str(cur + 1) + - u'/' + + '/' + str(total)), 'status_bar')) return \ @@ -44,10 +44,10 @@ def get_status_bar(self): 'status_bar') def create_kb_help_lines(self, header, use): - lines = [ urwid.AttrMap(urwid.Text(u''), + lines = [ urwid.AttrMap(urwid.Text(''), 'help_header', 'help_focus') ] - lines.append(urwid.AttrMap(urwid.Text(u' ' + header), + lines.append(urwid.AttrMap(urwid.Text(' ' + header), 'help_header', 'help_focus')) for c in self.config.keybinds: @@ -58,8 +58,8 @@ def create_kb_help_lines(self, header, use): urwid.Text( [ ('help_descr', ('{:>' + str(self.descr_width) + '} ').format(self.config.get_keybind_descr(c))), - ('help_config', ('{:>' + str(self.config_width) + '} ').format(u'kb_' + c)), - ('help_value', u"'" + self.config.get_keybind(c) + u"'") + ('help_config', ('{:>' + str(self.config_width) + '} ').format('kb_' + c)), + ('help_value', "'" + self.config.get_keybind(c) + "'") ] ), attr_map = None, @@ -72,10 +72,10 @@ def create_kb_help_lines(self, header, use): return lines def create_config_help_lines(self): - lines = [ urwid.AttrMap(urwid.Text(u''), + lines = [ urwid.AttrMap(urwid.Text(''), 'help_header', 'help_focus') ] - lines.append(urwid.AttrMap(urwid.Text(u' Configuration'), + lines.append(urwid.AttrMap(urwid.Text(' Configuration'), 'help_header', 'help_focus')) for c in self.config.configs: @@ -85,8 +85,8 @@ def create_config_help_lines(self): urwid.Text( [ ('help_descr', ('{:>' + str(self.descr_width) + '} ').format(self.config.get_config_descr(c))), - ('help_config', ('{:>' + str(self.config_width) + '} ').format(u'cfg_' + c)), - ('help_value', u"'" + self.config.get_config(c) + u"'") + ('help_config', ('{:>' + str(self.config_width) + '} ').format('cfg_' + c)), + ('help_value', "'" + self.config.get_config(c) + "'") ] ), attr_map = None, @@ -99,10 +99,10 @@ def create_config_help_lines(self): return lines def create_color_help_lines(self): - lines = [ urwid.AttrMap(urwid.Text(u''), + lines = [ urwid.AttrMap(urwid.Text(''), 'help_header', 'help_focus') ] - lines.append(urwid.AttrMap(urwid.Text(u' Colors'), + lines.append(urwid.AttrMap(urwid.Text(' Colors'), 'help_header', 'help_focus')) fmap = {} @@ -114,8 +114,8 @@ def create_color_help_lines(self): urwid.Text( [ ('help_descr', ('{:>' + str(self.descr_width) + '} ').format(self.config.get_color_descr(c))), - ('help_config', ('{:>' + str(self.config_width) + '} ').format(u'clr_' + c)), - (re.search('^(.*)(_fg|_bg)$', c).group(1), u"'" + self.config.get_color(c) + u"'") + ('help_config', ('{:>' + str(self.config_width) + '} ').format('clr_' + c)), + (re.search('^(.*)(_fg|_bg)$', c).group(1), "'" + self.config.get_color(c) + "'") ] ), attr_map = None, diff --git a/simplenote_cli/view_log.py b/simplenote_cli/view_log.py @@ -32,13 +32,13 @@ def get_status_bar(self): total = len(self.body.positions()) status_title = \ - urwid.AttrMap(urwid.Text(u'Sync Log', + urwid.AttrMap(urwid.Text('Sync Log', wrap='clip'), 'status_bar') status_index = \ - ('pack', urwid.AttrMap(urwid.Text(u' ' + + ('pack', urwid.AttrMap(urwid.Text(' ' + str(cur + 1) + - u'/' + + '/' + str(total)), 'status_bar')) return \ diff --git a/simplenote_cli/view_note.py b/simplenote_cli/view_note.py @@ -3,9 +3,9 @@ # Licensed under the MIT License import time, urwid -import utils +from . import utils import re -from clipboard import Clipboard +from .clipboard import Clipboard class ViewNote(urwid.ListBox): @@ -40,7 +40,7 @@ def get_note_content_as_list(self): urwid.AttrMap(urwid.Text(l.replace('\t', ' ' * self.tabstop)), 'note_content', 'note_content_focus')) - lines.append(urwid.AttrMap(urwid.Divider(u'-'), 'default')) + lines.append(urwid.AttrMap(urwid.Divider('-'), 'default')) return lines def update_note_view(self, key=None, version=None): @@ -52,22 +52,22 @@ def update_note_view(self, key=None, version=None): if self.key and version: # verify version is within range if int(version) <= 0 or int(version) >= self.note['version'] + 1: - self.log(u'Version v{0} is unavailable (key={1})'. + self.log('Version v{0} is unavailable (key={1})'. format(version, self.key)) return if (not version and self.old_note) or \ (self.key and version and version == self.note['version']): - self.log(u'Displaying latest version v{0} of note (key={1})'. + self.log('Displaying latest version v{0} of note (key={1})'. format(self.note['version'], self.key)) self.old_note = None elif self.key and version: # get a previous version of the note - self.log(u'Fetching version v{0} of note (key={1})'. + self.log('Fetching version v{0} of note (key={1})'. format(version, self.key)) version_note = self.ndb.get_note_version(self.key, version) if not version_note: - self.log(u'Failed to get version v{0} of note (key={1})'. + self.log('Failed to get version v{0} of note (key={1})'. format(version, self.key)) # don't do anything, keep current note/version else: @@ -79,11 +79,11 @@ def update_note_view(self, key=None, version=None): self.focus_position = 0 def lines_after_current_position(self): - lines_after_current_position = range(self.focus_position + 1, len(self.body.positions()) - 1) + lines_after_current_position = list(range(self.focus_position + 1, len(self.body.positions()) - 1)) return lines_after_current_position def lines_before_current_position(self): - lines_before_current_position = range(0, self.focus_position) + lines_before_current_position = list(range(0, self.focus_position)) lines_before_current_position.reverse() return lines_before_current_position @@ -121,7 +121,7 @@ def is_match(self, term, full_text): def get_status_bar(self): if not self.key: return \ - urwid.AttrMap(urwid.Text(u'No note...'), + urwid.AttrMap(urwid.Text('No note...'), 'status_bar') cur = -1 @@ -141,20 +141,20 @@ def get_status_bar(self): tags = utils.get_note_tags(self.note) version = self.note['version'] - mod_time = time.strftime(u'Date: %a, %d %b %Y %H:%M:%S', t) + mod_time = time.strftime('Date: %a, %d %b %Y %H:%M:%S', t) status_title = \ - urwid.AttrMap(urwid.Text(u'Title: ' + + urwid.AttrMap(urwid.Text('Title: ' + title, wrap='clip'), 'status_bar') status_key_index = \ - ('pack', urwid.AttrMap(urwid.Text(u' [' + + ('pack', urwid.AttrMap(urwid.Text(' [' + self.key + - u'] ' + + '] ' + str(cur + 1) + - u'/' + + '/' + str(total)), 'status_bar')) @@ -165,19 +165,19 @@ def get_status_bar(self): if self.old_note: status_tags_flags = \ - ('pack', urwid.AttrMap(urwid.Text(u'[OLD:v' + + ('pack', urwid.AttrMap(urwid.Text('[OLD:v' + str(version) + - u']'), + ']'), 'status_bar')) else: status_tags_flags = \ - ('pack', urwid.AttrMap(urwid.Text(u'[' + + ('pack', urwid.AttrMap(urwid.Text('[' + tags + - u'] [v' + + '] [v' + str(version) + - u'] [' + + '] [' + flags + - u']'), + ']'), 'status_bar')) pile_top = urwid.Columns([ status_title, status_key_index ]) @@ -189,7 +189,7 @@ def get_status_bar(self): 'status_bar') pile_publish = \ - urwid.AttrMap(urwid.Text(u'Published: http://simp.ly/publish/' + + urwid.AttrMap(urwid.Text('Published: http://simp.ly/publish/' + self.note['publishkey']), 'status_bar') return \ diff --git a/simplenote_cli/view_titles.py b/simplenote_cli/view_titles.py @@ -3,7 +3,7 @@ # Licensed under the MIT License import re, time, datetime, urwid, subprocess -import utils, view_note +from . import utils, view_note class ViewTitles(urwid.ListBox): @@ -24,7 +24,7 @@ def update_note_list(self, search_string, search_mode='gstyle'): self.body[:] = \ urwid.SimpleFocusListWalker(self.get_note_titles()) if len(self.note_list) == 0: - self.log(u'No notes found!') + self.log('No notes found!') else: self.focus_position = 0 @@ -150,7 +150,7 @@ def get_status_bar(self): cur = self.focus_position total = len(self.body.positions()) - hdr = u'Simplenote' + hdr = 'Simplenote' if self.search_string != None: hdr += ' - Search: ' + self.search_string @@ -159,9 +159,9 @@ def get_status_bar(self): wrap='clip'), 'status_bar') status_index = \ - ('pack', urwid.AttrMap(urwid.Text(u' ' + + ('pack', urwid.AttrMap(urwid.Text(' ' + str(cur + 1) + - u'/' + + '/' + str(total)), 'status_bar')) return \ @@ -173,12 +173,12 @@ def update_note_title(self, key=None): self.body[self.focus_position] = \ self.get_note_title(self.note_list[self.focus_position].note) else: - for i in xrange(len(self.note_list)): + for i in range(len(self.note_list)): if self.note_list[i].note['key'] == key: self.body[i] = self.get_note_title(self.note_list[i].note) def focus_note(self, key): - for i in xrange(len(self.note_list)): + for i in range(len(self.note_list)): if 'key' in self.note_list[i].note and \ self.note_list[i].note['key'] == key: self.focus_position = i diff --git a/sncli b/sncli @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 # # ** The MIT License **