mirror of
https://github.com/samsonjs/vdirsyncer.git
synced 2026-04-27 14:57:41 +00:00
Make .cli a subpackage
This commit is contained in:
parent
ecb40579df
commit
cfe252d458
3 changed files with 161 additions and 143 deletions
|
|
@ -66,7 +66,7 @@ def test_storage_instance_from_config(monkeypatch):
|
||||||
import vdirsyncer.storage
|
import vdirsyncer.storage
|
||||||
monkeypatch.setitem(vdirsyncer.storage.storage_names, 'lol', lol)
|
monkeypatch.setitem(vdirsyncer.storage.storage_names, 'lol', lol)
|
||||||
config = {'type': 'lol', 'foo': 'bar', 'baz': 1}
|
config = {'type': 'lol', 'foo': 'bar', 'baz': 1}
|
||||||
assert cli.storage_instance_from_config(config) == 'OK'
|
assert cli.utils.storage_instance_from_config(config) == 'OK'
|
||||||
|
|
||||||
|
|
||||||
def test_parse_pairs_args():
|
def test_parse_pairs_args():
|
||||||
|
|
@ -246,10 +246,13 @@ def test_deprecated_item_status(tmpdir):
|
||||||
'ident_two': ['href_a', 'etag_a', 'href_b', 'etag_b']
|
'ident_two': ['href_a', 'etag_a', 'href_b', 'etag_b']
|
||||||
}
|
}
|
||||||
|
|
||||||
assert cli.load_status(str(tmpdir), 'mypair', data_type='items') == data
|
assert cli.utils.load_status(
|
||||||
|
str(tmpdir), 'mypair', data_type='items') == data
|
||||||
|
|
||||||
cli.save_status(str(tmpdir), 'mypair', data_type='items', data=data)
|
cli.utils.save_status(
|
||||||
assert cli.load_status(str(tmpdir), 'mypair', data_type='items') == data
|
str(tmpdir), 'mypair', data_type='items', data=data)
|
||||||
|
assert cli.utils.load_status(
|
||||||
|
str(tmpdir), 'mypair', data_type='items') == data
|
||||||
|
|
||||||
|
|
||||||
def test_collections_cache_invalidation(tmpdir):
|
def test_collections_cache_invalidation(tmpdir):
|
||||||
|
|
|
||||||
147
vdirsyncer/cli/__init__.py
Normal file
147
vdirsyncer/cli/__init__.py
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
'''
|
||||||
|
vdirsyncer.cli
|
||||||
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
:copyright: (c) 2014 Markus Unterwaditzer & contributors
|
||||||
|
:license: MIT, see LICENSE for more details.
|
||||||
|
'''
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from .utils import CliError, WorkerQueue, cli_logger, collections_for_pair, \
|
||||||
|
handle_cli_error, load_config, parse_pairs_args, sync_pair
|
||||||
|
from .. import __version__, log
|
||||||
|
from ..doubleclick import click
|
||||||
|
from ..utils import expand_path
|
||||||
|
|
||||||
|
|
||||||
|
def catch_errors(f):
|
||||||
|
@functools.wraps(f)
|
||||||
|
def inner(*a, **kw):
|
||||||
|
try:
|
||||||
|
f(*a, **kw)
|
||||||
|
except:
|
||||||
|
if not handle_cli_error():
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
def validate_verbosity(ctx, param, value):
|
||||||
|
x = getattr(log.logging, value.upper(), None)
|
||||||
|
if x is None:
|
||||||
|
raise click.BadParameter('Invalid verbosity value {}. Must be '
|
||||||
|
'CRITICAL, ERROR, WARNING, INFO or DEBUG'
|
||||||
|
.format(value))
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
@click.group()
|
||||||
|
@click.option('--verbosity', '-v', default='INFO',
|
||||||
|
callback=validate_verbosity,
|
||||||
|
help='Either CRITICAL, ERROR, WARNING, INFO or DEBUG')
|
||||||
|
@click.version_option(version=__version__)
|
||||||
|
@click.pass_context
|
||||||
|
@catch_errors
|
||||||
|
def app(ctx, verbosity):
|
||||||
|
'''
|
||||||
|
vdirsyncer -- synchronize calendars and contacts
|
||||||
|
'''
|
||||||
|
log.add_handler(log.stdout_handler)
|
||||||
|
log.set_level(verbosity)
|
||||||
|
|
||||||
|
if ctx.obj is None:
|
||||||
|
ctx.obj = {}
|
||||||
|
|
||||||
|
if 'config' not in ctx.obj:
|
||||||
|
fname = expand_path(os.environ.get('VDIRSYNCER_CONFIG',
|
||||||
|
'~/.vdirsyncer/config'))
|
||||||
|
if not os.path.exists(fname):
|
||||||
|
xdg_config_dir = os.environ.get('XDG_CONFIG_HOME',
|
||||||
|
expand_path('~/.config/'))
|
||||||
|
fname = os.path.join(xdg_config_dir, 'vdirsyncer/config')
|
||||||
|
try:
|
||||||
|
with open(fname) as f:
|
||||||
|
ctx.obj['config'] = load_config(f)
|
||||||
|
except Exception as e:
|
||||||
|
raise CliError('Error during reading config {}: {}'
|
||||||
|
.format(fname, e))
|
||||||
|
|
||||||
|
main = app
|
||||||
|
|
||||||
|
max_workers_option = click.option(
|
||||||
|
'--max-workers', default=0, type=click.IntRange(min=0, max=None),
|
||||||
|
help=('Use at most this many connections, 0 means unlimited.')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
@click.argument('pairs', nargs=-1)
|
||||||
|
@click.option('--force-delete/--no-force-delete',
|
||||||
|
help=('Disable data-loss protection for the given pairs. '
|
||||||
|
'Can be passed multiple times'))
|
||||||
|
@max_workers_option
|
||||||
|
@click.pass_context
|
||||||
|
@catch_errors
|
||||||
|
def sync(ctx, pairs, force_delete, max_workers):
|
||||||
|
'''
|
||||||
|
Synchronize the given collections or pairs. If no arguments are given,
|
||||||
|
all will be synchronized.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
`vdirsyncer sync` will sync everything configured.
|
||||||
|
`vdirsyncer sync bob frank` will sync the pairs "bob" and "frank".
|
||||||
|
`vdirsyncer sync bob/first_collection` will sync "first_collection"
|
||||||
|
from the pair "bob".
|
||||||
|
'''
|
||||||
|
general, all_pairs, all_storages = ctx.obj['config']
|
||||||
|
|
||||||
|
cli_logger.debug('Using {} maximal workers.'.format(max_workers))
|
||||||
|
wq = WorkerQueue(max_workers)
|
||||||
|
wq.handled_jobs = set()
|
||||||
|
|
||||||
|
for pair_name, collections in parse_pairs_args(pairs, all_pairs):
|
||||||
|
wq.spawn_worker()
|
||||||
|
wq.put(
|
||||||
|
functools.partial(sync_pair, pair_name=pair_name,
|
||||||
|
collections_to_sync=collections,
|
||||||
|
general=general, all_pairs=all_pairs,
|
||||||
|
all_storages=all_storages,
|
||||||
|
force_delete=force_delete))
|
||||||
|
|
||||||
|
wq.join()
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
@click.argument('pairs', nargs=-1)
|
||||||
|
@max_workers_option
|
||||||
|
@click.pass_context
|
||||||
|
@catch_errors
|
||||||
|
def discover(ctx, pairs, max_workers):
|
||||||
|
'''
|
||||||
|
Refresh collection cache for the given pairs.
|
||||||
|
'''
|
||||||
|
general, all_pairs, all_storages = ctx.obj['config']
|
||||||
|
cli_logger.debug('Using {} maximal workers.'.format(max_workers))
|
||||||
|
wq = WorkerQueue(max_workers)
|
||||||
|
|
||||||
|
for pair in (pairs or all_pairs):
|
||||||
|
try:
|
||||||
|
name_a, name_b, pair_options = all_pairs[pair]
|
||||||
|
except KeyError:
|
||||||
|
raise CliError('Pair not found: {}\n'
|
||||||
|
'These are the pairs found: {}'
|
||||||
|
.format(pair, list(all_pairs)))
|
||||||
|
|
||||||
|
wq.spawn_worker()
|
||||||
|
wq.put(functools.partial(
|
||||||
|
(lambda wq, **kwargs: collections_for_pair(**kwargs)),
|
||||||
|
status_path=general['status_path'], name_a=name_a,
|
||||||
|
name_b=name_b, pair_name=pair, config_a=all_storages[name_a],
|
||||||
|
config_b=all_storages[name_b], pair_options=pair_options,
|
||||||
|
skip_cache=True))
|
||||||
|
|
||||||
|
wq.join()
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
'''
|
'''
|
||||||
vdirsyncer.cli
|
vdirsyncer.cli.utils
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
:copyright: (c) 2014 Markus Unterwaditzer & contributors
|
:copyright: (c) 2014 Markus Unterwaditzer & contributors
|
||||||
:license: MIT, see LICENSE for more details.
|
:license: MIT, see LICENSE for more details.
|
||||||
|
|
@ -17,11 +17,11 @@ import sys
|
||||||
import threading
|
import threading
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
|
||||||
from . import DOCS_HOME, PROJECT_HOME, __version__, log
|
from .. import DOCS_HOME, PROJECT_HOME, log
|
||||||
from .doubleclick import click
|
from ..doubleclick import click
|
||||||
from .storage import storage_names
|
from ..storage import storage_names
|
||||||
from .sync import StorageEmpty, SyncConflict, sync
|
from ..sync import StorageEmpty, SyncConflict, sync
|
||||||
from .utils import expand_path, get_class_init_args, parse_options, \
|
from ..utils import expand_path, get_class_init_args, parse_options, \
|
||||||
safe_write
|
safe_write
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -380,138 +380,6 @@ def parse_pairs_args(pairs_args, all_pairs):
|
||||||
|
|
||||||
return rv.items()
|
return rv.items()
|
||||||
|
|
||||||
# We create the app inside a factory and destroy that factory after first use
|
|
||||||
# to avoid pollution of the module namespace.
|
|
||||||
|
|
||||||
|
|
||||||
def _create_app():
|
|
||||||
def catch_errors(f):
|
|
||||||
@functools.wraps(f)
|
|
||||||
def inner(*a, **kw):
|
|
||||||
try:
|
|
||||||
f(*a, **kw)
|
|
||||||
except:
|
|
||||||
if not handle_cli_error():
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
return inner
|
|
||||||
|
|
||||||
def validate_verbosity(ctx, param, value):
|
|
||||||
x = getattr(log.logging, value.upper(), None)
|
|
||||||
if x is None:
|
|
||||||
raise click.BadParameter('Invalid verbosity value {}. Must be '
|
|
||||||
'CRITICAL, ERROR, WARNING, INFO or DEBUG'
|
|
||||||
.format(value))
|
|
||||||
return x
|
|
||||||
|
|
||||||
@click.group()
|
|
||||||
@click.option('--verbosity', '-v', default='INFO',
|
|
||||||
callback=validate_verbosity,
|
|
||||||
help='Either CRITICAL, ERROR, WARNING, INFO or DEBUG')
|
|
||||||
@click.version_option(version=__version__)
|
|
||||||
@click.pass_context
|
|
||||||
@catch_errors
|
|
||||||
def app(ctx, verbosity):
|
|
||||||
'''
|
|
||||||
vdirsyncer -- synchronize calendars and contacts
|
|
||||||
'''
|
|
||||||
log.add_handler(log.stdout_handler)
|
|
||||||
log.set_level(verbosity)
|
|
||||||
|
|
||||||
if ctx.obj is None:
|
|
||||||
ctx.obj = {}
|
|
||||||
|
|
||||||
if 'config' not in ctx.obj:
|
|
||||||
fname = expand_path(os.environ.get('VDIRSYNCER_CONFIG',
|
|
||||||
'~/.vdirsyncer/config'))
|
|
||||||
if not os.path.exists(fname):
|
|
||||||
xdg_config_dir = os.environ.get('XDG_CONFIG_HOME',
|
|
||||||
expand_path('~/.config/'))
|
|
||||||
fname = os.path.join(xdg_config_dir, 'vdirsyncer/config')
|
|
||||||
try:
|
|
||||||
with open(fname) as f:
|
|
||||||
ctx.obj['config'] = load_config(f)
|
|
||||||
except Exception as e:
|
|
||||||
raise CliError('Error during reading config {}: {}'
|
|
||||||
.format(fname, e))
|
|
||||||
|
|
||||||
max_workers_option = click.option(
|
|
||||||
'--max-workers', default=0, type=click.IntRange(min=0, max=None),
|
|
||||||
help=('Use at most this many connections, 0 means unlimited.')
|
|
||||||
)
|
|
||||||
|
|
||||||
@app.command()
|
|
||||||
@click.argument('pairs', nargs=-1)
|
|
||||||
@click.option('--force-delete/--no-force-delete',
|
|
||||||
help=('Disable data-loss protection for the given pairs. '
|
|
||||||
'Can be passed multiple times'))
|
|
||||||
@max_workers_option
|
|
||||||
@click.pass_context
|
|
||||||
@catch_errors
|
|
||||||
def sync(ctx, pairs, force_delete, max_workers):
|
|
||||||
'''
|
|
||||||
Synchronize the given collections or pairs. If no arguments are given,
|
|
||||||
all will be synchronized.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
`vdirsyncer sync` will sync everything configured.
|
|
||||||
`vdirsyncer sync bob frank` will sync the pairs "bob" and "frank".
|
|
||||||
`vdirsyncer sync bob/first_collection` will sync "first_collection"
|
|
||||||
from the pair "bob".
|
|
||||||
'''
|
|
||||||
general, all_pairs, all_storages = ctx.obj['config']
|
|
||||||
|
|
||||||
cli_logger.debug('Using {} maximal workers.'.format(max_workers))
|
|
||||||
wq = WorkerQueue(max_workers)
|
|
||||||
wq.handled_jobs = set()
|
|
||||||
|
|
||||||
for pair_name, collections in parse_pairs_args(pairs, all_pairs):
|
|
||||||
wq.spawn_worker()
|
|
||||||
wq.put(
|
|
||||||
functools.partial(sync_pair, pair_name=pair_name,
|
|
||||||
collections_to_sync=collections,
|
|
||||||
general=general, all_pairs=all_pairs,
|
|
||||||
all_storages=all_storages,
|
|
||||||
force_delete=force_delete))
|
|
||||||
|
|
||||||
wq.join()
|
|
||||||
|
|
||||||
@app.command()
|
|
||||||
@click.argument('pairs', nargs=-1)
|
|
||||||
@max_workers_option
|
|
||||||
@click.pass_context
|
|
||||||
@catch_errors
|
|
||||||
def discover(ctx, pairs, max_workers):
|
|
||||||
'''
|
|
||||||
Refresh collection cache for the given pairs.
|
|
||||||
'''
|
|
||||||
general, all_pairs, all_storages = ctx.obj['config']
|
|
||||||
cli_logger.debug('Using {} maximal workers.'.format(max_workers))
|
|
||||||
wq = WorkerQueue(max_workers)
|
|
||||||
|
|
||||||
for pair in (pairs or all_pairs):
|
|
||||||
try:
|
|
||||||
name_a, name_b, pair_options = all_pairs[pair]
|
|
||||||
except KeyError:
|
|
||||||
raise CliError('Pair not found: {}\n'
|
|
||||||
'These are the pairs found: {}'
|
|
||||||
.format(pair, list(all_pairs)))
|
|
||||||
|
|
||||||
wq.spawn_worker()
|
|
||||||
wq.put(functools.partial(
|
|
||||||
(lambda wq, **kwargs: collections_for_pair(**kwargs)),
|
|
||||||
status_path=general['status_path'], name_a=name_a,
|
|
||||||
name_b=name_b, pair_name=pair, config_a=all_storages[name_a],
|
|
||||||
config_b=all_storages[name_b], pair_options=pair_options,
|
|
||||||
skip_cache=True))
|
|
||||||
|
|
||||||
wq.join()
|
|
||||||
|
|
||||||
return app
|
|
||||||
|
|
||||||
app = main = _create_app()
|
|
||||||
del _create_app
|
|
||||||
|
|
||||||
|
|
||||||
def sync_pair(wq, pair_name, collections_to_sync, general, all_pairs,
|
def sync_pair(wq, pair_name, collections_to_sync, general, all_pairs,
|
||||||
all_storages, force_delete):
|
all_storages, force_delete):
|
||||||
Loading…
Reference in a new issue