diff options
-rw-r--r-- | .gitignore | 19 | ||||
-rw-r--r-- | Pipfile | 19 | ||||
-rw-r--r-- | Pipfile.lock | 287 | ||||
-rw-r--r-- | README.md | 22 | ||||
-rw-r--r-- | nnotes_cli/__init__.py | 11 | ||||
-rw-r--r-- | nnotes_cli/config.py | 27 | ||||
-rw-r--r-- | nnotes_cli/nextcloud_note.py | 45 | ||||
-rw-r--r-- | nnotes_cli/nncli.py | 38 | ||||
-rw-r--r-- | requirements.txt | 2 | ||||
-rw-r--r-- | setup.cfg | 12 | ||||
-rw-r--r--[-rwxr-xr-x] | setup.py | 8 | ||||
-rw-r--r-- | tests/test_config.py | 99 | ||||
-rw-r--r-- | tests/test_nncli.py | 278 | ||||
-rw-r--r-- | tests/test_version.py | 35 |
14 files changed, 824 insertions, 78 deletions
@@ -1,14 +1,7 @@ -# byte-compiled python files -*.pyc - -# venv dirs -env/ -venv/ - -# python packaging files/dirs -build -dist +build/ +dist/ MANIFEST -*.egg-info/* - -.ropeproject +*.egg-info/ +nnotes_cli/version.py +.coverage +.pytest_cache/ @@ -0,0 +1,19 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +appdirs = "*" +requests = "*" +urwid = "*" +setuptools = "*" +setuptools-scm = "*" + +[dev-packages] +pytest = "*" +pytest-cov = "*" +pytest-mock = "*" +pytest-runner = "*" +pylint = "*" +pudb = "*" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..8525be7 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,287 @@ +{ + "_meta": { + "hash": { + "sha256": "b4bc66838172733ffa35a5998d87982699b21d67eccdb8d913fffd8c7dcd101a" + }, + "pipfile-spec": 6, + "requires": {}, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "appdirs": { + "hashes": [ + "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", + "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + ], + "index": "pypi", + "version": "==1.4.3" + }, + "certifi": { + "hashes": [ + "sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638", + "sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a" + ], + "version": "==2018.8.24" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", + "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" + ], + "version": "==2.7" + }, + "requests": { + "hashes": [ + "sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1", + "sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a" + ], + "index": "pypi", + "version": "==2.19.1" + }, + "setuptools-scm": { + "hashes": [ + "sha256:1191f2a136b5e86f7ca8ab00a97ef7aef997131f1f6d4971be69a1ef387d8b40", + "sha256:cc6953d224a22f10e933fa2f55c95979317c55259016adcf93310ba2997febfa" + ], + "index": "pypi", + "version": "==3.1.0" + }, + "urllib3": { + "hashes": [ + "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", + "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5" + ], + "markers": "python_version != '3.1.*' and python_version != '3.3.*' and python_version >= '2.6' and python_version != '3.0.*' and python_version != '3.2.*' and python_version < '4'", + "version": "==1.23" + }, + "urwid": { + "hashes": [ + "sha256:644d3e3900867161a2fc9287a9762753d66bd194754679adb26aede559bcccbc" + ], + "index": "pypi", + "version": "==2.0.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:292fa429e69d60e4161e7612cb7cc8fa3609e2e309f80c224d93a76d5e7b58be", + "sha256:c7013d119ec95eb626f7a2011f0b63d0c9a095df9ad06d8507b37084eada1a8d" + ], + "version": "==2.0.4" + }, + "atomicwrites": { + "hashes": [ + "sha256:6b5282987b21cd79151f51caccead7a09d0a32e89c568bd9e3c4aaa7bbdf3f3a", + "sha256:e16334d50fe0f90919ef7339c24b9b62e6abaa78cd2d226f3d94eb067eb89043" + ], + "markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.3.*' and python_version != '3.0.*'", + "version": "==1.2.0" + }, + "attrs": { + "hashes": [ + "sha256:4b90b09eeeb9b88c35bc642cbac057e45a5fd85367b985bd2809c62b7b939265", + "sha256:e0d0eb91441a3b53dab4d9b743eafc1ac44476296a2053b6ca3af0b139faf87b" + ], + "version": "==18.1.0" + }, + "coverage": { + "hashes": [ + "sha256:03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba", + "sha256:0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed", + "sha256:10a46017fef60e16694a30627319f38a2b9b52e90182dddb6e37dcdab0f4bf95", + "sha256:198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640", + "sha256:23d341cdd4a0371820eb2b0bd6b88f5003a7438bbedb33688cd33b8eae59affd", + "sha256:28b2191e7283f4f3568962e373b47ef7f0392993bb6660d079c62bd50fe9d162", + "sha256:2a5b73210bad5279ddb558d9a2bfedc7f4bf6ad7f3c988641d83c40293deaec1", + "sha256:2eb564bbf7816a9d68dd3369a510be3327f1c618d2357fa6b1216994c2e3d508", + "sha256:337ded681dd2ef9ca04ef5d93cfc87e52e09db2594c296b4a0a3662cb1b41249", + "sha256:3a2184c6d797a125dca8367878d3b9a178b6fdd05fdc2d35d758c3006a1cd694", + "sha256:3c79a6f7b95751cdebcd9037e4d06f8d5a9b60e4ed0cd231342aa8ad7124882a", + "sha256:3d72c20bd105022d29b14a7d628462ebdc61de2f303322c0212a054352f3b287", + "sha256:3eb42bf89a6be7deb64116dd1cc4b08171734d721e7a7e57ad64cc4ef29ed2f1", + "sha256:4635a184d0bbe537aa185a34193898eee409332a8ccb27eea36f262566585000", + "sha256:56e448f051a201c5ebbaa86a5efd0ca90d327204d8b059ab25ad0f35fbfd79f1", + "sha256:5a13ea7911ff5e1796b6d5e4fbbf6952381a611209b736d48e675c2756f3f74e", + "sha256:69bf008a06b76619d3c3f3b1983f5145c75a305a0fea513aca094cae5c40a8f5", + "sha256:6bc583dc18d5979dc0f6cec26a8603129de0304d5ae1f17e57a12834e7235062", + "sha256:701cd6093d63e6b8ad7009d8a92425428bc4d6e7ab8d75efbb665c806c1d79ba", + "sha256:7608a3dd5d73cb06c531b8925e0ef8d3de31fed2544a7de6c63960a1e73ea4bc", + "sha256:76ecd006d1d8f739430ec50cc872889af1f9c1b6b8f48e29941814b09b0fd3cc", + "sha256:7aa36d2b844a3e4a4b356708d79fd2c260281a7390d678a10b91ca595ddc9e99", + "sha256:7d3f553904b0c5c016d1dad058a7554c7ac4c91a789fca496e7d8347ad040653", + "sha256:7e1fe19bd6dce69d9fd159d8e4a80a8f52101380d5d3a4d374b6d3eae0e5de9c", + "sha256:8c3cb8c35ec4d9506979b4cf90ee9918bc2e49f84189d9bf5c36c0c1119c6558", + "sha256:9d6dd10d49e01571bf6e147d3b505141ffc093a06756c60b053a859cb2128b1f", + "sha256:be6cfcd8053d13f5f5eeb284aa8a814220c3da1b0078fa859011c7fffd86dab9", + "sha256:c1bb572fab8208c400adaf06a8133ac0712179a334c09224fb11393e920abcdd", + "sha256:de4418dadaa1c01d497e539210cb6baa015965526ff5afc078c57ca69160108d", + "sha256:e05cb4d9aad6233d67e0541caa7e511fa4047ed7750ec2510d466e806e0255d6", + "sha256:f3f501f345f24383c0000395b26b726e46758b71393267aeae0bd36f8b3ade80" + ], + "markers": "python_version < '4' and python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.6'", + "version": "==4.5.1" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "markers": "python_version >= '2.7' and python_version != '3.3.*' and python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*'", + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "more-itertools": { + "hashes": [ + "sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092", + "sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e", + "sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d" + ], + "version": "==4.3.0" + }, + "pluggy": { + "hashes": [ + "sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1", + "sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1" + ], + "markers": "python_version != '3.0.*' and python_version != '3.2.*' and python_version != '3.1.*' and python_version != '3.3.*' and python_version >= '2.7'", + "version": "==0.7.1" + }, + "pudb": { + "hashes": [ + "sha256:8d8b974641b7a7a2a721af01c9dce5eac8e05a2ceebc2680725ba8eef1ca876e" + ], + "index": "pypi", + "version": "==2018.1" + }, + "py": { + "hashes": [ + "sha256:06a30435d058473046be836d3fc4f27167fd84c45b99704f2fb5509ef61f9af1", + "sha256:50402e9d1c9005d759426988a492e0edaadb7f4e68bcddfea586bc7432d009c6" + ], + "markers": "python_version != '3.0.*' and python_version != '3.2.*' and python_version != '3.3.*' and python_version != '3.1.*' and python_version >= '2.7'", + "version": "==1.6.0" + }, + "pygments": { + "hashes": [ + "sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d", + "sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc" + ], + "version": "==2.2.0" + }, + "pylint": { + "hashes": [ + "sha256:1d6d3622c94b4887115fe5204982eee66fdd8a951cf98635ee5caee6ec98c3ec", + "sha256:31142f764d2a7cd41df5196f9933b12b7ee55e73ef12204b648ad7e556c119fb" + ], + "index": "pypi", + "version": "==2.1.1" + }, + "pytest": { + "hashes": [ + "sha256:2e7c330338b2732ddb992217962e3454aa7290434e75329b1a6739cea41bea6b", + "sha256:4abcd98faeea3eb95bd05aa6a7b121d5f89d72e4d36ddb0dcbbfd1ec9f3651d1" + ], + "index": "pypi", + "version": "==3.7.3" + }, + "pytest-cov": { + "hashes": [ + "sha256:03aa752cf11db41d281ea1d807d954c4eda35cfa1b21d6971966cc041bbf6e2d", + "sha256:890fe5565400902b0c78b5357004aab1c814115894f4f21370e2433256a3eeec" + ], + "index": "pypi", + "version": "==2.5.1" + }, + "pytest-mock": { + "hashes": [ + "sha256:53801e621223d34724926a5c98bd90e8e417ce35264365d39d6c896388dcc928", + "sha256:d89a8209d722b8307b5e351496830d5cc5e192336003a485443ae9adeb7dd4c0" + ], + "index": "pypi", + "version": "==1.10.0" + }, + "pytest-runner": { + "hashes": [ + "sha256:d23f117be39919f00dd91bffeb4f15e031ec797501b717a245e377aee0f577be", + "sha256:d987fec1e31287592ffe1cb823a8c613c533db4c6aaca0ee1191dbc91e2fcc61" + ], + "index": "pypi", + "version": "==4.2" + }, + "six": { + "hashes": [ + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + ], + "version": "==1.11.0" + }, + "urwid": { + "hashes": [ + "sha256:644d3e3900867161a2fc9287a9762753d66bd194754679adb26aede559bcccbc" + ], + "index": "pypi", + "version": "==2.0.1" + }, + "wrapt": { + "hashes": [ + "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6" + ], + "version": "==1.10.11" + } + } +} @@ -1,6 +1,9 @@ nncli ===== + +[](https://pypi.org/project/nncli/) + NextCloud Notes Command Line Interface nncli is a Python application that gives you access to your NextCloud @@ -32,8 +35,11 @@ Check your OS distribution for installation packages. * Manually: - Clone this repository to your hard disk: `git clone https://github.com/djmoch/nncli.git` - - Install the requirements `pip3 install -r requirements.txt` - Install _nncli_: `python3 setup.py install` +* Development: + - Clone the repo + - Install Pipenv: `pip install pipenv` + - Stand up development environment: `pipenv install --dev` ### Features @@ -81,8 +87,8 @@ 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 - ~/.config/nncli/config) + -c <file>, --config=<file> - config file to read from + --version - version information COMMANDS: <none> - console gui mode when no command specified @@ -109,10 +115,9 @@ your NextCloud Notes account password must be stored someplace accessible to nncli. Use of the `cfg_nn_password_eval` option is recommended (see below). -nncli pulls in configuration from the `config` file located in your -`$XDG_CONFIG_HOME/nncli` directory. (By default, -`XDG_CONFIG_HOME=$HOME/.config`.) At the very least, the following example -`config` will get you going (using your account information): +nncli pulls in configuration from the `config` file located in the +standard location for your platform. At the very least, the following +example `config` will get you going (using your account information): ``` [nncli] @@ -146,7 +151,8 @@ cfg_nn_password_eval = gpg --quiet --for-your-eyes-only --no-tty --decrypt ~/.nn # see http://urwid.org/manual/userinput.html for examples of more key # combinations -kb_edit_note = space kb_page_down = ctrl f +kb_edit_note = space +kb_page_down = ctrl f # note that values must not be quoted clr_note_focus_bg = light blue diff --git a/nnotes_cli/__init__.py b/nnotes_cli/__init__.py index 054095f..9908156 100644 --- a/nnotes_cli/__init__.py +++ b/nnotes_cli/__init__.py @@ -1,5 +1,14 @@ +try: + from . import version + __version__ = version.version +except ImportError: + try: + from setuptools_scm import get_version + __version__ = get_version(root='..', relative_to=__file__) + except: + __version__ = '??-dev' + __productname__ = 'nncli' -__version__ = '0.1.1' __copyright__ = "Copyright (c) 2018 Daniel Moch" __author__ = "Daniel Moch" __author_email__ = "daniel@danielmoch.com" diff --git a/nnotes_cli/config.py b/nnotes_cli/config.py index 72b5992..31b5172 100644 --- a/nnotes_cli/config.py +++ b/nnotes_cli/config.py @@ -27,34 +27,19 @@ import os, sys, urwid, collections, configparser, subprocess +from appdirs import user_cache_dir, user_config_dir + class Config: def __init__(self, custom_file=None): - self.home = os.path.abspath(os.path.expanduser('~')) - if 'XDG_CONFIG_HOME' in os.environ.keys(): - self.config_home = \ - os.path.join(os.environ['XDG_CONFIG_HOME'], 'nncli') - else: - self.config_home = \ - os.path.join( - os.path.expanduser('~'), - '.config', - 'nncli' - ) - if 'XDG_CACHE_HOME' in os.environ.keys(): - self.cache_home = \ - os.path.join(os.environ['XDG_CACHE_HOME'], 'nncli') - else: - self.cache_home = \ - os.path.join( - os.path.expanduser('~'), - '.cache', - 'nncli' - ) + self.config_home = user_config_dir('nncli', 'djmoch') + self.cache_home = user_cache_dir('nncli', 'djmoch') + defaults = \ { 'cfg_nn_username' : '', 'cfg_nn_password' : '', + 'cfg_nn_password_eval' : '', 'cfg_db_path' : self.cache_home, 'cfg_search_categories' : 'yes', # with regex searches 'cfg_sort_mode' : 'date', # 'alpha' or 'date' diff --git a/nnotes_cli/nextcloud_note.py b/nnotes_cli/nextcloud_note.py index 64ce1b2..f26fb33 100644 --- a/nnotes_cli/nextcloud_note.py +++ b/nnotes_cli/nextcloud_note.py @@ -65,14 +65,11 @@ class NextcloudNote(object): def __init__(self, username, password, host): """ object constructor """ - self.username = urllib.parse.quote(username) - self.password = urllib.parse.quote(password) - self.api_url = \ - 'https://{}:{}@{}/index.php/apps/notes/api/v0.2/notes'. \ - format(username, password, host) - self.sanitized_url = \ - 'https://{}:****@{}/index.php/apps/notes/api/v0.2/notes'. \ - format(username, host) + self.username = username + self.password = password + self.url = \ + 'https://{}/index.php/apps/notes/api/v0.2/notes'. \ + format(host) self.status = 'offline' def get_note(self, noteid): @@ -89,10 +86,10 @@ class NextcloudNote(object): """ # request note - url = '{}/{}'.format(self.api_url, str(noteid)) - #logging.debug('REQUEST: ' + self.sanitized_url+params) + url = '{}/{}'.format(self.url, str(noteid)) + #logging.debug('REQUEST: ' + self.url+params) try: - res = requests.get(url) + res = requests.get(url, auth=(self.username, self.password)) res.raise_for_status() note = res.json() self.status = 'online' @@ -136,18 +133,20 @@ class NextcloudNote(object): if 'modified' not in note: note["modified"] = int(time.time()) - url = '{}/{}'.format(self.api_url, note["id"]) + url = '{}/{}'.format(self.url, note["id"]) del note["id"] else: - url = self.api_url + url = self.url #logging.debug('REQUEST: ' + url + ' - ' + str(note)) try: logging.debug('NOTE: ' + str(note)) - if url != self.api_url: - res = requests.put(url, data=note) + if url != self.url: + res = requests.put(url, auth=(self.username, + self.password), json=note) else: - res = requests.post(url, data=note) + res = requests.post(url, auth=(self.username, + self.password), json=note) note = res.json() res.raise_for_status() logging.debug('NOTE (from response): ' + str(res.json())) @@ -219,22 +218,25 @@ class NextcloudNote(object): # perform initial HTTP request try: - logging.debug('REQUEST: ' + self.sanitized_url + \ + logging.debug('REQUEST: ' + self.url + \ '?exclude=content') - res = requests.get(self.api_url, params=params) + res = requests.get(self.url, auth=(self.username, self.password), params=params) res.raise_for_status() #logging.debug('RESPONSE OK: ' + str(res)) note_list = res.json() self.status = 'online' except ConnectionError as e: + logging.exception('connection error') self.status = 'offline, connection error' status = -1 except RequestException as e: # if problem with network request/response + logging.exception('request error') status = -1 except ValueError as e: # if invalid json data status = -1 + logging.exception('request returned bad JSON data') # Can only filter for category at end, once all notes have been # retrieved. Below based on simplenote.vim, except we return @@ -258,12 +260,11 @@ class NextcloudNote(object): - status (int): 0 on sucesss and -1 otherwise """ - url = '{}/{}'.format(self.api_url, str(note['id'])) - logurl = '{}/{}'.format(self.sanitized_url, str(note['id'])) + url = '{}/{}'.format(self.url, str(note['id'])) try: - logging.debug('REQUEST DELETE: ' + logurl) - res = requests.delete(url) + logging.debug('REQUEST DELETE: ' + url) + res = requests.delete(url, auth=(self.username, self.password)) res.raise_for_status() self.status = 'online' except ConnectionError as e: diff --git a/nnotes_cli/nncli.py b/nnotes_cli/nncli.py index 060062f..7ef76a6 100644 --- a/nnotes_cli/nncli.py +++ b/nnotes_cli/nncli.py @@ -28,6 +28,7 @@ import os, sys, getopt, re, signal, time, datetime, shlex, hashlib import subprocess, threading, logging import copy, json, urwid, datetime +import nnotes_cli from . import view_titles, view_note, view_help, view_log, user_input from . import utils, temp from .config import Config @@ -1145,28 +1146,46 @@ 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 - ~/.config/nncli/config) + -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) + 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>) + 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 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 = '' + version_info += nnotes_cli.__productname__ + ' v' + \ + nnotes_cli.__version__ + "\n" + version_info += nnotes_cli.__description__ + "\n\n" + version_info += nnotes_cli.__copyright__ + "\n" + version_info += "Written by " + nnotes_cli.__author__ + \ + " and others\n" + version_info += "Licensed under the terms of the " + \ + nnotes_cli.__license__ + " license\n" + version_info += "The latest code is available at: " + \ + nnotes_cli.__url__ + print(version_info) + exit(0) def main(argv=sys.argv[1:]): verbose = False @@ -1178,14 +1197,17 @@ def main(argv=sys.argv[1:]): try: opts, args = getopt.getopt(argv, - 'hvnrk:t:c:', - [ 'help', 'verbose', 'nosync', 'regex', 'key=', 'title=', 'config=' ]) + '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']: diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 839715a..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -urwid -requests diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b0bd258 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,12 @@ +[aliases] +test=pytest + +[tool:pytest] +testpaths = tests +addopts = --cov=nnotes_cli --cov-report=term-missing + +[yapf] +coalesce_brackets = true +dedent_closing_brackets = true +space_between_ending_comma_and_closing_bracket = false +split_arguments_when_comma_terminated = true @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # # The MIT License (MIT) # @@ -29,7 +28,8 @@ from setuptools import setup import nnotes_cli -deps = ['urwid', 'requests'] +deps = ['urwid', 'requests', 'appdirs'] +test_deps = ['pytest', 'pytest-cov', 'pytest-runner', 'pytest-mock'] with open("README.md", "r") as fh: long_description = fh.read() @@ -39,13 +39,15 @@ setup( description=nnotes_cli.__description__, long_description=long_description, long_description_content_type="text/markdown", - version=nnotes_cli.__version__, author=nnotes_cli.__author__, author_email=nnotes_cli.__author_email__, url=nnotes_cli.__url__, license=nnotes_cli.__license__, requires=deps, install_requires=deps, + tests_require=test_deps, + use_scm_version= {'write_to': 'nnotes_cli/version.py'}, + setup_requires=['setuptools_scm'], packages=['nnotes_cli'], entry_points={ 'console_scripts': [ diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..75fe1e9 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,99 @@ +# +# The MIT License (MIT) +# +# Copyright (c) 2018 Daniel Moch +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +import os +import sys + +from nnotes_cli.config import Config +from pytest import raises + +def test_init(): + config = Config() + + if sys.platform == 'linux': + assert config.config_home == os.path.join(os.path.expanduser('~'), \ + '.config', 'nncli') + assert config.cache_home == os.path.join(os.path.expanduser('~'), \ + '.cache', 'nncli') + if sys.platform == 'darwin': + assert config.config_home == os.path.join(os.path.expanduser('~'), \ + 'Library', 'Preferences', 'nncli') + assert config.cache_home == os.path.join(os.path.expanduser('~'), \ + 'Library', 'Caches', 'nncli') + + +def test_custom_file(): + with open('test_cfg', 'w') as config_file: + config_file.write('[nncli]\n') + config_file.write('cfg_nn_username=user\n') + config_file.write('cfg_nn_password=password\n') + config_file.write('cfg_nn_host=nextcloud.example.org\n') + + config = Config('test_cfg') + os.remove('test_cfg') + +def test_bad_password_eval(): + with open('test_cfg', 'w') as config_file: + config_file.write('[nncli]\n') + config_file.write('cfg_nn_username=user\n') + config_file.write('cfg_nn_password_eval=password\n') + config_file.write('cfg_nn_host=nextcloud.example.org\n') + + with raises(SystemExit): + config = Config('test_cfg') + os.remove('test_cfg') + +def test_empty_config(): + with open('test_cfg', 'w') as config_file: + config_file.write('\n') + + config = Config('test_cfg') + os.remove('test_cfg') + +def test_get_config(): + config = Config('test_cfg') + assert config.get_config('sort_mode') == 'date' + +def test_get_config_descr(): + config = Config('test_cfg') + assert config.get_config_descr('sort_mode') == 'Sort mode' + +def test_get_keybind(): + config = Config('test_cfg') + assert config.get_keybind('help') == 'h' + +def test_get_keybind_use(): + config = Config('test_cfg') + assert config.get_keybind_use('help') == [ 'common' ] + +def test_get_keybind_descr(): + config = Config('test_cfg') + assert config.get_keybind_descr('help') == 'Help' + +def test_get_color(): + config = Config('test_cfg') + assert config.get_color('default_fg') == 'default' + +def test_get_color_descr(): + config = Config('test_cfg') + assert config.get_color_descr('default_fg') == 'Default fg' diff --git a/tests/test_nncli.py b/tests/test_nncli.py new file mode 100644 index 0000000..394135a --- /dev/null +++ b/tests/test_nncli.py @@ -0,0 +1,278 @@ +# +# The MIT License (MIT) +# +# Copyright (c) 2018 Daniel Moch +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +import logging +import os +import pytest +import shutil + +from logging.handlers import RotatingFileHandler +from nnotes_cli.nncli import nncli + +def mock_nncli(mocker): + mocker.patch('logging.getLogger') + mocker.patch('nnotes_cli.config.Config') + mocker.patch('nnotes_cli.notes_db.NotesDB') + mocker.patch('os.mkdir') + mocker.patch.object(RotatingFileHandler, '_open') + +def assert_initialized(): + logging.getLogger.assert_called_once() + RotatingFileHandler._open.assert_called_once() + assert os.mkdir.call_count == 2 + +@pytest.mark.parametrize('mock_nncli', [mock_nncli]) +def test_init_no_tempdir(mocker, mock_nncli): + mock_nncli(mocker) + + with open('test_cfg', 'w') as config_file: + config_file.write('[nncli]\n') + config_file.write('cfg_db_path=duh') + + nn = nncli(False, config_file='test_cfg') + assert_initialized() + assert nn.tempdir == None + os.mkdir.assert_called_with('duh') + + os.remove('test_cfg') + +@pytest.mark.parametrize('mock_nncli', [mock_nncli]) +def test_init(mocker, mock_nncli): + mock_nncli(mocker) + + with open('test_cfg', 'w') as config_file: + config_file.write('[nncli]\n') + config_file.write('cfg_tempdir=blah\n') + config_file.write('cfg_db_path=duh') + + nn = nncli(False, config_file='test_cfg') + assert_initialized() + assert nn.tempdir == 'blah' + + os.remove('test_cfg') + +@pytest.mark.parametrize('mock_nncli', [mock_nncli]) +def test_init_notesdb_fail(mocker, mock_nncli): + os.mkdir('duh') + mock_nncli(mocker) + + with open('duh/1.json', 'w') as bad_file: + bad_file.write('bad_json_data') + + with open('test_cfg', 'w') as config_file: + config_file.write('[nncli]\n') + config_file.write('cfg_db_path=duh') + + with pytest.raises(SystemExit): + nn = nncli(False, config_file='test_cfg') + + shutil.rmtree('duh') + +@pytest.mark.parametrize('mock_nncli', [mock_nncli]) +def test_get_editor(mocker, mock_nncli): + mock_nncli(mocker) + + with open('test_cfg', 'w') as config_file: + config_file.write('[nncli]\n') + config_file.write('cfg_db_path=duh') + config_file.write('cfg_editor=vim') + + nn = nncli(False, config_file='test_cfg') + assert_initialized() + assert nn.get_editor() == 'vim' + + os.remove('test_cfg') + +@pytest.mark.parametrize('mock_nncli', [mock_nncli]) +def test_no_editor(mocker, mock_nncli): + mock_nncli(mocker) + + with open('test_cfg', 'w') as config_file: + config_file.write('[nncli]\n') + config_file.write('cfg_db_path=duh') + + nn = nncli(False, config_file='test_cfg') + nn.config.configs['editor'] = [''] + assert_initialized() + assert nn.get_editor() == None + + os.remove('test_cfg') + +def test_get_pager(): + pass + +def test_get_diff(): + pass + +def test_exec_cmd_on_note(): + pass + +def test_exec_diff_on_note(): + pass + +def test_gui_header_clear(): + pass + +def test_gui_header_set(): + pass + +def test_gui_header_get(): + pass + +def test_gui_header_focus(): + pass + +def test_gui_footer_log_clear(): + pass + +def test_gui_footer_log_set(): + pass + +def test_gui_footer_log_get(): + pass + +def test_gui_footer_input_clear(): + pass + +def test_gui_footer_input_set(): + pass + +def test_gui_footer_input_get(): + pass + +def test_gui_footer_focus_input(): + pass + +def test_gui_body_clear(): + pass + +def test_gui_body_set(): + pass + +def test_gui_body_get(): + pass + +def test_gui_body_focus(): + pass + +def test_log_timeout(): + pass + +def test_log(): + pass + +def test_gui_update_view(): + pass + +def test_gui_update_status_bar(): + pass + +def test_gui_switch_frame_body(): + pass + +def test_delete_note_callback(): + pass + +def test_gui_yes_no_input(): + pass + +def test_gui_search_input(): + pass + +def test_gui_category_input(): + pass + +def test_gui_pipe_input(): + pass + +def test_gui_frame_keypress(): + pass + +def test_gui_init_view(): + pass + +def test_gui_clear(): + pass + +def test_gui_reset(): + pass + +def test_gui_stop(): + pass + +def test_gui(): + pass + +def test_cli_list_notes(): + pass + +def test_cli_note_dump(): + pass + +def test_cli_dump_notes(): + pass + +def test_cli_note_create(): + pass + +def test_cli_note_import(): + pass + +def test_cli_note_export(): + pass + +def test_cli_export_notes(): + pass + +def test_cli_note_edit(): + pass + +def test_cli_note_delete(): + pass + +def test_cli_note_favorite(): + pass + +def test_cli_note_category_get(): + pass + +def test_cli_note_category_set(): + pass + +def test_cli_note_category_rm(): + pass + +def test_SIGINT_handler(): + pass + +def test_usage(): + pass + +def test_version(): + pass + +def test_main(): + pass + +def test_nncli_start(): + pass diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 0000000..af1224a --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,35 @@ +# +# The MIT License (MIT) +# +# Copyright (c) 2018 Daniel Moch +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +import nnotes_cli +import pytest + +from nnotes_cli import version +from setuptools_scm import get_version + +@pytest.mark.skip(reason="test_version will fail outside of a Git repo") +def test_version(): + vers = get_version(root="..", relative_to=__file__) + + assert nnotes_cli.__version__ == vers + assert version.version == vers |