Merge branch 'dav_discovery'

Fix #11
This commit is contained in:
Markus Unterwaditzer 2014-03-11 18:03:47 +01:00
commit 355c53a8db
14 changed files with 145 additions and 49 deletions

View file

@ -21,9 +21,12 @@ class StorageTests(object):
item_template = item_template or self.item_template
return Item(item_template.format(uid=uid, r=r))
def _get_storage(self, **kwargs):
def get_storage_args(self, collection=None):
raise NotImplementedError()
def _get_storage(self):
return self.storage_class(**self.get_storage_args())
def test_generic(self):
items = map(self._create_bogus_item, range(1, 10))
for i, item in enumerate(items):
@ -88,3 +91,21 @@ class StorageTests(object):
assert not list(s.list())
s.upload(self._create_bogus_item('1'))
assert list(s.list())
def test_discover(self):
items = []
for i in range(4):
s = self.storage_class(**self.get_storage_args(collection=str(i)))
items.append(self._create_bogus_item(str(i)))
s.upload(items[-1])
d = self.storage_class.discover(
**self.get_storage_args(collection=None))
for s in d:
((href, etag),) = s.list()
item, etag = s.get(href)
assert item.raw in set(x.raw for x in items)
def test_collection_arg(self):
s = self.storage_class(**self.get_storage_args(collection='asd'))
assert s.collection == 'asd'

View file

@ -0,0 +1,6 @@
import pytest
@pytest.fixture
def class_tmpdir(request, tmpdir):
request.instance.tmpdir = str(tmpdir)

View file

@ -13,11 +13,11 @@
:license: MIT, see LICENSE for more details.
'''
import tempfile
import shutil
import sys
import os
import urlparse
import tempfile
import shutil
import mock
from werkzeug.test import Client
@ -104,6 +104,7 @@ class Response(object):
self.x = x
self.status_code = x.status_code
self.content = x.get_data(as_text=False)
self.text = x.get_data(as_text=True)
self.headers = x.headers
self.encoding = x.charset
@ -116,24 +117,20 @@ class Response(object):
class DavStorageTests(StorageTests):
'''hrefs are paths without scheme or netloc'''
tmpdir = None
storage_class = None
radicale_path = None
patcher = None
tmpdir = None
def _get_storage(self, **kwargs):
def setup_method(self, method):
self.tmpdir = tempfile.mkdtemp()
do_the_radicale_dance(self.tmpdir)
from radicale import Application
app = Application()
c = Client(app, WerkzeugResponse)
server = 'http://127.0.0.1'
full_url = server + self.radicale_path
def x(session, method, url, data=None, headers=None, **kw):
path = urlparse.urlparse(url).path or self.radicale_path
path = urlparse.urlparse(url).path
assert isinstance(data, bytes) or data is None
r = c.open(path=path, method=method, data=data, headers=headers)
r = Response(r)
@ -142,7 +139,9 @@ class DavStorageTests(StorageTests):
self.patcher = p = mock.patch('requests.Session.request', new=x)
p.start()
return self.storage_class(url=full_url, **kwargs)
def get_storage_args(self, collection=None):
url = 'http://127.0.0.1/bob/'
return {'url': url, 'collection': collection}
def teardown_method(self, method):
self.app = None

View file

@ -10,7 +10,6 @@
from unittest import TestCase
from vdirsyncer.storage.base import Item
from vdirsyncer.storage.dav.caldav import CaldavStorage
from . import DavStorageTests
@ -46,7 +45,6 @@ END:VCALENDAR'''
class CaldavStorageTests(TestCase, DavStorageTests):
storage_class = CaldavStorage
radicale_path = '/bob/test.ics/'
item_template = TASK_TEMPLATE

View file

