nncli

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

commit bfcac97591b757230d6a23549d1023ac054218c2
parent 207767bc879a6eeda61cf3a96388d2504057e6b1
Author: Daniel Moch <daniel@danielmoch.com>
Date:   Mon, 10 Sep 2018 06:05:53 -0400

Address pylint findings in the rest

Ref #10

Diffstat:
Mnncli/clipboard.py | 16+++++++++++++---
Mnncli/config.py | 1+
Mnncli/nextcloud_note.py | 10+++++-----
Mnncli/notes_db.py | 522++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mnncli/temp.py | 48++++++++++++++++++++++++++++--------------------
Mnncli/user_input.py | 7++++---
Mnncli/utils.py | 56++++++++++++++++++++++++++++++++++++--------------------
Mnncli/view_help.py | 172+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Mnncli/view_log.py | 30++++++++++++++++++------------
Mnncli/view_note.py | 93+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mnncli/view_titles.py | 175+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
11 files changed, 678 insertions(+), 452 deletions(-)

diff --git a/nncli/clipboard.py b/nncli/clipboard.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- """clipboard module""" import os -from distutils import spawn +import subprocess +from subprocess import CalledProcessError class Clipboard: """Class implements copying note content to the clipboard""" @@ -11,10 +12,19 @@ def __init__(self): @staticmethod def get_copy_command(): """Defines the copy command based on the contents of $PATH""" - if spawn.find_executable('xsel'): + + try: + subprocess.check_output(['which', 'xsel']) return 'echo "%s" | xsel -ib' - if spawn.find_executable('pbcopy'): + except CalledProcessError: + pass + + try: + subprocess.check_output(['which', 'pbcopy']) return 'echo "%s" | pbcopy' + except CalledProcessError: + pass + return None def copy(self, text): diff --git a/nncli/config.py b/nncli/config.py @@ -8,6 +8,7 @@ from appdirs import user_cache_dir, user_config_dir +# pylint: disable=too-few-public-methods class Config: """A class to contain all configuration data for nncli""" class State: diff --git a/nncli/nextcloud_note.py b/nncli/nextcloud_note.py @@ -105,14 +105,14 @@ def update_note(self, note): self.status = 'online' except ConnectionError as ex: self.status = 'offline, connection error' - return ex, -1 + raise ex except RequestException as ex: logging.debug('RESPONSE ERROR: %s', ex) logging.debug(traceback.print_exc()) self.status = 'error updating note, check log' - return ex, -1 + raise ex except ValueError as ex: - return ex, -1 + raise ex #logging.debug('RESPONSE OK: ' + str(note)) return note, 0 @@ -199,7 +199,7 @@ def delete_note(self, note): self.status = 'online' except ConnectionError as ex: self.status = 'offline, connection error' - return ex, -1 + raise ex except RequestException as ex: - return ex, -1 + raise ex return {}, 0 diff --git a/nncli/notes_db.py b/nncli/notes_db.py @@ -1,14 +1,25 @@ # -*- coding: utf-8 -*- +"""notes_db module""" +import copy +import glob +import json +import os +import re +import threading +import time +from requests.exceptions import RequestException -import os, time, re, glob, json, copy, threading from . import utils from .nextcloud_note import NextcloudNote -import logging +# pylint: disable=too-many-instance-attributes, too-many-locals +# pylint: disable=too-many-branches, too-many-statements class ReadError(RuntimeError): + """Exception thrown on a read error""" pass class WriteError(RuntimeError): + """Exception thrown on a write error""" pass class NotesDB(): @@ -17,13 +28,13 @@ class NotesDB(): NextCloud Notes """ def __init__(self, config, log, update_view=None): - self.config = config - self.log = log + self.config = config + self.log = log self.update_view = update_view self.last_sync = 0 # set to zero to trigger a full sync self.sync_lock = threading.Lock() - self.go_cond = threading.Condition() + self.go_cond = threading.Condition() # create db dir if it does not exist if not os.path.exists(self.config.get_config('db_path')): @@ -31,32 +42,35 @@ def __init__(self, config, log, update_view=None): now = int(time.time()) # now read all .json files from disk - fnlist = glob.glob(self.helper_key_to_fname('*')) + fnlist = glob.glob(self._helper_key_to_fname('*')) self.notes = {} - for fn in fnlist: + for func in fnlist: try: - n = json.load(open(fn, 'r')) - except IOError as e: - raise ReadError ('Error opening {0}: {1}'.format(fn, str(e))) - except ValueError as e: - raise ReadError ('Error reading {0}: {1}'.format(fn, str(e))) + note = json.load(open(func, 'r')) + except IOError as ex: + raise ReadError('Error opening {0}: {1}'.format(func, str(ex))) + except ValueError as ex: + raise ReadError('Error reading {0}: {1}'.format(func, str(ex))) else: # we always have a localkey, also when we don't have a # note['id'] yet (no sync) - localkey = n.get('localkey', os.path.splitext(os.path.basename(fn))[0]) + localkey = note.get( + 'localkey', + os.path.splitext(os.path.basename(func))[0] + ) # we maintain in memory a timestamp of the last save # these notes have just been read, so at this moment # they're in sync with the disc. - n['savedate'] = now + note['savedate'] = now # set a localkey to each note in memory # Note: 'id' is used only for syncing with server - 'localkey' # is used for everything else in nncli - n['localkey'] = localkey + note['localkey'] = localkey # add the note to our database - self.notes[localkey] = n + self.notes[localkey] = note # initialise the NextCloud instance we're going to use # this does not yet need network access @@ -65,14 +79,19 @@ def __init__(self, config, log, update_view=None): self.config.get_config('nn_host')) def set_update_view(self, update_view): + """Set the update_view method""" self.update_view = update_view - def filtered_notes_sort(self, filtered_notes, sort_mode='date'): + def _filtered_notes_sort(self, filtered_notes, sort_mode='date'): + """Sort filtered note set""" if sort_mode == 'date': if self.config.get_config('favorite_ontop') == 'yes': - filtered_notes.sort(key=utils.sort_by_modify_date_favorite, reverse=True) + filtered_notes.sort(key=utils.sort_by_modify_date_favorite, + reverse=True) else: - filtered_notes.sort(key=lambda o: -float(o.note.get('modified', 0))) + filtered_notes.sort( + key=lambda o: -float(o.note.get('modified', 0)) + ) elif sort_mode == 'alpha': if self.config.get_config('favorite_ontop') == 'yes': filtered_notes.sort(key=utils.sort_by_title_favorite) @@ -83,7 +102,8 @@ def filtered_notes_sort(self, filtered_notes, sort_mode='date'): utils.sort_notes_by_categories(filtered_notes, \ favorite_ontop=favorite) - def filter_notes(self, search_string=None, search_mode='gstyle', sort_mode='date'): + def filter_notes(self, search_string=None, search_mode='gstyle', + sort_mode='date'): """Return list of notes filtered with search string. Based on the search mode that has been selected in self.config, @@ -98,21 +118,24 @@ def filter_notes(self, search_string=None, search_mode='gstyle', sort_mode='date if search_mode == 'gstyle': filtered_notes, match_regexp, active_notes = \ - self.filter_notes_gstyle(search_string) + self._filter_notes_gstyle(search_string) else: filtered_notes, match_regexp, active_notes = \ - self.filter_notes_regex(search_string) + self._filter_notes_regex(search_string) - self.filtered_notes_sort(filtered_notes, sort_mode) + self._filtered_notes_sort(filtered_notes, sort_mode) return filtered_notes, match_regexp, active_notes - def _helper_gstyle_categorymatch(self, cat_pats, note): + @staticmethod + def _helper_gstyle_categorymatch(cat_pats, note): + """Match categories using a Google-style search string""" # Returns: # 2 = match - no category patterns specified # 1 = match - all category patterns match a category on this # note - # 0 = no match - note has no category or not all category patterns match + # 0 = no match - note has no category or not all category + # patterns match if not cat_pats: # match because no category patterns were specified @@ -121,16 +144,17 @@ def _helper_gstyle_categorymatch(self, cat_pats, note): note_category = note.get('category') if not note_category: - # category patterns specified but note has no categories, so no match + # category patterns specified but note has no categories, + # so no match return 0 # for each cat_pat, we have to find a matching category # .lower() used for case-insensitive search cat_pats_matched = 0 - for tp in cat_pats: - tp = tp.lower() - for t in note_category: - if tp in t.lower(): + for cat_pat in cat_pats: + cat_pat = cat_pat.lower() + for pat in note_category: + if cat_pat in pat.lower(): cat_pats_matched += 1 break @@ -141,32 +165,36 @@ def _helper_gstyle_categorymatch(self, cat_pats, note): # note doesn't match return 0 - def _helper_gstyle_wordmatch(self, word_pats, content): + @staticmethod + def _helper_gstyle_wordmatch(word_pats, content): + """Match note contents based no a Google-style search string""" if not word_pats: return True word_pats_matched = 0 lowercase_content = content.lower() # case insensitive search - for wp in word_pats: - wp = wp.lower() # case insensitive search - if wp in lowercase_content: + for word_pat in word_pats: + word_pat = word_pat.lower() # case insensitive search + if word_pat in lowercase_content: word_pats_matched += 1 if word_pats_matched == len(word_pats): - return True; + return True return False - def filter_notes_gstyle(self, search_string=None): - + def _filter_notes_gstyle(self, search_string=None): + """Filter the notes based of a Google-style search string""" filtered_notes = [] active_notes = 0 if not search_string: - for k in self.notes: - n = self.notes[k] + for key in self.notes: + note = self.notes[key] active_notes += 1 - filtered_notes.append(utils.KeyValueObject(key=k, note=n, catfound=0)) + filtered_notes.append( + utils.KeyValueObject(key=key, note=note, catfound=0) + ) return filtered_notes, [], active_notes @@ -174,41 +202,48 @@ def filter_notes_gstyle(self, search_string=None): # group1: multiple words in quotes # group2: single words - # example result for: 'category:category1 category:category2 word1 "word2 word3" category:category3' + # example result for: 'category:category1 category:category2 + # word1 "word2 word3" category:category3' # [ ('category1', '', ''), # ('category2', '', ''), # ('', '', 'word1'), # ('', 'word2 word3', ''), # ('category3', '', '') ] - groups = re.findall('category:([^\s]+)|"([^"]+)"|([^\s]+)', search_string) + groups = re.findall( + r'category:([^\s]+)|"([^"]+)"|([^\s]+)', search_string + ) all_pats = [[] for _ in range(3)] # we end up with [[cat_pats],[multi_word_pats],[single_word_pats]] - for g in groups: + for group in groups: for i in range(3): - if g[i]: all_pats[i].append(g[i]) + if group[i]: + all_pats[i].append(group[i]) - for k in self.notes: - n = self.notes[k] + for key in self.notes: + note = self.notes[key] active_notes += 1 - catmatch = self._helper_gstyle_categorymatch(all_pats[0], n) + catmatch = self._helper_gstyle_categorymatch(all_pats[0], + note) word_pats = all_pats[1] + all_pats[2] if catmatch and \ - self._helper_gstyle_wordmatch(word_pats, n.get('content')): + self._helper_gstyle_wordmatch(word_pats, note.get('content')): # we have a note that can go through! filtered_notes.append( - utils.KeyValueObject(key=k, - note=n, - catfound=1 if catmatch == 1 else 0)) + utils.KeyValueObject(key=key, + note=note, + catfound=1 \ + if catmatch == 1 \ + else 0)) return filtered_notes, '|'.join(all_pats[1] + all_pats[2]), active_notes - def filter_notes_regex(self, search_string=None): + def _filter_notes_regex(self, search_string=None): """ Return a list of notes filtered using the regex search_string. Each element in the list is a tuple (local_key, note). @@ -218,32 +253,40 @@ def filter_notes_regex(self, search_string=None): filtered_notes = [] active_notes = 0 # total number of notes, including deleted ones - for k in self.notes: - n = self.notes[k] + for key in self.notes: + note = self.notes[key] active_notes += 1 if not sspat: - filtered_notes.append(utils.KeyValueObject(key=k, note=n, catfound=0)) + filtered_notes.append( + utils.KeyValueObject(key=key, note=note, catfound=0) + ) continue if self.config.get_config('search_categories') == 'yes': cat_matched = False - for t in n.get('category'): - if sspat.search(t): + for cat in note.get('category'): + if sspat.search(cat): cat_matched = True - filtered_notes.append(utils.KeyValueObject(key=k, note=n, catfound=1)) + filtered_notes.append( + utils.KeyValueObject(key=key, + note=note, catfound=1) + ) break if cat_matched: continue - if sspat.search(n.get('content')): - filtered_notes.append(utils.KeyValueObject(key=k, note=n, catfound=0)) + if sspat.search(note.get('content')): + filtered_notes.append( + utils.KeyValueObject(key=key, note=note, catfound=0) + ) match_regexp = search_string if sspat else '' return filtered_notes, match_regexp, active_notes def import_note(self, note): + """Import a note into the database""" # need to get a key unique to this database. not really important # what it is, as long as it's unique. new_key = note['id'] if note.get('id') else utils.generate_random_key() @@ -255,21 +298,23 @@ def import_note(self, note): try: modified = float(note.get('modified', timestamp)) except ValueError: - raise ValueError('date fields must be numbers or string representations of numbers') + raise ValueError('date fields must be numbers or string' + 'representations of numbers') # note has no internal key yet. - new_note = { - 'content' : note.get('content', ''), - 'modified' : modified, - 'title' : note.get('title'), - 'category' : note.get('category') \ - if note.get('category') is not None \ - else '', - 'savedate' : 0, # never been written to disc - 'syncdate' : 0, # never been synced with server - 'favorite' : False, - 'deleted' : False - } + new_note = \ + { + 'content' : note.get('content', ''), + 'modified' : modified, + 'title' : note.get('title'), + 'category' : note.get('category') \ + if note.get('category') is not None \ + else '', + 'savedate' : 0, # never been written to disc + 'syncdate' : 0, # never been synced with server + 'favorite' : False, + 'deleted' : False + } # sanity check all note values if not isinstance(new_note['content'], str): @@ -290,6 +335,7 @@ def import_note(self, note): return new_key def create_note(self, content): + """Create a new note in the database""" # need to get a key unique to this database. not really important # what it is, as long as it's unique. new_key = utils.generate_random_key() @@ -300,84 +346,95 @@ def create_note(self, content): title = content.split('\n')[0] # note has no internal key yet. - new_note = { - 'localkey' : new_key, - 'content' : content, - 'modified' : timestamp, - 'category' : '', - 'savedate' : 0, # never been written to disc - 'syncdate' : 0, # never been synced with server - 'favorite' : False, - 'deleted' : False, - 'title' : title - } + new_note = \ + { + 'localkey' : new_key, + 'content' : content, + 'modified' : timestamp, + 'category' : '', + 'savedate' : 0, # never been written to disc + 'syncdate' : 0, # never been synced with server + 'favorite' : False, + 'deleted' : False, + 'title' : title + } self.notes[new_key] = new_note return new_key def get_note(self, key): + """Get a note from the database""" return self.notes[key] - def get_note_category(self, key): + def _get_note_category(self, key): + """Get a category for a note""" return self.notes[key].get('category') - def flag_what_changed(self, note, what_changed): + @staticmethod + def _flag_what_changed(note, what_changed): + """Flag a note field as changed""" if 'what_changed' not in note: note['what_changed'] = [] if what_changed not in note['what_changed']: note['what_changed'].append(what_changed) def set_note_deleted(self, key, deleted): - n = self.notes[key] - old_deleted = n['deleted'] if 'deleted' in n else 0 + """Mark a note for deletion""" + note = self.notes[key] + old_deleted = note['deleted'] if 'deleted' in note else 0 if old_deleted != deleted: - n['deleted'] = deleted - n['modified'] = int(time.time()) - self.flag_what_changed(n, 'deleted') + note['deleted'] = deleted + note['modified'] = int(time.time()) + self._flag_what_changed(note, 'deleted') self.log('Note marked for deletion (key={0})'.format(key)) def set_note_content(self, key, content): - n = self.notes[key] - old_content = n.get('content') + """Set the content of a note in the database""" + note = self.notes[key] + old_content = note.get('content') if content != old_content: - n['content'] = content - n['modified'] = int(time.time()) - self.flag_what_changed(n, 'content') + note['content'] = content + note['modified'] = int(time.time()) + self._flag_what_changed(note, 'content') self.log('Note content updated (key={0})'.format(key)) def set_note_category(self, key, category): - n = self.notes[key] - old_category = n.get('category') + """Set the category of a note in the database""" + note = self.notes[key] + old_category = note.get('category') if category != old_category: - n['category'] = category - n['modified'] = int(time.time()) - self.flag_what_changed(n, 'category') + note['category'] = category + note['modified'] = int(time.time()) + self._flag_what_changed(note, 'category') self.log('Note category updated (key={0})'.format(key)) def set_note_favorite(self, key, favorite): - n = self.notes[key] - old_favorite = utils.note_favorite(n) + """Mark a note in the database as a favorite""" + note = self.notes[key] + old_favorite = utils.note_favorite(note) if favorite != old_favorite: - n['favorite'] = favorite - n['modified'] = int(time.time()) - self.flag_what_changed(n, 'favorite') + note['favorite'] = favorite + note['modified'] = int(time.time()) + self._flag_what_changed(note, 'favorite') self.log('Note {0} (key={1})'. \ format('favorite' if favorite else \ 'unfavorited', key)) - def helper_key_to_fname(self, k): + def _helper_key_to_fname(self, k): + """Convert a note key into a file name""" return os.path.join(self.config.get_config('db_path'), str(k)) + '.json' - def helper_save_note(self, k, note): + def _helper_save_note(self, k, note): + """Save a note to the file system""" # Save a single note to disc. - fn = self.helper_key_to_fname(k) - json.dump(note, open(fn, 'w'), indent=2) + func = self._helper_key_to_fname(k) + json.dump(note, open(func, 'w'), indent=2) # record that we saved this to disc. note['savedate'] = int(time.time()) - def sync_notes(self, server_sync=True, full_sync=True): + def _sync_notes(self, server_sync=True, full_sync=True): """Perform a full bi-directional sync with server. Psuedo-code algorithm for syncing: @@ -412,15 +469,15 @@ def sync_notes(self, server_sync=True, full_sync=True): # 1. for any note changed locally, including new notes: # save note to server, update note with response - for note_index, local_key in enumerate(self.notes.keys()): - n = self.notes[local_key] + for _, local_key in enumerate(self.notes.keys()): + note = self.notes[local_key] - if not n.get('id') or \ - float(n.get('modified')) > float(n.get('syncdate')): + if not note.get('id') or \ + float(note.get('modified')) > float(note.get('syncdate')): - savedate = float(n.get('savedate')) - if float(n.get('modified')) > savedate or \ - float(n.get('syncdate')) > savedate: + savedate = float(note.get('savedate')) + if float(note.get('modified')) > savedate or \ + float(note.get('syncdate')) > savedate: # this will trigger a save to disk after sync algorithm # we want this note saved even if offline or sync fails local_updates[local_key] = True @@ -431,77 +488,81 @@ def sync_notes(self, server_sync=True, full_sync=True): continue # only send required fields - cn = copy.deepcopy(n) - if 'what_changed' in n: - del n['what_changed'] - - if 'localkey' in cn: - del cn['localkey'] - - if 'minversion' in cn: - del cn['minversion'] - del cn['syncdate'] - del cn['savedate'] - del cn['deleted'] - if 'etag' in cn: - del cn['etag'] - if 'title' in cn: - del cn['title'] - - if 'what_changed' in cn: - if 'content' not in cn['what_changed'] \ - and 'category' not in cn['what_changed']: - del cn['content'] - if 'category' not in cn['what_changed']: - del cn['category'] - if 'favorite' not in cn['what_changed']: - del cn['favorite'] - del cn['what_changed'] - - if n['deleted']: - uret = self.note.delete_note(cn) - else: - uret = self.note.update_note(cn) + cnote = copy.deepcopy(note) + if 'what_changed' in note: + del note['what_changed'] + + if 'localkey' in cnote: + del cnote['localkey'] + + if 'minversion' in cnote: + del cnote['minversion'] + del cnote['syncdate'] + del cnote['savedate'] + del cnote['deleted'] + if 'etag' in cnote: + del cnote['etag'] + if 'title' in cnote: + del cnote['title'] + + if 'what_changed' in cnote: + if 'content' not in cnote['what_changed'] \ + and 'category' not in cnote['what_changed']: + del cnote['content'] + if 'category' not in cnote['what_changed']: + del cnote['category'] + if 'favorite' not in cnote['what_changed']: + del cnote['favorite'] + del cnote['what_changed'] + + try: + if note['deleted']: + uret = self.note.delete_note(cnote) + else: + uret = self.note.update_note(cnote) - if uret[1] == 0: # success # if this is a new note our local key is not valid anymore # merge the note we got back (content could be empty) # record syncdate and save the note at the assigned key del self.notes[local_key] - k = uret[0].get('id') - t = uret[0].get('title') - c = uret[0].get('category') - c = c if c is not None else '' - n.update(uret[0]) - n['syncdate'] = now - n['localkey'] = k - n['category'] = c - self.notes[k] = n - - local_updates[k] = True - if local_key != k: + key = uret[0].get('id') + category = uret[0].get('category') + category = category if category is not None else '' + note.update(uret[0]) + note['syncdate'] = now + note['localkey'] = key + note['category'] = category + self.notes[key] = note + + local_updates[key] = True + if local_key != key: # if local_key was a different key it should be deleted local_deletes[local_key] = True if local_key in local_updates: del local_updates[local_key] - self.log('Synced note to server (key={0})'.format(local_key)) - else: - self.log('ERROR: Failed to sync note to server (key={0})'.format(local_key)) + self.log( + 'Synced note to server (key={0})'.format(local_key) + ) + except (ConnectionError, RequestException, ValueError): + self.log( + 'ERROR: Failed to sync note to server (key={0})'. + format(local_key) + ) sync_errors += 1 # 2. get the note index if not server_sync: - nl = [] + note_list = [] else: - nl = self.note.get_note_list() + note_list = self.note.get_note_list() - if nl[1] == 0: # success - nl = nl[0] + if note_list[1] == 0: # success + note_list = note_list[0] else: self.log('ERROR: Failed to get note list from server') sync_errors += 1 - nl = [] + note_list = [] skip_remote_syncing = True # 3. for each remote note @@ -509,44 +570,58 @@ def sync_notes(self, server_sync=True, full_sync=True): # a new note and key is not in local store # retrieve note, update note with response if not skip_remote_syncing: - for note_index, n in enumerate(nl): - k = n.get('id') - c = n.get('category') if n.get('category') is not None \ + for _, note in enumerate(note_list): + key = note.get('id') + category = note.get('category') \ + if note.get('category') is not None \ else '' - server_keys[k] = True + server_keys[key] = True # this works because in the prior step we rewrite local keys to # server keys when we get an updated note back from the server - if k in self.notes: + if key in self.notes: # we already have this note # if the server note has a newer syncnum we need to get it - if int(n.get('modified')) > int(self.notes[k].get('modified')): - gret = self.note.get_note(k) + if int(note.get('modified')) > \ + int(self.notes[key].get('modified')): + gret = self.note.get_note(key) if gret[1] == 0: - self.notes[k].update(gret[0]) - local_updates[k] = True - self.notes[k]['syncdate'] = now - self.notes[k]['localkey'] = k - self.notes[k]['category'] = c - self.notes[k]['deleted'] = False - - self.log('Synced newer note from server (key={0})'.format(k)) + self.notes[key].update(gret[0]) + local_updates[key] = True + self.notes[key]['syncdate'] = now + self.notes[key]['localkey'] = key + self.notes[key]['category'] = category + self.notes[key]['deleted'] = False + + self.log( + 'Synced newer note from server (key={0})'. + format(key) + ) else: - self.log('ERROR: Failed to sync newer note from server (key={0})'.format(k)) + self.log( + 'ERROR: Failed to sync newer note ' + 'from server (key={0})'.format(key) + ) sync_errors += 1 else: # this is a new note - gret = self.note.get_note(k) + gret = self.note.get_note(key) if gret[1] == 0: - self.notes[k] = gret[0] - local_updates[k] = True - self.notes[k]['syncdate'] = now - self.notes[k]['localkey'] = k - self.notes[k]['category'] = c - self.notes[k]['deleted'] = False - - self.log('Synced new note from server (key={0})'.format(k)) + self.notes[key] = gret[0] + local_updates[key] = True + self.notes[key]['syncdate'] = now + self.notes[key]['localkey'] = key + self.notes[key]['category'] = category + self.notes[key]['deleted'] = False + + self.log( + 'Synced new note from server (key={0})'. + format(key) + ) else: - self.log('ERROR: Failed syncing new note from server (key={0})'.format(k)) + self.log( + 'ERROR: Failed syncing new note from' + 'server (key={0})'.format(key) + ) sync_errors += 1 # 4. for each local note not in the index @@ -562,22 +637,22 @@ def sync_notes(self, server_sync=True, full_sync=True): for k in list(local_updates.keys()): try: - self.helper_save_note(k, self.notes[k]) - except WriteError as e: - raise WriteError (str(e)) - self.log("Saved note to disk (key={0})".format(k)) + self._helper_save_note(k, self.notes[k]) + except WriteError as ex: + raise WriteError(str(ex)) + self.log("Saved note to disk (key={0})".format(key)) for k in list(local_deletes.keys()): - fn = self.helper_key_to_fname(k) - if os.path.exists(fn): - os.unlink(fn) - self.log("Deleted note from disk (key={0})".format(k)) + fnote = self._helper_key_to_fname(k) + if os.path.exists(fnote): + os.unlink(fnote) + self.log("Deleted note from disk (key={0})".format(key)) if not sync_errors: self.last_sync = sync_start_time # if there were any changes then update the current view - if len(local_updates) > 0 or len(local_deletes) > 0: + if local_updates or local_deletes: self.update_view() if server_sync and full_sync: @@ -585,35 +660,41 @@ def sync_notes(self, server_sync=True, full_sync=True): return sync_errors - def get_note_status(self, key): - n = self.notes[key] - o = utils.KeyValueObject(saved=False, synced=False, modified=False) - modified = float(n['modified']) - savedate = float(n['savedate']) + def _get_note_status(self, key): + """Get the note status""" + note = self.notes[key] + obj = utils.KeyValueObject(saved=False, synced=False, modified=False) + modified = float(note['modified']) + savedate = float(note['savedate']) if savedate > modified: - o.saved = True - return o + obj.saved = True + return obj - def verify_all_saved(self): + def _verify_all_saved(self): + """ + Verify all notes in the local database are saved to the + server + """ all_saved = True self.sync_lock.acquire() for k in list(self.notes.keys()): - o = self.get_note_status(k) - if not o.saved: + obj = self._get_note_status(k) + if not obj.saved: all_saved = False break self.sync_lock.release() return all_saved def sync_now(self, do_server_sync=True): + """Sync the notes to the server""" self.sync_lock.acquire() - self.sync_notes(server_sync=do_server_sync, - full_sync=True if not self.last_sync else False) + self._sync_notes(server_sync=do_server_sync, + full_sync=True if not self.last_sync else False) self.sync_lock.release() - # sync worker thread... - def sync_worker(self, do_server_sync): + def _sync_worker(self, do_server_sync): + """The sync worker thread""" time.sleep(1) # give some time to wait for GUI initialization self.log('Sync worker: started') self.sync_now(do_server_sync) @@ -623,7 +704,8 @@ def sync_worker(self, do_server_sync): self.sync_now(do_server_sync) self.go_cond.release() - def sync_worker_go(self): + def _sync_worker_go(self): + """Start the sync worker""" self.go_cond.acquire() self.go_cond.notify() self.go_cond.release() diff --git a/nncli/temp.py b/nncli/temp.py @@ -1,42 +1,50 @@ # -*- coding: utf-8 -*- - -import os, json, tempfile +"""temp module""" +import json +import os +import tempfile def tempfile_create(note, raw=False, tempdir=None): + """create a temp file""" if raw: # dump the raw json of the note - tf = tempfile.NamedTemporaryFile(suffix='.json', delete=False, dir=tempdir) + tfile = tempfile.NamedTemporaryFile(suffix='.json', + delete=False, dir=tempdir) contents = json.dumps(note, indent=2) - tf.write(contents.encode('utf-8')) - tf.flush() + tfile.write(contents.encode('utf-8')) + tfile.flush() else: ext = '.mkd' - tf = tempfile.NamedTemporaryFile(suffix=ext, delete=False, dir=tempdir) + tfile = tempfile.NamedTemporaryFile(suffix=ext, delete=False, + dir=tempdir) if note: contents = note['content'] - tf.write(contents.encode('utf-8')) - tf.flush() - return tf + tfile.write(contents.encode('utf-8')) + tfile.flush() + return tfile -def tempfile_delete(tf): - if tf: - tf.close() - os.unlink(tf.name) +def tempfile_delete(tfile): + """delete a temp file""" + if tfile: + tfile.close() + os.unlink(tfile.name) -def tempfile_name(tf): - if tf: - return tf.name +def tempfile_name(tfile): + """get the name of a temp file""" + if tfile: + return tfile.name return '' -def tempfile_content(tf): +def tempfile_content(tfile): + """read the contents of the temp file""" # This 'hack' is needed because some editors use an intermediate temporary # file, and rename it to that of the correct file, overwriting it. This # means that the tf file handle won't be updated with the new contents, and # the tempfile must be re-opened and read - if not tf: + if not tfile: return None - with open(tf.name, 'rb') as f: - updated_tf_contents = f.read() + with open(tfile.name, 'rb') as temp: + updated_tf_contents = temp.read() return updated_tf_contents.decode('utf-8') diff --git a/nncli/user_input.py b/nncli/user_input.py @@ -1,12 +1,13 @@ # -*- coding: utf-8 -*- - +"""user_input module""" import urwid +# pylint: disable=too-many-arguments class UserInput(urwid.Edit): - + """UserInput class""" def __init__(self, config, caption, edit_text, callback_func, args): self.config = config - self.callback_func = callback_func + self.callback_func = callback_func self.callback_func_args = args super(UserInput, self).__init__(caption=caption, edit_text=edit_text, diff --git a/nncli/utils.py b/nncli/utils.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- - -import datetime +"""utils module""" import random import re import shlex @@ -10,6 +9,7 @@ from . import temp +# pylint: disable=too-many-arguments,too-few-public-methods def get_editor(config, logger): """Get the editor""" editor = config.get_config('editor') @@ -87,16 +87,21 @@ def generate_random_key(): return '%030x' % (random.randrange(256**15),) def get_note_category(note): + """get a note category""" if 'category' in note: category = note['category'] if note['category'] is not None else '' else: category = '' return category -# Returns a fixed length string: -# 'X' - needs sync -# '*' - favorite def get_note_flags(note): + """ + get the note flags + + Returns a fixed length string: + 'X' - needs sync + '*' - favorite + """ flags = '' flags += 'X' if float(note['modified']) > float(note['syncdate']) else ' ' if 'favorite' in note: @@ -106,30 +111,40 @@ def get_note_flags(note): return flags def get_note_title(note): + """get the note title""" if 'title' in note: return note['title'] - else: - return '' + return '' -def note_favorite(n): - if 'favorite' in n: - return n['favorite'] - else: - return False +def note_favorite(note): + """ + get the status of the note as a favorite -def sort_by_title_favorite(a): - return (not note_favorite(a.note), get_note_title(a.note)) + returns True if the note is marked as a favorite + False otherwise + """ + if 'favorite' in note: + return note['favorite'] + return False + +def sort_by_title_favorite(left): + """sort notes by title, favorites on top""" + return (not note_favorite(left.note), get_note_title(left.note)) def sort_notes_by_categories(notes, favorite_ontop=False): + """ + sort notes by category, optionally pushing favorites to the + top + """ notes.sort(key=lambda i: (favorite_ontop and not note_favorite(i.note), i.note.get('category'), get_note_title(i.note))) -def sort_by_modify_date_favorite(a): - if note_favorite(a.note): - return 100.0 * float(a.note.get('modified', 0)) - else: - return float(a.note.get('modified', 0)) +def sort_by_modify_date_favorite(left): + """sort notest by modify date, favorites on top""" + if note_favorite(left.note): + return 100.0 * float(left.note.get('modified', 0)) + return float(left.note.get('modified', 0)) class KeyValueObject: """Store key=value pairs in this object and retrieve with o.key. @@ -155,7 +170,8 @@ def build_regex_search(search_string): } if search_string: try: - search_string, flag_letters = re.match(r'^(.+?)(?:/([a-z]+))?$', search_string).groups() + search_string, flag_letters = \ + re.match(r'^(.+?)(?:/([a-z]+))?$', search_string).groups() flags = 0 # if flags are given, OR together all the valid flags # see https://docs.python.org/3/library/re.html#re.compile diff --git a/nncli/view_help.py b/nncli/view_help.py @@ -1,19 +1,22 @@ # -*- coding: utf-8 -*- - -import re, urwid +"""view_help module""" +import re +import urwid class ViewHelp(urwid.ListBox): - + """ViewHelp class""" def __init__(self, config): self.config = config - self.descr_width = 26 + self.descr_width = 26 self.config_width = 29 lines = [] 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_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', ''))) @@ -21,10 +24,11 @@ def __init__(self, config): super(ViewHelp, self).__init__(urwid.SimpleFocusListWalker(lines)) def get_status_bar(self): - cur = -1 + """get the status bar""" + cur = -1 total = 0 - if len(self.body.positions()) > 0: - cur = self.focus_position + if self.body.positions(): + cur = self.focus_position total = len(self.body.positions()) status_title = \ @@ -38,87 +42,123 @@ def get_status_bar(self): str(total)), 'status_bar')) return \ - urwid.AttrMap(urwid.Columns([ status_title, status_index ]), + urwid.AttrMap(urwid.Columns([status_title, status_index]), 'status_bar') def create_kb_help_lines(self, header, use): - lines = [ urwid.AttrMap(urwid.Text(''), - 'help_header', - 'help_focus') ] + """create the help page for the keybindings""" + lines = [urwid.AttrMap(urwid.Text(''), + 'help_header', + 'help_focus')] lines.append(urwid.AttrMap(urwid.Text(' ' + header), 'help_header', 'help_focus')) - for c in self.config.keybinds: - if use not in self.config.get_keybind_use(c): + for config in self.config.keybinds: + if use not in self.config.get_keybind_use(config): continue - lines.append( - urwid.AttrMap(urwid.AttrMap( - urwid.Text( + keybinds_text = urwid.Text( [ - ('help_descr', ('{:>' + str(self.descr_width) + '} ').format(self.config.get_keybind_descr(c))), - ('help_config', ('{:>' + str(self.config_width) + '} ').format('kb_' + c)), - ('help_value', "'" + self.config.get_keybind(c) + "'") - ] - ), - attr_map = None, - focus_map = { - 'help_value' : 'help_focus', - 'help_config' : 'help_focus', - 'help_descr' : 'help_focus' - } - ), 'default', 'help_focus')) + ( + 'help_descr', + ( + '{:>' + str(self.descr_width) + '} ' + ).format( + self.config.get_keybind_descr( + config + ) + ) + ), + ( + 'help_config', + ( + '{:>' + str(self.config_width) \ + + '} ' + ).format('kb_' + config) + ), + ( + 'help_value', + "'" + + self.config.get_keybind(config) + "'" + ) + ]) + lines.append( + urwid.AttrMap( + urwid.AttrMap( + keybinds_text, + attr_map=None, + focus_map= \ + { + 'help_value': 'help_focus', + 'help_config' : 'help_focus', + 'help_descr' : 'help_focus' + }), + 'default', 'help_focus')) return lines def create_config_help_lines(self): - lines = [ urwid.AttrMap(urwid.Text(''), - 'help_header', - 'help_focus') ] + """create the help lines for the general config settings""" + lines = [urwid.AttrMap(urwid.Text(''), + 'help_header', + 'help_focus')] lines.append(urwid.AttrMap(urwid.Text(' Configuration'), 'help_header', 'help_focus')) - for c in self.config.configs: - if c in [ 'nn_username', 'nn_password' ]: continue - lines.append( - urwid.AttrMap(urwid.AttrMap( - urwid.Text( + for config in self.config.configs: + if config in ['nn_username', 'nn_password']: + continue + config_text = urwid.Text( [ - ('help_descr', ('{:>' + str(self.descr_width) + '} ').format(self.config.get_config_descr(c))), - ('help_config', ('{:>' + str(self.config_width) + '} ').format('cfg_' + c)), - ('help_value', "'" + str(self.config.get_config(c)) + "'") - ] - ), - attr_map = None, - focus_map = { - 'help_value' : 'help_focus', - 'help_config' : 'help_focus', - 'help_descr' : 'help_focus' - } - ), 'default', 'help_focus')) + ('help_descr', + ('{:>' + str(self.descr_width) + '} '). + format(self.config.get_config_descr(config))), + ('help_config', + ('{:>' + str(self.config_width) + '} '). + format('cfg_' + config)), + ('help_value', + "'" + + str(self.config.get_config(config)) + "'") + ]) + lines.append( + urwid.AttrMap(urwid.AttrMap( + config_text, + attr_map=None, + focus_map={ + 'help_value' : 'help_focus', + 'help_config' : 'help_focus', + 'help_descr' : 'help_focus' + } + ), 'default', 'help_focus')) return lines def create_color_help_lines(self): - lines = [ urwid.AttrMap(urwid.Text(''), - 'help_header', - 'help_focus') ] + """create the help lines for the color settings""" + lines = [urwid.AttrMap(urwid.Text(''), + 'help_header', + 'help_focus')] lines.append(urwid.AttrMap(urwid.Text(' Colors'), 'help_header', 'help_focus')) fmap = {} - for c in self.config.colors: - fmap[re.search('^(.*)(_fg|_bg)$', c).group(1)] = 'help_focus' - for c in self.config.colors: - lines.append( - urwid.AttrMap(urwid.AttrMap( - urwid.Text( + for config in self.config.colors: + fmap[re.search('^(.*)(_fg|_bg)$', config).group(1)] = 'help_focus' + for color in self.config.colors: + colors_text = urwid.Text( [ - ('help_descr', ('{:>' + str(self.descr_width) + '} ').format(self.config.get_color_descr(c))), - ('help_config', ('{:>' + str(self.config_width) + '} ').format('clr_' + c)), - (re.search('^(.*)(_fg|_bg)$', c).group(1), "'" + self.config.get_color(c) + "'") - ] - ), - attr_map = None, - focus_map = fmap - ), 'default', 'help_focus')) + ('help_descr', + ('{:>' + str(self.descr_width) + '} '). + format(self.config.get_color_descr(color))), + ('help_config', + ('{:>' + str(self.config_width) + '} '). + format('clr_' + color)), + (re.search('^(.*)(_fg|_bg)$', color).group(1), + "'" + self.config.get_color(color) + "'") + ]) + lines.append( + urwid.AttrMap(urwid.AttrMap( + colors_text, + attr_map=None, + focus_map=fmap + ), 'default', 'help_focus')) return lines def keypress(self, size, key): diff --git a/nncli/view_log.py b/nncli/view_log.py @@ -1,32 +1,38 @@ # -*- coding: utf-8 -*- - +"""view_log module""" import urwid class ViewLog(urwid.ListBox): + """ + ViewLog class + This class defines the urwid view class for the log viewer + """ def __init__(self, config): self.config = config super(ViewLog, self).__init__(urwid.SimpleFocusListWalker([])) def update_log(self): + """update the log""" lines = [] - f = open(self.config.logfile) - for line in f: - lines.append( - urwid.AttrMap(urwid.Text(line.rstrip()), - 'note_content', - 'note_content_focus')) - f.close() + with open(self.config.logfile) as logfile: + for line in logfile: + lines.append( + urwid.AttrMap(urwid.Text(line.rstrip()), + 'note_content', + 'note_content_focus') + ) if self.config.get_config('log_reversed') == 'yes': lines.reverse() self.body[:] = urwid.SimpleFocusListWalker(lines) self.focus_position = 0 def get_status_bar(self): - cur = -1 + """get the log view status bar""" + cur = -1 total = 0 - if len(self.body.positions()) > 0: - cur = self.focus_position + if self.body.positions(): + cur = self.focus_position total = len(self.body.positions()) status_title = \ @@ -40,7 +46,7 @@ def get_status_bar(self): str(total)), 'status_bar')) return \ - urwid.AttrMap(urwid.Columns([ status_title, status_index ]), + urwid.AttrMap(urwid.Columns([status_title, status_index]), 'status_bar') def keypress(self, size, key): diff --git a/nncli/view_note.py b/nncli/view_note.py @@ -1,13 +1,18 @@ # -*- coding: utf-8 -*- - -import time, urwid +"""view_note module""" +import time +import urwid from . import utils -import re from .clipboard import Clipboard -import logging +# pylint: disable=too-many-instance-attributes class ViewNote(urwid.ListBox): + """ + ViewNote class + This class defines the urwid class responsible for displaying an + individual note in an internal pager + """ def __init__(self, config, args): self.config = config self.ndb = args['ndb'] @@ -21,31 +26,35 @@ def __init__(self, config, args): self.tabstop = int(self.config.get_config('tabstop')) self.clipboard = Clipboard() super(ViewNote, self).__init__( - urwid.SimpleFocusListWalker(self.get_note_content_as_list())) + urwid.SimpleFocusListWalker(self.get_note_content_as_list())) def get_note_content_as_list(self): + """return the contents of a note as a list of strings""" lines = [] if not self.key: return lines if self.old_note: - for l in self.old_note['content'].split('\n'): + for line in self.old_note['content'].split('\n'): lines.append( - urwid.AttrMap(urwid.Text(l.replace('\t', ' ' * self.tabstop)), - 'note_content_old', - 'note_content_old_focus')) + urwid.AttrMap(urwid.Text( + line.replace('\t', ' ' * self.tabstop)), + 'note_content_old', + 'note_content_old_focus')) else: - for l in self.note['content'].split('\n'): + for line in self.note['content'].split('\n'): lines.append( - urwid.AttrMap(urwid.Text(l.replace('\t', ' ' * self.tabstop)), - 'note_content', - 'note_content_focus')) + urwid.AttrMap(urwid.Text( + line.replace('\t', ' ' * self.tabstop)), + 'note_content', + 'note_content_focus')) lines.append(urwid.AttrMap(urwid.Divider('-'), 'default')) return lines - def update_note_view(self, key=None, version=None): + def update_note_view(self, key=None): + """update the view""" if key: # setting a new note - self.key = key - self.note = self.ndb.get_note(self.key) + self.key = key + self.note = self.ndb.get_note(self.key) self.old_note = None self.body[:] = \ @@ -54,63 +63,80 @@ def update_note_view(self, key=None, version=None): self.focus_position = 0 def lines_after_current_position(self): - lines_after_current_position = list(range(self.focus_position + 1, len(self.body.positions()) - 1)) + """ + return the number of lines after the currently-focused + line + """ + 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): + """ + return the number of lines before the currently-focused line + """ lines_before_current_position = list(range(0, self.focus_position)) lines_before_current_position.reverse() return lines_before_current_position def search_note_view_next(self, search_string=None, search_mode=None): + """move to the next match in search mode""" if search_string: self.search_string = search_string if search_mode: self.search_mode = search_mode - note_range = self.lines_after_current_position() if self.search_direction == 'forward' else self.lines_before_current_position() + note_range = self.lines_after_current_position() \ + if self.search_direction == 'forward' \ + else self.lines_before_current_position() self.search_note_range(note_range) def search_note_view_prev(self, search_string=None, search_mode=None): + """move to the previous match in search mode""" if search_string: self.search_string = search_string if search_mode: self.search_mode = search_mode - note_range = self.lines_after_current_position() if self.search_direction == 'backward' else self.lines_before_current_position() + note_range = self.lines_after_current_position() \ + if self.search_direction == 'backward' \ + else self.lines_before_current_position() self.search_note_range(note_range) def search_note_range(self, note_range): + """search within a range of lines""" for line in note_range: line_content = self.note['content'].split('\n')[line] - if (self.is_match(self.search_string, line_content)): + if self.is_match(self.search_string, line_content): self.focus_position = line break self.update_note_view() def is_match(self, term, full_text): + """returns True if there is a match, False otherwise""" if self.search_mode == 'gstyle': return term in full_text - else: - sspat = utils.build_regex_search(term) - return sspat and sspat.search(full_text) + sspat = utils.build_regex_search(term) + return sspat and sspat.search(full_text) def get_status_bar(self): + """get the note view status bar""" if not self.key: return \ urwid.AttrMap(urwid.Text('No note...'), 'status_bar') - cur = -1 + cur = -1 total = 0 - if len(self.body.positions()) > 0: - cur = self.focus_position + if self.body.positions(): + cur = self.focus_position total = len(self.body.positions()) - t = time.localtime(float(self.note['modified'])) - title = utils.get_note_title(self.note) - flags = utils.get_note_flags(self.note) + localtime = time.localtime(float(self.note['modified'])) + title = utils.get_note_title(self.note) + flags = utils.get_note_flags(self.note) category = utils.get_note_category(self.note) - mod_time = time.strftime('Date: %a, %d %b %Y %H:%M:%S', t) + mod_time = time.strftime('Date: %a, %d %b %Y %H:%M:%S', localtime) status_title = \ urwid.AttrMap(urwid.Text('Title: ' + @@ -140,14 +166,15 @@ def get_status_bar(self): ']'), 'status_bar')) - pile_top = urwid.Columns([ status_title, status_key_index ]) - pile_bottom = urwid.Columns([ status_date, status_category_flags ]) + pile_top = urwid.Columns([status_title, status_key_index]) + pile_bottom = urwid.Columns([status_date, status_category_flags]) return \ - urwid.AttrMap(urwid.Pile([ pile_top, pile_bottom ]), + urwid.AttrMap(urwid.Pile([pile_top, pile_bottom]), 'status_bar') def copy_note_text(self): + """copy the text of the note to the system clipboard""" line_content = self.note['content'].split('\n')[self.focus_position] self.clipboard.copy(line_content) diff --git a/nncli/view_titles.py b/nncli/view_titles.py @@ -1,32 +1,48 @@ # -*- coding: utf-8 -*- - -import re, time, datetime, urwid, subprocess -from . import utils, view_note - +"""view_titles module""" +import re +import time +import datetime +import urwid +from . import utils + +# pylint: disable=too-many-instance-attributes, too-many-statements class ViewTitles(urwid.ListBox): + """ + ViewTitles class + Implements the urwid class for the view_titles view + """ def __init__(self, config, args): self.config = config self.ndb = args['ndb'] self.search_string = args['search_string'] self.log = args['log'] self.note_list, self.match_regex, self.all_notes_cnt = \ - self.ndb.filter_notes(self.search_string, sort_mode=self.config.get_config('sort_mode')) + self.ndb.filter_notes( + self.search_string, + sort_mode=self.config.get_config('sort_mode') + ) super(ViewTitles, self).__init__( - urwid.SimpleFocusListWalker(self.get_note_titles())) + urwid.SimpleFocusListWalker(self.get_note_titles())) - def update_note_list(self, search_string, search_mode='gstyle', sort_mode='date'): + def update_note_list(self, search_string, + search_mode='gstyle', sort_mode='date'): + """update the note list""" self.search_string = search_string self.note_list, self.match_regex, self.all_notes_cnt = \ - self.ndb.filter_notes(self.search_string, search_mode, sort_mode=sort_mode) + self.ndb.filter_notes( + self.search_string, search_mode, sort_mode=sort_mode + ) self.body[:] = \ urwid.SimpleFocusListWalker(self.get_note_titles()) - if len(self.note_list) == 0: + if self.note_list: self.log('No notes found!') else: self.focus_position = 0 def sort_note_list(self, sort_mode): + """sort the note list""" self.ndb.filtered_notes_sort(self.note_list, sort_mode) self.body[:] = \ urwid.SimpleFocusListWalker(self.get_note_titles()) @@ -44,21 +60,23 @@ def format_title(self, note): %N -- note title """ - t = time.localtime(float(note['modified'])) - mod_time = time.strftime(self.config.get_config('format_strftime'), t) + localtime = time.localtime(float(note['modified'])) + mod_time = \ + time.strftime(self.config.get_config('format_strftime'), + localtime) title = utils.get_note_title(note) flags = utils.get_note_flags(note) - category = utils.get_note_category(note) + category = utils.get_note_category(note) # get the age of the note - dt = datetime.datetime.fromtimestamp(time.mktime(t)) - if dt > datetime.datetime.now() - datetime.timedelta(days=1): + dtime = datetime.datetime.fromtimestamp(time.mktime(localtime)) + if dtime > datetime.datetime.now() - datetime.timedelta(days=1): note_age = 'd' # less than a day old - elif dt > datetime.datetime.now() - datetime.timedelta(weeks=1): + elif dtime > datetime.datetime.now() - datetime.timedelta(weeks=1): note_age = 'w' # less than a week old - elif dt > datetime.datetime.now() - datetime.timedelta(weeks=4): + elif dtime > datetime.datetime.now() - datetime.timedelta(weeks=4): note_age = 'm' # less than a month old - elif dt > datetime.datetime.now() - datetime.timedelta(weeks=52): + elif dtime > datetime.datetime.now() - datetime.timedelta(weeks=52): note_age = 'y' # less than a year old else: note_age = 'a' # ancient @@ -66,86 +84,101 @@ def format_title(self, note): def recursive_format(title_format): if not title_format: return None - fmt = re.search("^(.*)%([-]*)([0-9]*)([FDTN])(.*)$", title_format) + fmt = re.search(r'^(.*)%([-]*)([0-9]*)([FDTN])(.*)$', title_format) if not fmt: - m = ('pack', urwid.AttrMap(urwid.Text(title_format), - 'default')) + attr_map = ('pack', urwid.AttrMap(urwid.Text(title_format), + 'default')) l_fmt = None r_fmt = None else: - l = fmt.group(1) if fmt.group(1) else None - m = None - r = fmt.group(5) if fmt.group(5) else None + left = fmt.group(1) if fmt.group(1) else None + attr_map = None + right = fmt.group(5) if fmt.group(5) else None align = 'left' if fmt.group(2) == '-' else 'right' width = int(fmt.group(3)) if fmt.group(3) else 'pack' if fmt.group(4) == 'F': - m = (width, urwid.AttrMap(urwid.Text(flags, - align=align, - wrap='clip'), - 'note_flags')) + attr_map = (width, urwid.AttrMap(urwid.Text(flags, + align=align, + wrap='clip'), + 'note_flags')) elif fmt.group(4) == 'D': - m = (width, urwid.AttrMap(urwid.Text(mod_time, - align=align, - wrap='clip'), - 'note_date')) + attr_map = (width, urwid.AttrMap(urwid.Text(mod_time, + align=align, + wrap='clip'), + 'note_date')) elif fmt.group(4) == 'T': - m = (width, urwid.AttrMap(urwid.Text(category, - align=align, - wrap='clip'), - 'note_category')) + attr_map = (width, urwid.AttrMap(urwid.Text(category, + align=align, + wrap='clip'), + 'note_category')) elif fmt.group(4) == 'N': - if note_age == 'd': attr = 'note_title_day' - elif note_age == 'w': attr = 'note_title_week' - elif note_age == 'm': attr = 'note_title_month' - elif note_age == 'y': attr = 'note_title_year' - elif note_age == 'a': attr = 'note_title_ancient' + if note_age == 'd': + attr = 'note_title_day' + elif note_age == 'w': + attr = 'note_title_week' + elif note_age == 'm': + attr = 'note_title_month' + elif note_age == 'y': + attr = 'note_title_year' + elif note_age == 'a': + attr = 'note_title_ancient' if width != 'pack': - m = (width, urwid.AttrMap(urwid.Text(title, - align=align, - wrap='clip'), - attr)) + attr_map = (width, urwid.AttrMap( + urwid.Text( + title, + align=align, + wrap='clip' + ), + attr)) else: - m = urwid.AttrMap(urwid.Text(title, - align=align, - wrap='clip'), - attr) - l_fmt = recursive_format(l) - r_fmt = recursive_format(r) + attr_map = urwid.AttrMap(urwid.Text(title, + align=align, + wrap='clip'), + attr) + l_fmt = recursive_format(left) + r_fmt = recursive_format(right) tmp = [] - if l_fmt: tmp.extend(l_fmt) - tmp.append(m) - if r_fmt: tmp.extend(r_fmt) + if l_fmt: + tmp.extend(l_fmt) + tmp.append(attr_map) + if r_fmt: + tmp.extend(r_fmt) return tmp # convert the format string into the actual note title line - title_line = recursive_format(self.config.get_config('format_note_title')) + title_line = recursive_format( + self.config.get_config('format_note_title') + ) return urwid.Columns(title_line) def get_note_title(self, note): + """get the title of a note""" return urwid.AttrMap(self.format_title(note), 'default', - { 'default' : 'note_focus', - 'note_title_day' : 'note_focus', - 'note_title_week' : 'note_focus', - 'note_title_month' : 'note_focus', - 'note_title_year' : 'note_focus', - 'note_title_ancient' : 'note_focus', - 'note_date' : 'note_focus', - 'note_flags' : 'note_focus', - 'note_categories' : 'note_focus' }) + {'default' : 'note_focus', + 'note_title_day' : 'note_focus', + 'note_title_week' : 'note_focus', + 'note_title_month' : 'note_focus', + 'note_title_year' : 'note_focus', + 'note_title_ancient' : 'note_focus', + 'note_date' : 'note_focus', + 'note_flags' : 'note_focus', + 'note_categories' : 'note_focus'}) def get_note_titles(self): + """get the titles of all of the notes""" lines = [] - for n in self.note_list: - lines.append(self.get_note_title(n.note)) + for note in self.note_list: + lines.append(self.get_note_title(note.note)) return lines def get_status_bar(self): - cur = -1 + """get the status bar""" + cur = -1 total = 0 - if len(self.body.positions()) > 0: - cur = self.focus_position + if self.body.positions(): + cur = self.focus_position total = len(self.body.positions()) hdr = 'NextCloud Notes' @@ -153,7 +186,7 @@ def get_status_bar(self): # include connection status in header hdr += ' (' + self.ndb.note.status + ')' - if self.search_string != None: + if self.search_string is not None: hdr += ' - Search: ' + self.search_string status_title = \ @@ -167,10 +200,11 @@ def get_status_bar(self): str(total)), 'status_bar')) return \ - urwid.AttrMap(urwid.Columns([ status_title, status_index ]), + urwid.AttrMap(urwid.Columns([status_title, status_index]), 'status_bar') def update_note_title(self, key=None): + """update a note title""" if not key: self.body[self.focus_position] = \ self.get_note_title(self.note_list[self.focus_position].note) @@ -180,6 +214,7 @@ def update_note_title(self, key=None): self.body[i] = self.get_note_title(self.note_list[i].note) def focus_note(self, key): + """set the focus on a given note""" for i in range(len(self.note_list)): if 'localkey' in self.note_list[i].note and \ self.note_list[i].note['localkey'] == key: