mirror of
https://github.com/samsonjs/vdirsyncer.git
synced 2026-04-27 14:57:41 +00:00
Port CLI to Click.
After a while i finally gave in and ported the whole command line interface of vdirsyncer to Click. While it might seem like a huge dependency, i hope we can eliminate some helper functions in the process, and maybe are more motivated to write a more beautiful and intuitive interface due to all the niceties Click provides.
This commit is contained in:
parent
d98c4b37ba
commit
8b889e9ecd
4 changed files with 29 additions and 52 deletions
2
setup.py
2
setup.py
|
|
@ -38,7 +38,7 @@ setup(
|
||||||
'console_scripts': ['vdirsyncer = vdirsyncer.cli:main']
|
'console_scripts': ['vdirsyncer = vdirsyncer.cli:main']
|
||||||
},
|
},
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'argvard>=0.3.0',
|
'click',
|
||||||
'requests',
|
'requests',
|
||||||
'lxml',
|
'lxml',
|
||||||
'icalendar>=3.6',
|
'icalendar>=3.6',
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import argvard
|
import click
|
||||||
|
|
||||||
from . import log
|
from . import log
|
||||||
from .storage import storage_names
|
from .storage import storage_names
|
||||||
|
|
@ -206,41 +206,29 @@ def parse_pairs_args(pairs_args, all_pairs):
|
||||||
|
|
||||||
def _main(env, file_cfg):
|
def _main(env, file_cfg):
|
||||||
general, all_pairs, all_storages = file_cfg
|
general, all_pairs, all_storages = file_cfg
|
||||||
app = argvard.Argvard()
|
|
||||||
|
|
||||||
@app.option('--verbosity verbosity')
|
@click.group()
|
||||||
def verbose_option(context, verbosity):
|
@click.option('--verbosity', '-v', default='INFO',
|
||||||
|
help='Either CRITICAL, ERROR, WARNING, INFO or DEBUG')
|
||||||
|
def app(verbosity):
|
||||||
'''
|
'''
|
||||||
Basically Python logging levels.
|
vdirsyncer -- synchronize calendars and contacts
|
||||||
|
|
||||||
CRITICAL: Config errors, at most.
|
|
||||||
|
|
||||||
ERROR: Normal errors, at most.
|
|
||||||
|
|
||||||
WARNING: Problems of which vdirsyncer thinks that it can handle them
|
|
||||||
itself, but which might crash other clients.
|
|
||||||
|
|
||||||
INFO: Normal output.
|
|
||||||
|
|
||||||
DEBUG: Show e.g. HTTP traffic. Not supposed to be readable by the
|
|
||||||
normal user.
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
verbosity = verbosity.upper()
|
verbosity = verbosity.upper()
|
||||||
x = getattr(log.logging, verbosity, None)
|
x = getattr(log.logging, verbosity, None)
|
||||||
if x is None:
|
if x is None:
|
||||||
raise ValueError(u'Invalid verbosity value: {}'.format(verbosity))
|
cli_logger.critical(u'Invalid verbosity value: {}'
|
||||||
log.set_level(x)
|
.format(verbosity))
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
log.set_level(x)
|
||||||
|
|
||||||
sync_command = argvard.Command()
|
@app.command()
|
||||||
|
@click.argument('pairs', nargs=-1)
|
||||||
@sync_command.option('--force-delete status_name')
|
@click.option('--force-delete', multiple=True,
|
||||||
def force_delete(context, status_name):
|
help=('Disable data-loss protection for the given pairs. '
|
||||||
'''Pretty please delete all my data.'''
|
'Can be passed multiple times'))
|
||||||
context.setdefault('force_delete', set()).add(status_name)
|
def sync(pairs, force_delete):
|
||||||
|
|
||||||
@sync_command.main('[pairs...]')
|
|
||||||
def sync_main(context, pairs=None):
|
|
||||||
'''
|
'''
|
||||||
Synchronize the given pairs. If no pairs are given, all will be
|
Synchronize the given pairs. If no pairs are given, all will be
|
||||||
synchronized.
|
synchronized.
|
||||||
|
|
@ -253,7 +241,7 @@ def _main(env, file_cfg):
|
||||||
'''
|
'''
|
||||||
actions = []
|
actions = []
|
||||||
handled_collections = set()
|
handled_collections = set()
|
||||||
force_delete = context.get('force_delete', set())
|
force_delete = set(force_delete)
|
||||||
for pair_name, _collection in parse_pairs_args(pairs, all_pairs):
|
for pair_name, _collection in parse_pairs_args(pairs, all_pairs):
|
||||||
for collection in expand_collection(pair_name, _collection,
|
for collection in expand_collection(pair_name, _collection,
|
||||||
all_pairs, all_storages):
|
all_pairs, all_storages):
|
||||||
|
|
@ -293,16 +281,12 @@ def _main(env, file_cfg):
|
||||||
from multiprocessing import Pool
|
from multiprocessing import Pool
|
||||||
p = Pool(processes=general.get('processes', 0) or len(actions))
|
p = Pool(processes=general.get('processes', 0) or len(actions))
|
||||||
if not all(p.map_async(_sync_collection, actions).get(10**9)):
|
if not all(p.map_async(_sync_collection, actions).get(10**9)):
|
||||||
raise CliError()
|
sys.exit(1)
|
||||||
|
|
||||||
app.register_command('sync', sync_command)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
app()
|
app()
|
||||||
except CliError as e:
|
except CliError as e:
|
||||||
msg = str(e)
|
cli_logger.critical(str(e))
|
||||||
if msg:
|
|
||||||
cli_logger.critical(msg)
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,10 @@
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
import click
|
||||||
|
|
||||||
from .. import exceptions, log
|
from .. import exceptions, log
|
||||||
from .compat import get_raw_input, iteritems, urlparse
|
from .compat import iteritems, urlparse
|
||||||
|
|
||||||
|
|
||||||
logger = log.get(__name__)
|
logger = log.get(__name__)
|
||||||
|
|
@ -155,8 +156,6 @@ def get_password(username, resource):
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import getpass
|
|
||||||
|
|
||||||
for func in (_password_from_netrc, _password_from_keyring):
|
for func in (_password_from_netrc, _password_from_keyring):
|
||||||
password = func(username, resource)
|
password = func(username, resource)
|
||||||
if password is not None:
|
if password is not None:
|
||||||
|
|
@ -164,18 +163,14 @@ def get_password(username, resource):
|
||||||
.format(username, func.__doc__))
|
.format(username, func.__doc__))
|
||||||
return password
|
return password
|
||||||
|
|
||||||
prompt = ('Server password for {} at the resource {}: '
|
prompt = ('Server password for {} at the resource {}'
|
||||||
.format(username, resource))
|
.format(username, resource))
|
||||||
password = getpass.getpass(prompt=prompt)
|
password = click.prompt(prompt=prompt, hide_input=True)
|
||||||
|
|
||||||
if keyring is not None:
|
if keyring is not None and \
|
||||||
answer = None
|
click.confirm('Save this password in the keyring?', default=False):
|
||||||
while answer not in ['', 'y', 'n']:
|
keyring.set_password(password_key_prefix + resource,
|
||||||
prompt = 'Save this password in the keyring? [y/N] '
|
username, password)
|
||||||
answer = get_raw_input(prompt).lower()
|
|
||||||
if answer == 'y':
|
|
||||||
keyring.set_password(password_key_prefix + resource,
|
|
||||||
username, password)
|
|
||||||
|
|
||||||
return password
|
return password
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ if PY2:
|
||||||
text_type = unicode # flake8: noqa
|
text_type = unicode # flake8: noqa
|
||||||
iteritems = lambda x: x.iteritems()
|
iteritems = lambda x: x.iteritems()
|
||||||
itervalues = lambda x: x.itervalues()
|
itervalues = lambda x: x.itervalues()
|
||||||
get_raw_input = raw_input
|
|
||||||
else:
|
else:
|
||||||
import urllib.parse as urlparse
|
import urllib.parse as urlparse
|
||||||
urlquote_plus = urlparse.quote_plus
|
urlquote_plus = urlparse.quote_plus
|
||||||
|
|
@ -28,4 +27,3 @@ else:
|
||||||
text_type = str
|
text_type = str
|
||||||
iteritems = lambda x: x.items()
|
iteritems = lambda x: x.items()
|
||||||
itervalues = lambda x: x.values()
|
itervalues = lambda x: x.values()
|
||||||
get_raw_input = input
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue