nncli

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

commit a8041335ed93a8c2675abdd7ffccde345582fa34
parent f8a9dcf39937b2bdc6db3b954a507bd09c48748a
Author: Daniel Moch <daniel@danielmoch.com>
Date:   Fri,  7 Sep 2018 12:24:14 -0400

Move to click for command-line parsing

Closes #4

Diffstat:
MPipfile | 1+
MPipfile.lock | 16++++++++++++----
Mnncli/__main__.py | 4++--
Anncli/cli.py | 242+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnncli/nncli.py | 202++-----------------------------------------------------------------------------
Mpyproject.toml | 4++--
Mtests/test_nncli.py | 14+++++++-------
7 files changed, 270 insertions(+), 213 deletions(-)

diff --git a/Pipfile b/Pipfile @@ -7,6 +7,7 @@ name = "pypi" appdirs = "*" requests = "*" urwid = "*" +click = "*" [dev-packages] pytest = "*" diff --git a/Pipfile.lock b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a99ad6723213e0eb2b128bc8cc247fcf543fe0881dcf614f7c942e0572ae0723" + "sha256": "44a70f798cfaf69e97a5d6d1dfc9d4e7f19feb48ae8f32cbe55b2d34f3083731" }, "pipfile-spec": 6, "requires": {}, @@ -36,6 +36,14 @@ ], "version": "==3.0.4" }, + "click": { + "hashes": [ + "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", + "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b" + ], + "index": "pypi", + "version": "==6.7" + }, "idna": { "hashes": [ "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", @@ -419,11 +427,11 @@ }, "sphinx": { "hashes": [ - "sha256:a07050845cc9a2f4026a6035cc8ed795a5ce7be6528bbc82032385c10807dfe7", - "sha256:d719de667218d763e8fd144b7fcfeefd8d434a6201f76bf9f0f0c1fa6f47fcdb" + "sha256:217a7705adcb573da5bbe1e0f5cab4fa0bd89fd9342c9159121746f593c2d5a4", + "sha256:a602513f385f1d5785ff1ca420d9c7eb1a1b63381733b2f0ea8188a391314a86" ], "index": "pypi", - "version": "==1.7.8" + "version": "==1.7.9" }, "sphinxcontrib-websupport": { "hashes": [ diff --git a/nncli/__main__.py b/nncli/__main__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """nncli main module""" -import nncli.nncli +import nncli.cli if __name__ == '__main__': - nncli.nncli.main() + nncli.cli.main() diff --git a/nncli/cli.py b/nncli/cli.py @@ -0,0 +1,242 @@ +# -*- coding: utf-8 -*- +"""Command line interface module""" +import click + +from . import __version__ +from .nncli import Nncli + +class StdinFlag(click.ParamType): + """StdinFlag Click Parameter Type""" + name = "stdin_flag" + + def convert(self, value, param, ctx): + if value == '-': + return True + return self.fail('%s is not a valid stdin_flag') + +STDIN_FLAG = StdinFlag() + +@click.command() +@click.pass_obj +def rm_category(ctx_obj): + """Remove note category.""" + nncli = ctx_obj['nncli'] + key = ctx_obj['key'] + nncli.cli_note_category_rm(key) + +@click.command() +@click.argument('category', required=True) +@click.pass_obj +def set_category(ctx_obj, category): + """Set the note category.""" + nncli = ctx_obj['nncli'] + key = ctx_obj['key'] + nncli.cli_note_category_set(key, category) + +@click.command(short_help="Print the note category.") +@click.pass_obj +def get_category(ctx_obj): + """Print the category for the given note on stdout.""" + nncli = ctx_obj['nncli'] + key = ctx_obj['key'] + category = nncli.cli_note_category_get(key) + if category: + print(category) + +@click.group() +@click.option( + '-k', + '--key', + required=True, + type=click.INT, + help="Specify the note key." + ) +@click.pass_context +def cat(ctx, key): + """Operate on the note category.""" + nncli = ctx.obj + ctx.obj = {} + ctx.obj['nncli'] = nncli + ctx.obj['key'] = key + +cat.add_command(get_category, 'get') +cat.add_command(set_category, 'set') +cat.add_command(rm_category, 'rm') + +@click.command() +@click.option( + '-k', + '--key', + required=True, + type=click.INT, + help="Specify the note key.") +@click.pass_obj +def favorite(nncli, key): + """Mark as note as a favorite.""" + nncli.cli_note_favorite(key, 1) + +@click.command() +@click.option( + '-k', + '--key', + required=True, + type=click.INT, + help="Specify the note key." + ) +@click.pass_obj +def unfavorite(nncli, key): + """Remove favorite flag from a note.""" + nncli.cli_note_favorite(key, 0) + +@click.command(short_help="Print JSON-formatted note to stdout.") +@click.option('-k', '--key', type=click.INT, help="Specify the note key.") +@click.option( + '-r', + '--regex', + is_flag=True, + help="Treat search term(s) as regular expressions." + ) +@click.argument('search_terms', nargs=-1) +@click.pass_obj +def export(nncli, key, regex, search_terms): + """ + Print JSON-formatted note to stdout. If a key is specified, then regex + and search_terms are ignored. + """ + if key: + nncli.cli_note_export(key) + else: + nncli.cli_export_notes(regex, ' '.join(search_terms)) + +@click.command(short_help="Print note contents to stdout.") +@click.option('-k', '--key', type=click.INT, help="Specify the note key.") +@click.option( + '-r', + '--regex', + is_flag=True, + help="Treat search term(s) as regular expressions." + ) +@click.argument('search_terms', nargs=-1) +@click.pass_obj +def dump(nncli, key, regex, search_terms): + """ + Print note contents to stdout. If a key is specified, then regex + and search_terms are ignored. + """ + if key: + nncli.cli_note_dump(key) + else: + nncli.cli_dump_notes(regex, ' '.join(search_terms)) + +@click.command(short_help="List notes.") +@click.option( + '-r', + '--regex', + is_flag=True, + help="Treat search term(s) as regular expressions." + ) +@click.argument('search_terms', nargs=-1) +@click.pass_obj +def list_notes(nncli, regex, search_terms): + """ + List notes, optionally providing search terms to narrow the + results. + """ + nncli.cli_list_notes(regex, ' '.join(search_terms)) + +@click.command(short_help="Sync notes to server.") +def sync(): + """ + Perform a full, bi-directional sync of your notes between the + server and the local cache. + """ + pass + +@click.command() +@click.option( + '-k', + '--key', + required=True, + type=click.INT, + help="Specify the note key." + ) +@click.pass_obj +def delete(nncli, key): + """Delete an existing note.""" + nncli.cli_note_delete(key, True) + +@click.command() +@click.option( + '-k', + '--key', + required=True, + type=click.INT, + help="Specify the note key." + ) +@click.pass_obj +def edit(nncli, key): + """Edit an existing note.""" + nncli.cli_note_edit(key) + +@click.command(short_help="Import a JSON note.") +@click.argument('from_stdin', metavar='[-]', type=STDIN_FLAG) +@click.pass_obj +def json_import(nncli, from_stdin): + """ + Import a JSON-formatted note file into your account. The expected + JSON format is the same format used internally by nncli. If - is + specified, the note is read from stdin, otherwise the editor will + open. + """ + nncli.cli_note_import(from_stdin) + +@click.command(short_help="Add a new note.") +@click.option('-t', '--title', help="Specify the title of note for create.") +@click.argument('from_stdin', metavar='[-]', type=STDIN_FLAG) +@click.pass_obj +def create(nncli, title, from_stdin): + """ + Create a new note, either opening the editor or, if - is specified, + reading from stdin. + """ + nncli.cli_note_create(from_stdin, title) + +@click.group(invoke_without_command=True) +@click.option( + '-n', + '--nosync', + is_flag=True, + help="Don't perform a server sync." + ) +@click.option('-v', '--verbose', is_flag=True, help="Print verbose output.") +@click.option( + '-c', + '--config', + type=click.Path(exists=True), + help="Specify the config file to read from." + ) +@click.option('-k', '--key', type=click.INT, help="Specify the note key.") +@click.version_option(version=__version__, message='%(prog)s %(version)s') +@click.pass_context +def main(ctx, nosync, verbose, config, key): + """ + Run the NextClound Note Command Line Interface. No COMMAND means + to open the console GUI. + """ + ctx.obj = Nncli(not nosync, verbose, config) + if ctx.invoked_subcommand is None: + ctx.obj.gui(key) + elif not nosync: + ctx.obj.sync_notes() + +main.add_command(create) +main.add_command(edit) +main.add_command(delete) +main.add_command(sync) +main.add_command(json_import, name='import') +main.add_command(list_notes, name='list') +main.add_command(dump) +main.add_command(export) +main.add_command(favorite) +main.add_command(unfavorite) +main.add_command(cat) diff --git a/nncli/nncli.py b/nncli/nncli.py @@ -10,7 +10,7 @@ from .notes_db import NotesDB, ReadError, WriteError from logging.handlers import RotatingFileHandler -class nncli: +class Nncli: def __init__(self, do_server_sync, verbose=False, config_file=None): self.config = Config(config_file) @@ -124,8 +124,9 @@ def exec_cmd_on_note(self, note, cmd=None, raw=False): temp.tempfile_delete(tf) - self.nncli_loop.screen.clear() - self.nncli_loop.draw_screen() + if self.do_gui: + self.nncli_loop.screen.clear() + self.nncli_loop.draw_screen() return content @@ -1059,198 +1060,3 @@ def SIGINT_handler(signum, frame): sys.exit(1) signal.signal(signal.SIGINT, SIGINT_handler) - -def usage(): - print (''' -Usage: - nncli [OPTIONS] [COMMAND] [COMMAND_ARGS] - - OPTIONS: - -h, --help - usage help - -v, --verbose - verbose output - -n, --nosync - don't perform a server sync - -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 - -V, --version - version information - - COMMANDS: - <none> - console gui mode when no command specified - sync - perform a full sync with the server - list [search_string] - list notes (refined with search string) - export [search_string] - export notes in JSON (refined with search - string) - dump [search_string] - dump notes (refined with search string) - create [-] - create a note ('-' content from stdin) - import [-] - import a note in JSON format ('-' JSON from - stdin) - export - export a note in JSON format (specified by - <key>) - dump - dump a note (specified by <key>) - edit - edit a note (specified by <key>) - delete - delete a note (specified by <key>) - < favorite | unfavorite > - favorite/unfavorite a note (specified by <key>) - cat get - retrieve the category from a note (specified - by <key>) - cat set <category> - set the category for a note (specified by <key>) - cat rm - remove category from a note (specified by <key>) -''') - sys.exit(0) - -def version(): - version_info = 'nncli {}'.format(__version__) - print(version_info) - exit(0) - -def main(argv=sys.argv[1:]): - verbose = False - sync = True - regex = False - key = None - title = None - config = None - - try: - opts, args = getopt.getopt(argv, - 'hvnrk:t:c:V', - [ 'help', 'verbose', 'nosync', 'regex', 'key=', 'title=', \ - 'config=', 'version' ]) - except: - usage() - - for opt, arg in opts: - if opt in [ '-h', '--help']: - usage() - elif opt in ['-V', '--version' ]: - version() - elif opt in [ '-v', '--verbose']: - verbose = True - elif opt in [ '-n', '--nosync']: - sync = False - elif opt in [ '-r', '--regex']: - regex = True - elif opt in [ '-k', '--key']: - try: - key = int(arg) - except: - print('ERROR: Key specified with -k must be an integer') - elif opt in [ '-t', '--title']: - title = arg - elif opt in [ '-c', '--config']: - config = arg - else: - print('ERROR: Unhandled option') - usage() - - if not args: - nncli(sync, verbose, config).gui(key) - return - - def nncli_start(sync=sync, verbose=verbose, config=config): - sn = nncli(sync, verbose, config) - if sync: sn.sync_notes() - return sn - - if args[0] == 'sync': - sn = nncli_start(True) - - elif args[0] == 'list': - - sn = nncli_start() - sn.cli_list_notes(regex, ' '.join(args[1:])) - - elif args[0] == 'dump': - - sn = nncli_start() - if key: - sn.cli_note_dump(key) - else: - sn.cli_dump_notes(regex, ' '.join(args[1:])) - - elif args[0] == 'create': - - if len(args) == 1: - sn = nncli_start() - sn.cli_note_create(False, title) - elif len(args) == 2 and args[1] == '-': - sn = nncli_start() - sn.cli_note_create(True, title) - else: - usage() - - elif args[0] == 'import': - - if len(args) == 1: - sn = nncli_start() - sn.cli_note_import(False) - elif len(args) == 2 and args[1] == '-': - sn = nncli_start() - sn.cli_note_import(True) - else: - usage() - - elif args[0] == 'export': - - sn = nncli_start() - if key: - sn.cli_note_export(key) - else: - sn.cli_export_notes(regex, ' '.join(args[1:])) - - elif args[0] == 'edit': - - if not key: - usage() - - sn = nncli_start() - sn.cli_note_edit(key) - - elif args[0] == 'delete': - - if not key: - usage() - - sn = nncli_start() - sn.cli_note_delete(key, True) - - elif args[0] == 'favorite' or args[0] == 'unfavorite': - - if not key: - usage() - - sn = nncli_start() - sn.cli_note_favorite(key, 1 if args[0] == 'favorite' else 0) - - # Category API - elif args[0] == 'cat': - - if not key: - usage() - - nargs = len(args) - correct_other = (args[1] in ['get', 'rm'] and nargs == 2) - correct_set = (args[1] == 'set' and nargs == 3) - if not (correct_set or correct_other): - usage() - - if args[1] == 'get': - - sn = nncli_start() - category = sn.cli_note_category_get(key) - if category: - print(category) - - elif args[1] == 'set': - - category = args[2] - sn = nncli_start() - sn.cli_note_category_set(key, category) - - elif args[1] == 'rm': - - sn = nncli_start() - sn.cli_note_category_rm(key) - - else: - usage() diff --git a/pyproject.toml b/pyproject.toml @@ -8,7 +8,7 @@ author = "Daniel Moch" author-email = "daniel@danielmoch.com" home-page = "https://github.com/djmoch/nncli" description-file = "README.rst" -requires = ["urwid", "requests", "appdirs"] +requires = ["urwid", "requests", "appdirs", "click"] classifiers = ["License :: OSI Approved :: MIT License", "Development Status :: 4 - Beta", "Environment :: Console :: Curses", @@ -24,4 +24,4 @@ Documentation = "https://nncli.readthedocs.io/en/latest" dev = ["pipenv"] [tool.flit.scripts] -nncli = "nncli.nncli:main" +nncli = "nncli.cli:main" diff --git a/tests/test_nncli.py b/tests/test_nncli.py @@ -30,14 +30,14 @@ def assert_initialized(): def test_init_no_tempdir(mocker, mock_nncli): mock_get_config(mocker, ['what', '', 'duh', 'duh', 'duh']) - nn = nncli.nncli.nncli(False) + nn = nncli.nncli.Nncli(False) assert_initialized() assert nn.tempdir == None os.mkdir.assert_called_with('duh') def test_init(mocker, mock_nncli): mock_get_config(mocker, ['what', 'blah', 'duh', 'duh', 'duh']) - nn = nncli.nncli.nncli(False) + nn = nncli.nncli.Nncli(False) assert_initialized() assert nn.tempdir == 'blah' @@ -47,25 +47,25 @@ def test_init_notesdb_fail(mocker, mock_nncli): new=mocker.MagicMock(side_effect=SystemExit) ) with pytest.raises(SystemExit): - nn = nncli.nncli.nncli(False) + nn = nncli.nncli.Nncli(False) def test_get_editor(mocker, mock_nncli): mock_get_config(mocker, ['what', 'blah', 'duh', 'duh', 'duh', 'vim', '']) - nn = nncli.nncli.nncli(False) + nn = nncli.nncli.Nncli(False) assert_initialized() assert nn.get_editor() == 'vim' assert nn.get_editor() == None def test_get_pager(mocker, mock_nncli): mock_get_config(mocker, ['what', 'blah', 'duh', 'duh', 'duh', 'less', '']) - nn = nncli.nncli.nncli(False) + nn = nncli.nncli.Nncli(False) assert_initialized() assert nn.get_editor() == 'less' assert nn.get_editor() == None def test_get_diff(mocker, mock_nncli): mock_get_config(mocker, ['what', 'blah', 'duh', 'duh', 'duh', 'diff', '']) - nn = nncli.nncli.nncli(False) + nn = nncli.nncli.Nncli(False) assert_initialized() assert nn.get_editor() == 'diff' assert nn.get_editor() == None @@ -73,7 +73,7 @@ def test_get_diff(mocker, mock_nncli): @pytest.mark.skip def test_exec_cmd_on_note(mocker, mock_nncli): mocker.patch.object( - 'nncli.nncli.nncli', + 'nncli.nncli.Nncli', get_editor, new=mocker.MagicMock(return_value='vim')) mocker.patch('nncli.temp.tempfile_create')