@ -10,14 +10,12 @@
from unittest import TestCase
from vdirsyncer.storage.base import Item
from vdirsyncer.storage.dav.carddav import CarddavStorage
from . import DavStorageTests
class CarddavStorageTests(TestCase, DavStorageTests):
storage_class = CarddavStorage
radicale_path = '/bob/test.vcf/'
item_template = (u'BEGIN:VCARD\n'
u'VERSION:3.0\n'

View file

@ -9,20 +9,18 @@
'''
from unittest import TestCase
import tempfile
import shutil
import pytest
import os
from vdirsyncer.storage.filesystem import FilesystemStorage
from . import StorageTests
@pytest.mark.usefixtures('class_tmpdir')
class FilesystemStorageTests(TestCase, StorageTests):
tmpdir = None
storage_class = FilesystemStorage
def _get_storage(self, **kwargs):
path = self.tmpdir = tempfile.mkdtemp()
return FilesystemStorage(path=path, fileext='.txt', **kwargs)
def teardown_method(self, method):
if self.tmpdir is not None:
shutil.rmtree(self.tmpdir)
self.tmpdir = None
def get_storage_args(self, collection=None):
path = self.tmpdir
if collection is not None:
os.makedirs(os.path.join(path, collection))
return {'path': path, 'fileext': '.txt', 'collection': collection}

View file

@ -8,12 +8,10 @@
'''
import mock
import requests
from unittest import TestCase
from . import StorageTests
from .. import assert_item_equals
from textwrap import dedent
from vdirsyncer.storage.http import HttpStorage, Item, split_collection
from vdirsyncer.storage.http import HttpStorage, Item
class HttpStorageTests(TestCase):
@ -31,7 +29,6 @@ class HttpStorageTests(TestCase):
METHOD:PUBLISH
BEGIN:VEVENT
UID:461092315540@example.com
ORGANIZER;CN="Alice Balder, Example Inc.":MAILTO:alice@example.com
LOCATION:Somewhere
SUMMARY:Eine Kurzinfo
DESCRIPTION:Beschreibung des Termines
@ -48,7 +45,7 @@ class HttpStorageTests(TestCase):
BEGIN:VALARM
ACTION:AUDIO
TRIGGER:19980403T120000
ATTACH;FMTTYPE=audio/basic:http://host.com/pub/audio-files/ssbanner.aud
ATTACH;FMTTYPE=audio/basic:http://host.com/pub/ssbanner.aud
REPEAT:4
DURATION:PT1H
END:VALARM
@ -68,7 +65,6 @@ class HttpStorageTests(TestCase):
assert_item_equals(item, Item(dedent(u'''
BEGIN:VEVENT
UID:461092315540@example.com
ORGANIZER;CN="Alice Balder, Example Inc.":MAILTO:alice@example.com
LOCATION:Somewhere
SUMMARY:Eine Kurzinfo
DESCRIPTION:Beschreibung des Termines
@ -89,7 +85,7 @@ class HttpStorageTests(TestCase):
BEGIN:VALARM
ACTION:AUDIO
TRIGGER:19980403T120000
ATTACH;FMTTYPE=audio/basic:http://host.com/pub/audio-files/ssbanner.aud
ATTACH;FMTTYPE=audio/basic:http://host.com/pub/ssbanner.aud
REPEAT:4
DURATION:PT1H
END:VALARM

View file

@ -15,5 +15,13 @@ from . import StorageTests
class MemoryStorageTests(TestCase, StorageTests):
def _get_storage(self, **kwargs):
return MemoryStorage(**kwargs)
storage_class = MemoryStorage
def get_storage_args(self, **kwargs):
return kwargs
def test_discover(self):
'''This test doesn't make any sense here.'''
def test_collection_arg(self):
'''This test doesn't make any sense here.'''

View file

@ -144,7 +144,8 @@ def _main(env, file_cfg):
actions = []
for pair_name in pairs:
try:
a_name, b_name, pair_options, storage_defaults = all_pairs[pair_name]
a_name, b_name, pair_options, storage_defaults = \
all_pairs[pair_name]
except KeyError:
cli_logger.critical('Pair not found: {}'.format(pair_name))
cli_logger.critical('These are the pairs found: ')

View file

@ -30,15 +30,31 @@ class Storage(object):
Terminology:
- UID: Global identifier of the item, across storages.
- HREF: Per-storage identifier of item, might be UID.
- HREF: Per-storage identifier of item, might be UID. The reason items
aren't just referenced by their UID is because the CalDAV and CardDAV
specifications make this imperformant to implement.
- ETAG: Checksum of item, or something similar that changes when the
object does.
object does.
:param collection: If None, the given URL or path is already directly
referring to a collection. Otherwise it will be treated as a basepath
to many collections (e.g. a vdir) and the given collection name will be
looked for.
'''
fileext = '.txt'
_repr_attributes = ()
def __init__(self, item_class=Item):
self.item_class = item_class
@classmethod
def discover(cls, **kwargs):
'''
Discover collections given a basepath or -URL to many collections.
:param **kwargs: Keyword arguments to additionally pass to the storage
instances returned. You shouldn't pass `collection` here, otherwise
TypeError will be raised.
:returns: Iterable of storages which represent the discovered
collections, all of which are passed kwargs during initialization.
'''
raise NotImplementedError()
def _get_href(self, uid):
return uid + self.fileext

View file

@ -16,12 +16,20 @@ from lxml import etree
class DavStorage(Storage):
# the file extension of items. Useful for testing against radicale.
fileext = None
# mimetype of items
item_mimetype = None
# The expected header for resource validation.
dav_header = None
# XML to use when fetching multiple hrefs.
get_multi_template = None
# The LXML query for extracting results in get_multi
get_multi_data_query = None
list_xml = None
# The leif class to use for autodiscovery
# This should be the class *name* (i.e. "module attribute name") instead of
# the class, because leif is an optional dependency
leif_class = None
_session = None
_repr_attributes = ('url', 'username')
@ -29,12 +37,13 @@ class DavStorage(Storage):
def __init__(self, url, username='', password='', collection=None,
verify=True, auth='basic', useragent='vdirsyncer', **kwargs):
'''
:param url: Direct URL for the CalDAV collection. No autodiscovery.
:param url: Base URL or an URL to a collection. Autodiscovery should be
done via :py:meth:`DavStorage.discover`.
:param username: Username for authentication.
:param password: Password for authentication.
:param verify: Verify SSL certificate, default True.
:param auth: Authentication method, from {'basic', 'digest'}, default
'basic'.
'basic'.
:param useragent: Default 'vdirsyncer'.
'''
super(DavStorage, self).__init__(**kwargs)
@ -56,6 +65,7 @@ class DavStorage(Storage):
url = urlparse.urljoin(url, collection)
self.url = url.rstrip('/') + '/'
self.parsed_url = urlparse.urlparse(self.url)
self.collection = collection
headers = self._default_headers()
headers['Depth'] = 1
@ -68,6 +78,25 @@ class DavStorage(Storage):
if self.dav_header not in response.headers.get('DAV', ''):
raise exceptions.StorageError('URL is not a collection')
@classmethod
def discover(cls, url, **kwargs):
if kwargs.pop('collection', None) is not None:
raise TypeError('collection argument must not be given.')
from leif import leif
d = getattr(leif, cls.leif_class)(
url,
user=kwargs.get('username', None),
password=kwargs.get('password', None),
ssl_verify=kwargs.get('verify', True)
)
for c in d.discover():
collection = c['href']
if collection.startswith(url):
collection = collection[len(url):]
s = cls(url=url, collection=collection, **kwargs)
s.displayname = c['displayname']
yield s
def _normalize_href(self, href):
'''Normalize the href to be a path only relative to hostname and
schema.'''
@ -180,6 +209,8 @@ class DavStorage(Storage):
def update(self, href, obj, etag):
href = self._normalize_href(href)
if etag is None:
raise ValueError('etag must be given and must not be None.')
return self._put(href, obj, etag)
def upload(self, obj):

View file

@ -21,9 +21,10 @@ class CaldavStorage(DavStorage):
fileext = '.ics'
item_mimetype = 'text/calendar'
dav_header = 'calendar-access'
leif_class = 'CalDiscover'
start_date = None
end_date = None
item_types = None
get_multi_template = '''<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-multiget xmlns:D="DAV:"
@ -42,6 +43,10 @@ class CaldavStorage(DavStorage):
'''
:param start_date: Start date of timerange to show, default -inf.
:param end_date: End date of timerange to show, default +inf.
:param item_types: The item types to show from the server. Dependent on
server functionality, no clientside validation of results. This
currently only affects the `list` method, but this shouldn't cause
problems in the normal usecase.
'''
super(CaldavStorage, self).__init__(**kwargs)
if isinstance(item_types, str):

View file

@ -17,6 +17,7 @@ class CarddavStorage(DavStorage):
fileext = '.vcf'
item_mimetype = 'text/vcard'
dav_header = 'addressbook'
leif_class = 'CardDiscover'
get_multi_template = '''<?xml version="1.0" encoding="utf-8" ?>
<C:addressbook-multiget xmlns:D="DAV:"

View file

@ -23,16 +23,34 @@ class FilesystemStorage(Storage):
_repr_attributes = ('path',)
def __init__(self, path, fileext, collection=None, encoding='utf-8', **kwargs):
def __init__(self, path, fileext, collection=None, encoding='utf-8',
**kwargs):
'''
:param path: Absolute path to a *collection* inside a vdir.
:param path: Absolute path to a vdir or collection, depending on the
collection parameter (see
:py:class:`vdirsyncer.storage.base.Storage`).
:param fileext: The file extension to use (e.g. `".txt"`). Contained in
the href, so if you change the file extension after a sync, this
will trigger a re-download of everything (but *should* not cause
data-loss of any kind).
:param encoding: File encoding for items.
'''
self.path = expand_path(path)
super(FilesystemStorage, self).__init__(**kwargs)
if collection is not None:
self.path = os.path.join(self.path, collection)
path = os.path.join(path, collection)
self.collection = collection
self.path = expand_path(path)
self.encoding = encoding
self.fileext = fileext
super(FilesystemStorage, self).__init__(**kwargs)
@classmethod
def discover(cls, path, **kwargs):
if kwargs.pop('collection', None) is not None:
raise TypeError('collection argument must not be given.')
for collection in os.listdir(path):
s = cls(path=path, collection=collection, **kwargs)
if next(s.list(), None) is not None:
yield s
def _get_filepath(self, href):
return os.path.join(self.path, href)