mirror of
https://github.com/samsonjs/vdirsyncer.git
synced 2026-04-27 14:57:41 +00:00
Move config tools to own file
This commit is contained in:
parent
565ef2e96e
commit
32abaae9b9
3 changed files with 179 additions and 175 deletions
|
|
@ -74,7 +74,7 @@ def app(ctx):
|
||||||
'''
|
'''
|
||||||
vdirsyncer -- synchronize calendars and contacts
|
vdirsyncer -- synchronize calendars and contacts
|
||||||
'''
|
'''
|
||||||
from .utils import load_config
|
from .config import load_config
|
||||||
|
|
||||||
if not ctx.config:
|
if not ctx.config:
|
||||||
ctx.config = load_config()
|
ctx.config = load_config()
|
||||||
|
|
|
||||||
177
vdirsyncer/cli/config.py
Normal file
177
vdirsyncer/cli/config.py
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import string
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
|
from . import CliError, cli_logger
|
||||||
|
from .. import PROJECT_HOME
|
||||||
|
from ..utils import expand_path
|
||||||
|
from ..utils.compat import text_type
|
||||||
|
|
||||||
|
try:
|
||||||
|
from ConfigParser import RawConfigParser
|
||||||
|
except ImportError:
|
||||||
|
from configparser import RawConfigParser
|
||||||
|
|
||||||
|
GENERAL_ALL = frozenset(['status_path', 'password_command'])
|
||||||
|
GENERAL_REQUIRED = frozenset(['status_path'])
|
||||||
|
SECTION_NAME_CHARS = frozenset(chain(string.ascii_letters, string.digits, '_'))
|
||||||
|
|
||||||
|
|
||||||
|
def validate_section_name(name, section_type):
|
||||||
|
invalid = set(name) - SECTION_NAME_CHARS
|
||||||
|
if invalid:
|
||||||
|
chars_display = ''.join(sorted(SECTION_NAME_CHARS))
|
||||||
|
raise CliError('The {}-section "{}" contains invalid characters. Only '
|
||||||
|
'the following characters are allowed for storage and '
|
||||||
|
'pair names:\n{}'.format(section_type, name,
|
||||||
|
chars_display))
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_general_section(general_config):
|
||||||
|
if 'passwordeval' in general_config:
|
||||||
|
# XXX: Deprecation
|
||||||
|
cli_logger.warning('The `passwordeval` parameter has been renamed to '
|
||||||
|
'`password_command`.')
|
||||||
|
|
||||||
|
invalid = set(general_config) - GENERAL_ALL
|
||||||
|
missing = GENERAL_REQUIRED - set(general_config)
|
||||||
|
problems = []
|
||||||
|
|
||||||
|
if invalid:
|
||||||
|
problems.append(u'general section doesn\'t take the parameters: {}'
|
||||||
|
.format(u', '.join(invalid)))
|
||||||
|
|
||||||
|
if missing:
|
||||||
|
problems.append(u'general section is missing the parameters: {}'
|
||||||
|
.format(u', '.join(missing)))
|
||||||
|
|
||||||
|
if problems:
|
||||||
|
raise CliError(u'Invalid general section. You should copy the example '
|
||||||
|
u'config from the repository and edit it: {}\n'
|
||||||
|
.format(PROJECT_HOME), problems=problems)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_pair_section(pair_config):
|
||||||
|
collections = pair_config.get('collections', None)
|
||||||
|
if collections is None:
|
||||||
|
return
|
||||||
|
e = ValueError('`collections` parameter must be a list of collection '
|
||||||
|
'names (strings!) or `null`.')
|
||||||
|
if not isinstance(collections, list) or \
|
||||||
|
any(not isinstance(x, (text_type, bytes)) for x in collections):
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
def load_config():
|
||||||
|
fname = os.environ.get('VDIRSYNCER_CONFIG', None)
|
||||||
|
if not fname:
|
||||||
|
fname = expand_path('~/.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:
|
||||||
|
general, pairs, storages = read_config(f)
|
||||||
|
except Exception as e:
|
||||||
|
raise CliError('Error during reading config {}: {}'
|
||||||
|
.format(fname, e))
|
||||||
|
|
||||||
|
return general, pairs, storages
|
||||||
|
|
||||||
|
|
||||||
|
def read_config(f):
|
||||||
|
c = RawConfigParser()
|
||||||
|
c.readfp(f)
|
||||||
|
|
||||||
|
def get_options(s):
|
||||||
|
return dict(parse_options(c.items(s), section=s))
|
||||||
|
|
||||||
|
general = {}
|
||||||
|
pairs = {}
|
||||||
|
storages = {}
|
||||||
|
|
||||||
|
def handle_storage(storage_name, options):
|
||||||
|
storages.setdefault(storage_name, {}).update(options)
|
||||||
|
storages[storage_name]['instance_name'] = storage_name
|
||||||
|
|
||||||
|
def handle_pair(pair_name, options):
|
||||||
|
_validate_pair_section(options)
|
||||||
|
a, b = options.pop('a'), options.pop('b')
|
||||||
|
pairs[pair_name] = a, b, options
|
||||||
|
|
||||||
|
def handle_general(_, options):
|
||||||
|
if general:
|
||||||
|
raise CliError('More than one general section in config file.')
|
||||||
|
general.update(options)
|
||||||
|
|
||||||
|
def bad_section(name, options):
|
||||||
|
cli_logger.error('Unknown section: {}'.format(name))
|
||||||
|
|
||||||
|
handlers = {'storage': handle_storage, 'pair': handle_pair, 'general':
|
||||||
|
handle_general}
|
||||||
|
|
||||||
|
for section in c.sections():
|
||||||
|
if ' ' in section:
|
||||||
|
section_type, name = section.split(' ', 1)
|
||||||
|
else:
|
||||||
|
section_type = name = section
|
||||||
|
|
||||||
|
try:
|
||||||
|
validate_section_name(name, section_type)
|
||||||
|
f = handlers.get(section_type, bad_section)
|
||||||
|
f(name, get_options(section))
|
||||||
|
except ValueError as e:
|
||||||
|
raise CliError('Section `{}`: {}'.format(section, str(e)))
|
||||||
|
|
||||||
|
_validate_general_section(general)
|
||||||
|
if getattr(f, 'name', None):
|
||||||
|
general['status_path'] = os.path.join(
|
||||||
|
os.path.dirname(f.name),
|
||||||
|
expand_path(general['status_path'])
|
||||||
|
)
|
||||||
|
return general, pairs, storages
|
||||||
|
|
||||||
|
|
||||||
|
def parse_config_value(value):
|
||||||
|
try:
|
||||||
|
return json.loads(value)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for wrong, right in [
|
||||||
|
(('on', 'yes'), 'true'),
|
||||||
|
(('off', 'no'), 'false'),
|
||||||
|
(('none',), 'null')
|
||||||
|
]:
|
||||||
|
if value.lower() in wrong + (right,):
|
||||||
|
cli_logger.warning('You probably meant {} instead of "{}", which '
|
||||||
|
'will now be interpreted as a literal string.'
|
||||||
|
.format(right, value))
|
||||||
|
|
||||||
|
if '#' in value:
|
||||||
|
raise ValueError('Invalid value:{}\n'
|
||||||
|
'Use double quotes (") if you want to use hashes in '
|
||||||
|
'your value.')
|
||||||
|
|
||||||
|
if len(value.splitlines()) > 1:
|
||||||
|
# ConfigParser's barrier for mistaking an arbitrary line for the
|
||||||
|
# continuation of a value is awfully low. The following example will
|
||||||
|
# also contain the second line in the value:
|
||||||
|
#
|
||||||
|
# foo = bar
|
||||||
|
# # my comment
|
||||||
|
raise ValueError('No multiline-values allowed:\n{}'.format(value))
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def parse_options(items, section=None):
|
||||||
|
for key, value in items:
|
||||||
|
try:
|
||||||
|
yield key, parse_config_value(value)
|
||||||
|
except ValueError as e:
|
||||||
|
raise ValueError('Section "{}", option "{}": {}'
|
||||||
|
.format(section, key, e))
|
||||||
|
|
@ -6,9 +6,7 @@ import hashlib
|
||||||
import importlib
|
import importlib
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import string
|
|
||||||
import sys
|
import sys
|
||||||
from itertools import chain
|
|
||||||
|
|
||||||
from atomicwrites import atomic_write
|
from atomicwrites import atomic_write
|
||||||
|
|
||||||
|
|
@ -17,16 +15,9 @@ import click
|
||||||
import click_threading
|
import click_threading
|
||||||
|
|
||||||
from . import CliError, cli_logger
|
from . import CliError, cli_logger
|
||||||
from .. import DOCS_HOME, PROJECT_HOME, exceptions
|
from .. import DOCS_HOME, exceptions
|
||||||
from ..sync import IdentConflict, StorageEmpty, SyncConflict
|
from ..sync import IdentConflict, StorageEmpty, SyncConflict
|
||||||
from ..utils import expand_path, get_class_init_args
|
from ..utils import expand_path, get_class_init_args
|
||||||
from ..utils.compat import text_type
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
from ConfigParser import RawConfigParser
|
|
||||||
except ImportError:
|
|
||||||
from configparser import RawConfigParser
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import Queue as queue
|
import Queue as queue
|
||||||
|
|
@ -68,11 +59,6 @@ storage_names = _StorageIndex()
|
||||||
del _StorageIndex
|
del _StorageIndex
|
||||||
|
|
||||||
|
|
||||||
GENERAL_ALL = frozenset(['status_path', 'password_command'])
|
|
||||||
GENERAL_REQUIRED = frozenset(['status_path'])
|
|
||||||
SECTION_NAME_CHARS = frozenset(chain(string.ascii_letters, string.digits, '_'))
|
|
||||||
|
|
||||||
|
|
||||||
class JobFailed(RuntimeError):
|
class JobFailed(RuntimeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -135,16 +121,6 @@ def handle_cli_error(status_name=None):
|
||||||
cli_logger.exception(msg)
|
cli_logger.exception(msg)
|
||||||
|
|
||||||
|
|
||||||
def validate_section_name(name, section_type):
|
|
||||||
invalid = set(name) - SECTION_NAME_CHARS
|
|
||||||
if invalid:
|
|
||||||
chars_display = ''.join(sorted(SECTION_NAME_CHARS))
|
|
||||||
raise CliError('The {}-section "{}" contains invalid characters. Only '
|
|
||||||
'the following characters are allowed for storage and '
|
|
||||||
'pair names:\n{}'.format(section_type, name,
|
|
||||||
chars_display))
|
|
||||||
|
|
||||||
|
|
||||||
def get_status_name(pair, collection):
|
def get_status_name(pair, collection):
|
||||||
if collection is None:
|
if collection is None:
|
||||||
return pair
|
return pair
|
||||||
|
|
@ -306,113 +282,6 @@ def _collections_for_pair_impl(status_path, name_a, name_b, pair_name,
|
||||||
yield collection, (a_args, b_args)
|
yield collection, (a_args, b_args)
|
||||||
|
|
||||||
|
|
||||||
def _validate_general_section(general_config):
|
|
||||||
if 'passwordeval' in general_config:
|
|
||||||
# XXX: Deprecation
|
|
||||||
cli_logger.warning('The `passwordeval` parameter has been renamed to '
|
|
||||||
'`password_command`.')
|
|
||||||
|
|
||||||
invalid = set(general_config) - GENERAL_ALL
|
|
||||||
missing = GENERAL_REQUIRED - set(general_config)
|
|
||||||
problems = []
|
|
||||||
|
|
||||||
if invalid:
|
|
||||||
problems.append(u'general section doesn\'t take the parameters: {}'
|
|
||||||
.format(u', '.join(invalid)))
|
|
||||||
|
|
||||||
if missing:
|
|
||||||
problems.append(u'general section is missing the parameters: {}'
|
|
||||||
.format(u', '.join(missing)))
|
|
||||||
|
|
||||||
if problems:
|
|
||||||
raise CliError(u'Invalid general section. You should copy the example '
|
|
||||||
u'config from the repository and edit it: {}\n'
|
|
||||||
.format(PROJECT_HOME), problems=problems)
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_pair_section(pair_config):
|
|
||||||
collections = pair_config.get('collections', None)
|
|
||||||
if collections is None:
|
|
||||||
return
|
|
||||||
e = ValueError('`collections` parameter must be a list of collection '
|
|
||||||
'names (strings!) or `null`.')
|
|
||||||
if not isinstance(collections, list) or \
|
|
||||||
any(not isinstance(x, (text_type, bytes)) for x in collections):
|
|
||||||
raise e
|
|
||||||
|
|
||||||
|
|
||||||
def load_config():
|
|
||||||
fname = os.environ.get('VDIRSYNCER_CONFIG', None)
|
|
||||||
if not fname:
|
|
||||||
fname = expand_path('~/.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:
|
|
||||||
general, pairs, storages = read_config(f)
|
|
||||||
except Exception as e:
|
|
||||||
raise CliError('Error during reading config {}: {}'
|
|
||||||
.format(fname, e))
|
|
||||||
|
|
||||||
return general, pairs, storages
|
|
||||||
|
|
||||||
|
|
||||||
def read_config(f):
|
|
||||||
c = RawConfigParser()
|
|
||||||
c.readfp(f)
|
|
||||||
|
|
||||||
def get_options(s):
|
|
||||||
return dict(parse_options(c.items(s), section=s))
|
|
||||||
|
|
||||||
general = {}
|
|
||||||
pairs = {}
|
|
||||||
storages = {}
|
|
||||||
|
|
||||||
def handle_storage(storage_name, options):
|
|
||||||
storages.setdefault(storage_name, {}).update(options)
|
|
||||||
storages[storage_name]['instance_name'] = storage_name
|
|
||||||
|
|
||||||
def handle_pair(pair_name, options):
|
|
||||||
_validate_pair_section(options)
|
|
||||||
a, b = options.pop('a'), options.pop('b')
|
|
||||||
pairs[pair_name] = a, b, options
|
|
||||||
|
|
||||||
def handle_general(_, options):
|
|
||||||
if general:
|
|
||||||
raise CliError('More than one general section in config file.')
|
|
||||||
general.update(options)
|
|
||||||
|
|
||||||
def bad_section(name, options):
|
|
||||||
cli_logger.error('Unknown section: {}'.format(name))
|
|
||||||
|
|
||||||
handlers = {'storage': handle_storage, 'pair': handle_pair, 'general':
|
|
||||||
handle_general}
|
|
||||||
|
|
||||||
for section in c.sections():
|
|
||||||
if ' ' in section:
|
|
||||||
section_type, name = section.split(' ', 1)
|
|
||||||
else:
|
|
||||||
section_type = name = section
|
|
||||||
|
|
||||||
try:
|
|
||||||
validate_section_name(name, section_type)
|
|
||||||
f = handlers.get(section_type, bad_section)
|
|
||||||
f(name, get_options(section))
|
|
||||||
except ValueError as e:
|
|
||||||
raise CliError('Section `{}`: {}'.format(section, str(e)))
|
|
||||||
|
|
||||||
_validate_general_section(general)
|
|
||||||
if getattr(f, 'name', None):
|
|
||||||
general['status_path'] = os.path.join(
|
|
||||||
os.path.dirname(f.name),
|
|
||||||
expand_path(general['status_path'])
|
|
||||||
)
|
|
||||||
return general, pairs, storages
|
|
||||||
|
|
||||||
|
|
||||||
def load_status(base_path, pair, collection=None, data_type=None):
|
def load_status(base_path, pair, collection=None, data_type=None):
|
||||||
assert data_type is not None
|
assert data_type is not None
|
||||||
status_name = get_status_name(pair, collection)
|
status_name = get_status_name(pair, collection)
|
||||||
|
|
@ -616,48 +485,6 @@ class WorkerQueue(object):
|
||||||
return self._queue.put(f)
|
return self._queue.put(f)
|
||||||
|
|
||||||
|
|
||||||
def parse_config_value(value):
|
|
||||||
try:
|
|
||||||
return json.loads(value)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
for wrong, right in [
|
|
||||||
(('on', 'yes'), 'true'),
|
|
||||||
(('off', 'no'), 'false'),
|
|
||||||
(('none',), 'null')
|
|
||||||
]:
|
|
||||||
if value.lower() in wrong + (right,):
|
|
||||||
cli_logger.warning('You probably meant {} instead of "{}", which '
|
|
||||||
'will now be interpreted as a literal string.'
|
|
||||||
.format(right, value))
|
|
||||||
|
|
||||||
if '#' in value:
|
|
||||||
raise ValueError('Invalid value:{}\n'
|
|
||||||
'Use double quotes (") if you want to use hashes in '
|
|
||||||
'your value.')
|
|
||||||
|
|
||||||
if len(value.splitlines()) > 1:
|
|
||||||
# ConfigParser's barrier for mistaking an arbitrary line for the
|
|
||||||
# continuation of a value is awfully low. The following example will
|
|
||||||
# also contain the second line in the value:
|
|
||||||
#
|
|
||||||
# foo = bar
|
|
||||||
# # my comment
|
|
||||||
raise ValueError('No multiline-values allowed:\n{}'.format(value))
|
|
||||||
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def parse_options(items, section=None):
|
|
||||||
for key, value in items:
|
|
||||||
try:
|
|
||||||
yield key, parse_config_value(value)
|
|
||||||
except ValueError as e:
|
|
||||||
raise ValueError('Section "{}", option "{}": {}'
|
|
||||||
.format(section, key, e))
|
|
||||||
|
|
||||||
|
|
||||||
def format_storage_config(cls, header=True):
|
def format_storage_config(cls, header=True):
|
||||||
if header is True:
|
if header is True:
|
||||||
yield '[storage example_for_{}]'.format(cls.storage_name)
|
yield '[storage example_for_{}]'.format(cls.storage_name)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue