mirror of
https://github.com/samsonjs/vdirsyncer.git
synced 2026-04-27 14:57:41 +00:00
Auto merge of #399 - pimutils:google, r=untitaker
Google contacts and calendar - [x] Proper documentation - [x] With the refactor, `get_storage_init_args` now misses a lot of arguments for all DAV and Google storage types Fix #8
This commit is contained in:
commit
77d5a7d655
9 changed files with 264 additions and 69 deletions
|
|
@ -9,6 +9,12 @@ Package maintainers and users who have to manually update their installation
|
||||||
may want to subscribe to `GitHub's tag feed
|
may want to subscribe to `GitHub's tag feed
|
||||||
<https://github.com/pimutils/vdirsyncer/tags.atom>`_.
|
<https://github.com/pimutils/vdirsyncer/tags.atom>`_.
|
||||||
|
|
||||||
|
Version 0.10.0
|
||||||
|
==============
|
||||||
|
|
||||||
|
- New storage types :storage:`google_calendar` and :storage:`google_contacts`
|
||||||
|
have been added.
|
||||||
|
|
||||||
Version 0.9.3
|
Version 0.9.3
|
||||||
=============
|
=============
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -116,13 +116,6 @@ Storage Section
|
||||||
Supported Storages
|
Supported Storages
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
Read-write storages
|
|
||||||
~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
These storages generally support reading and changing of their items. Their
|
|
||||||
default value for ``read_only`` is ``false``, but can be set to ``true`` if
|
|
||||||
wished.
|
|
||||||
|
|
||||||
CalDAV and CardDAV
|
CalDAV and CardDAV
|
||||||
++++++++++++++++++
|
++++++++++++++++++
|
||||||
|
|
||||||
|
|
@ -130,6 +123,20 @@ CalDAV and CardDAV
|
||||||
|
|
||||||
.. autostorage:: vdirsyncer.storage.dav.CarddavStorage
|
.. autostorage:: vdirsyncer.storage.dav.CarddavStorage
|
||||||
|
|
||||||
|
Google
|
||||||
|
++++++
|
||||||
|
|
||||||
|
At first run you will be asked to authorize application for google account
|
||||||
|
access.
|
||||||
|
|
||||||
|
To use this storage type, you need to install some additional dependencies::
|
||||||
|
|
||||||
|
pip install vdirsyncer[google]
|
||||||
|
|
||||||
|
.. autostorage:: vdirsyncer.storage.google.GoogleCalendarStorage
|
||||||
|
|
||||||
|
.. autostorage:: vdirsyncer.storage.google.GoogleContactsStorage
|
||||||
|
|
||||||
remoteStorage
|
remoteStorage
|
||||||
+++++++++++++
|
+++++++++++++
|
||||||
|
|
||||||
|
|
@ -157,7 +164,7 @@ Local
|
||||||
|
|
||||||
|
|
||||||
Read-only storages
|
Read-only storages
|
||||||
~~~~~~~~~~~~~~~~~~
|
++++++++++++++++++
|
||||||
|
|
||||||
These storages don't support writing of their items, consequently ``read_only``
|
These storages don't support writing of their items, consequently ``read_only``
|
||||||
is set to ``true`` by default. Changing ``read_only`` to ``false`` on them
|
is set to ``true`` by default. Changing ``read_only`` to ``false`` on them
|
||||||
|
|
|
||||||
|
|
@ -207,5 +207,25 @@ Vdirsyncer is continuously tested against the latest version of Baikal_.
|
||||||
Google
|
Google
|
||||||
------
|
------
|
||||||
|
|
||||||
Vdirsyncer doesn't currently support Google accounts fully. For possible
|
Using vdirsyncer with Google Calendar is possible, but it is not tested
|
||||||
solutions see :gh:`202` and :gh:`8`.
|
frequently.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
[storage cal]
|
||||||
|
type = google_calendar
|
||||||
|
token_file = ~/.google-token
|
||||||
|
|
||||||
|
[storage card]
|
||||||
|
type = google_contacts
|
||||||
|
token_file = ~/.google-token
|
||||||
|
|
||||||
|
At first run you will be asked to authorize application for google account
|
||||||
|
access. Simply follow the instructions.
|
||||||
|
|
||||||
|
- Google's CardDav implementation is very limited, may lead to data loss, use
|
||||||
|
with care.
|
||||||
|
- You can select which calendars to sync on `CalDav settings page
|
||||||
|
<https://calendar.google.com/calendar/syncselect>`_
|
||||||
|
|
||||||
|
For more information see :gh:`202` and :gh:`8`.
|
||||||
|
|
|
||||||
3
setup.py
3
setup.py
|
|
@ -80,7 +80,8 @@ setup(
|
||||||
},
|
},
|
||||||
install_requires=requirements,
|
install_requires=requirements,
|
||||||
extras_require={
|
extras_require={
|
||||||
'remotestorage': ['requests-oauthlib']
|
'remotestorage': ['requests-oauthlib'],
|
||||||
|
'google': ['requests-oauthlib'],
|
||||||
},
|
},
|
||||||
cmdclass={
|
cmdclass={
|
||||||
'minimal_requirements': PrintRequirements
|
'minimal_requirements': PrintRequirements
|
||||||
|
|
|
||||||
|
|
@ -23,20 +23,10 @@ def no_debug_output(request):
|
||||||
logger.setLevel(logging.WARNING)
|
logger.setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
|
||||||
def test_get_class_init_args():
|
def test_get_storage_init_args():
|
||||||
class Foobar(object):
|
|
||||||
def __init__(self, foo, bar, baz=None):
|
|
||||||
pass
|
|
||||||
|
|
||||||
all, required = utils.get_class_init_args(Foobar)
|
|
||||||
assert all == {'foo', 'bar', 'baz'}
|
|
||||||
assert required == {'foo', 'bar'}
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_class_init_args_on_storage():
|
|
||||||
from vdirsyncer.storage.memory import MemoryStorage
|
from vdirsyncer.storage.memory import MemoryStorage
|
||||||
|
|
||||||
all, required = utils.get_class_init_args(MemoryStorage)
|
all, required = utils.get_storage_init_args(MemoryStorage)
|
||||||
assert all == set(['fileext', 'collection', 'read_only', 'instance_name'])
|
assert all == set(['fileext', 'collection', 'read_only', 'instance_name'])
|
||||||
assert not required
|
assert not required
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import click_threading
|
||||||
from . import cli_logger
|
from . import cli_logger
|
||||||
from .. import DOCS_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_storage_init_args
|
||||||
from ..utils.compat import to_native
|
from ..utils.compat import to_native
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -46,6 +46,8 @@ class _StorageIndex(object):
|
||||||
'vdirsyncer.storage.remotestorage.RemoteStorageContacts'),
|
'vdirsyncer.storage.remotestorage.RemoteStorageContacts'),
|
||||||
remotestorage_calendars=(
|
remotestorage_calendars=(
|
||||||
'vdirsyncer.storage.remotestorage.RemoteStorageCalendars'),
|
'vdirsyncer.storage.remotestorage.RemoteStorageCalendars'),
|
||||||
|
google_calendar='vdirsyncer.storage.google.GoogleCalendarStorage',
|
||||||
|
google_contacts='vdirsyncer.storage.google.GoogleContactsStorage'
|
||||||
)
|
)
|
||||||
|
|
||||||
def __getitem__(self, name):
|
def __getitem__(self, name):
|
||||||
|
|
@ -380,7 +382,7 @@ def handle_storage_init_error(cls, config):
|
||||||
if isinstance(e, (click.Abort, exceptions.UserError, KeyboardInterrupt)):
|
if isinstance(e, (click.Abort, exceptions.UserError, KeyboardInterrupt)):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
all, required = get_class_init_args(cls)
|
all, required = get_storage_init_args(cls)
|
||||||
given = set(config)
|
given = set(config)
|
||||||
missing = required - given
|
missing = required - given
|
||||||
invalid = given - all
|
invalid = given - all
|
||||||
|
|
@ -488,9 +490,9 @@ def format_storage_config(cls, header=True):
|
||||||
yield 'type = {}'.format(cls.storage_name)
|
yield 'type = {}'.format(cls.storage_name)
|
||||||
|
|
||||||
from ..storage.base import Storage
|
from ..storage.base import Storage
|
||||||
from ..utils import get_class_init_specs
|
from ..utils import get_storage_init_specs
|
||||||
handled = set()
|
handled = set()
|
||||||
for spec in get_class_init_specs(cls, stop_at=Storage):
|
for spec in get_storage_init_specs(cls, stop_at=Storage):
|
||||||
defaults = spec.defaults or ()
|
defaults = spec.defaults or ()
|
||||||
defaults = dict(zip(spec.args[-len(defaults):], defaults))
|
defaults = dict(zip(spec.args[-len(defaults):], defaults))
|
||||||
for key in spec.args[1:]:
|
for key in spec.args[1:]:
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ from .base import Item, Storage, normalize_meta_value
|
||||||
from .http import HTTP_STORAGE_PARAMETERS, USERAGENT, prepare_auth, \
|
from .http import HTTP_STORAGE_PARAMETERS, USERAGENT, prepare_auth, \
|
||||||
prepare_client_cert, prepare_verify
|
prepare_client_cert, prepare_verify
|
||||||
from .. import exceptions, utils
|
from .. import exceptions, utils
|
||||||
from ..utils.compat import text_type, to_native
|
from ..utils.compat import PY2, getargspec_ish, text_type, to_native
|
||||||
|
|
||||||
|
|
||||||
dav_logger = logging.getLogger(__name__)
|
dav_logger = logging.getLogger(__name__)
|
||||||
|
|
@ -103,11 +103,6 @@ def _fuzzy_matches_mimetype(strict, weak):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _get_collection_from_url(url):
|
|
||||||
_, collection = url.rstrip('/').rsplit('/', 1)
|
|
||||||
return utils.compat.urlunquote(collection)
|
|
||||||
|
|
||||||
|
|
||||||
class Discover(object):
|
class Discover(object):
|
||||||
_namespace = None
|
_namespace = None
|
||||||
_resourcetype = None
|
_resourcetype = None
|
||||||
|
|
@ -122,18 +117,18 @@ class Discover(object):
|
||||||
</d:propfind>
|
</d:propfind>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, session, kwargs):
|
||||||
if kwargs.pop('collection', None) is not None:
|
if kwargs.pop('collection', None) is not None:
|
||||||
raise TypeError('collection argument must not be given.')
|
raise TypeError('collection argument must not be given.')
|
||||||
|
|
||||||
discover_args, _ = utils.split_dict(kwargs, lambda key: key in (
|
self.session = session
|
||||||
'url', 'username', 'password', 'verify', 'auth', 'useragent',
|
|
||||||
'verify_fingerprint', 'auth_cert',
|
|
||||||
))
|
|
||||||
|
|
||||||
self.session = DavSession(**discover_args)
|
|
||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_collection_from_url(url):
|
||||||
|
_, collection = url.rstrip('/').rsplit('/', 1)
|
||||||
|
return utils.compat.urlunquote(collection)
|
||||||
|
|
||||||
def find_dav(self):
|
def find_dav(self):
|
||||||
try:
|
try:
|
||||||
response = self.session.request(
|
response = self.session.request(
|
||||||
|
|
@ -212,14 +207,14 @@ class Discover(object):
|
||||||
def discover(self):
|
def discover(self):
|
||||||
for c in self.find_collections():
|
for c in self.find_collections():
|
||||||
url = c['href']
|
url = c['href']
|
||||||
collection = _get_collection_from_url(url)
|
collection = self._get_collection_from_url(url)
|
||||||
storage_args = dict(self.kwargs)
|
storage_args = dict(self.kwargs)
|
||||||
storage_args.update({'url': url, 'collection': collection})
|
storage_args.update({'url': url, 'collection': collection})
|
||||||
yield storage_args
|
yield storage_args
|
||||||
|
|
||||||
def create(self, collection):
|
def create(self, collection):
|
||||||
if collection is None:
|
if collection is None:
|
||||||
collection = _get_collection_from_url(self.kwargs['url'])
|
collection = self._get_collection_from_url(self.kwargs['url'])
|
||||||
|
|
||||||
for c in self.discover():
|
for c in self.discover():
|
||||||
if c['collection'] == collection:
|
if c['collection'] == collection:
|
||||||
|
|
@ -297,26 +292,36 @@ class DavSession(object):
|
||||||
A helper class to connect to DAV servers.
|
A helper class to connect to DAV servers.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def init_and_remaining_args(cls, **kwargs):
|
||||||
|
argspec = getargspec_ish(cls.__init__)
|
||||||
|
self_args, remainder = \
|
||||||
|
utils.split_dict(kwargs, argspec.args.__contains__)
|
||||||
|
|
||||||
|
return cls(**self_args), remainder
|
||||||
|
|
||||||
def __init__(self, url, username='', password='', verify=True, auth=None,
|
def __init__(self, url, username='', password='', verify=True, auth=None,
|
||||||
useragent=USERAGENT, verify_fingerprint=None,
|
useragent=USERAGENT, verify_fingerprint=None,
|
||||||
auth_cert=None):
|
auth_cert=None):
|
||||||
self._settings = {
|
self._settings = {
|
||||||
'auth': prepare_auth(auth, username, password),
|
|
||||||
'cert': prepare_client_cert(auth_cert),
|
'cert': prepare_client_cert(auth_cert),
|
||||||
|
'auth': prepare_auth(auth, username, password)
|
||||||
}
|
}
|
||||||
self._settings.update(prepare_verify(verify, verify_fingerprint))
|
self._settings.update(prepare_verify(verify, verify_fingerprint))
|
||||||
|
|
||||||
self.useragent = useragent
|
self.useragent = useragent
|
||||||
self.url = url.rstrip('/') + '/'
|
self.url = url.rstrip('/') + '/'
|
||||||
self.parsed_url = utils.compat.urlparse.urlparse(self.url)
|
|
||||||
self._session = None
|
self._session = requests.session()
|
||||||
|
|
||||||
|
@utils.cached_property
|
||||||
|
def parsed_url(self):
|
||||||
|
return utils.compat.urlparse.urlparse(self.url)
|
||||||
|
|
||||||
def request(self, method, path, **kwargs):
|
def request(self, method, path, **kwargs):
|
||||||
url = self.url
|
url = self.url
|
||||||
if path:
|
if path:
|
||||||
url = utils.compat.urlparse.urljoin(self.url, path)
|
url = utils.compat.urlparse.urljoin(self.url, path)
|
||||||
if self._session is None:
|
|
||||||
self._session = requests.session()
|
|
||||||
|
|
||||||
more = dict(self._settings)
|
more = dict(self._settings)
|
||||||
more.update(kwargs)
|
more.update(kwargs)
|
||||||
|
|
@ -334,8 +339,6 @@ class DavStorage(Storage):
|
||||||
__doc__ = '''
|
__doc__ = '''
|
||||||
:param url: Base URL or an URL to a collection.
|
:param url: Base URL or an URL to a collection.
|
||||||
''' + HTTP_STORAGE_PARAMETERS + '''
|
''' + HTTP_STORAGE_PARAMETERS + '''
|
||||||
:param unsafe_href_chars: Replace the given characters when generating
|
|
||||||
hrefs. Defaults to ``'@'``.
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
|
|
@ -353,36 +356,38 @@ class DavStorage(Storage):
|
||||||
get_multi_data_query = None
|
get_multi_data_query = None
|
||||||
# The Discover subclass to use
|
# The Discover subclass to use
|
||||||
discovery_class = None
|
discovery_class = None
|
||||||
|
# The DavSession class to use
|
||||||
|
session_class = DavSession
|
||||||
|
|
||||||
_session = None
|
|
||||||
_repr_attributes = ('username', 'url')
|
_repr_attributes = ('username', 'url')
|
||||||
|
|
||||||
_property_table = {
|
_property_table = {
|
||||||
'displayname': ('displayname', 'DAV:'),
|
'displayname': ('displayname', 'DAV:'),
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, url, username='', password='', verify=True, auth=None,
|
def __init__(self, **kwargs):
|
||||||
useragent=USERAGENT, verify_fingerprint=None, auth_cert=None,
|
# defined for _repr_attributes
|
||||||
**kwargs):
|
self.username = kwargs.get('username')
|
||||||
|
self.url = kwargs.get('url')
|
||||||
|
|
||||||
|
self.session, kwargs = \
|
||||||
|
self.session_class.init_and_remaining_args(**kwargs)
|
||||||
super(DavStorage, self).__init__(**kwargs)
|
super(DavStorage, self).__init__(**kwargs)
|
||||||
|
|
||||||
url = url.rstrip('/') + '/'
|
if not PY2:
|
||||||
self.session = DavSession(url, username, password, verify, auth,
|
import inspect
|
||||||
useragent, verify_fingerprint,
|
__init__.__signature__ = inspect.signature(session_class.__init__)
|
||||||
auth_cert)
|
|
||||||
|
|
||||||
# defined for _repr_attributes
|
|
||||||
self.username = username
|
|
||||||
self.url = url
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def discover(cls, **kwargs):
|
def discover(cls, **kwargs):
|
||||||
d = cls.discovery_class(**kwargs)
|
session, _ = cls.session_class.init_and_remaining_args(**kwargs)
|
||||||
|
d = cls.discovery_class(session, kwargs)
|
||||||
return d.discover()
|
return d.discover()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_collection(cls, collection, **kwargs):
|
def create_collection(cls, collection, **kwargs):
|
||||||
d = cls.discovery_class(**kwargs)
|
session, _ = cls.session_class.init_and_remaining_args(**kwargs)
|
||||||
|
d = cls.discovery_class(session, kwargs)
|
||||||
return d.create(collection)
|
return d.create(collection)
|
||||||
|
|
||||||
def _normalize_href(self, *args, **kwargs):
|
def _normalize_href(self, *args, **kwargs):
|
||||||
|
|
|
||||||
158
vdirsyncer/storage/google.py
Normal file
158
vdirsyncer/storage/google.py
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
from . import dav
|
||||||
|
from .. import exceptions, utils
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
TOKEN_URL = 'https://accounts.google.com/o/oauth2/v2/auth'
|
||||||
|
REFRESH_URL = 'https://www.googleapis.com/oauth2/v4/token'
|
||||||
|
|
||||||
|
CLIENT_ID = ('155040592229-mth5eiq7qt9dtk46j0vcnoometuab9mb'
|
||||||
|
'.apps.googleusercontent.com')
|
||||||
|
CLIENT_SECRET = 'flVXF7jB2A2-YC4rg2sUCCX1'
|
||||||
|
|
||||||
|
try:
|
||||||
|
from requests_oauthlib import OAuth2Session
|
||||||
|
have_oauth2 = True
|
||||||
|
except ImportError:
|
||||||
|
have_oauth2 = False
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleSession(dav.DavSession):
|
||||||
|
def __init__(self, token_file, client_id=None, client_secret=None):
|
||||||
|
# Not a default in function signature, otherwise these show up in user
|
||||||
|
# documentation
|
||||||
|
client_id = client_id or CLIENT_ID
|
||||||
|
client_secret = client_secret or CLIENT_SECRET
|
||||||
|
|
||||||
|
self.useragent = client_id
|
||||||
|
self._settings = {}
|
||||||
|
|
||||||
|
if not have_oauth2:
|
||||||
|
raise exceptions.UserError('requests-oauthlib not installed')
|
||||||
|
|
||||||
|
token = None
|
||||||
|
try:
|
||||||
|
with open(token_file) as f:
|
||||||
|
token = json.load(f)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _save_token(token):
|
||||||
|
with open(token_file, 'w') as f:
|
||||||
|
json.dump(token, f)
|
||||||
|
|
||||||
|
self._session = OAuth2Session(
|
||||||
|
client_id=client_id,
|
||||||
|
token=token,
|
||||||
|
redirect_uri='urn:ietf:wg:oauth:2.0:oob',
|
||||||
|
scope=self.scope,
|
||||||
|
auto_refresh_url=REFRESH_URL,
|
||||||
|
auto_refresh_kwargs={
|
||||||
|
'client_id': client_id,
|
||||||
|
'client_secret': client_secret,
|
||||||
|
},
|
||||||
|
token_updater=_save_token
|
||||||
|
)
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
authorization_url, state = self._session.authorization_url(
|
||||||
|
TOKEN_URL,
|
||||||
|
# access_type and approval_prompt are Google specific
|
||||||
|
# extra parameters.
|
||||||
|
access_type='offline', approval_prompt='force')
|
||||||
|
click.echo('Opening {} ...'.format(authorization_url))
|
||||||
|
try:
|
||||||
|
utils.open_graphical_browser(authorization_url)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(str(e))
|
||||||
|
|
||||||
|
click.echo("Follow the instructions on the page.")
|
||||||
|
code = click.prompt("Paste obtained code")
|
||||||
|
token = self._session.fetch_token(
|
||||||
|
REFRESH_URL,
|
||||||
|
code=code,
|
||||||
|
# Google specific extra parameter used for client
|
||||||
|
# authentication
|
||||||
|
client_secret=client_secret,
|
||||||
|
)
|
||||||
|
# FIXME: Ugly
|
||||||
|
_save_token(token)
|
||||||
|
|
||||||
|
|
||||||
|
GOOGLE_PARAMS_DOCS = '''
|
||||||
|
:param token_file: A filepath where access tokens are stored.
|
||||||
|
:param client_id/client_secret: OAuth credentials. Hardcoded ones are
|
||||||
|
provided, you shouldn't need this unless you hit API rate limits.
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleCalendarStorage(dav.CaldavStorage):
|
||||||
|
__doc__ = '''Google calendar.
|
||||||
|
|
||||||
|
Please refer to :storage:`caldav` regarding
|
||||||
|
the ``item_types`` and timerange parameters.
|
||||||
|
''' + GOOGLE_PARAMS_DOCS
|
||||||
|
|
||||||
|
class session_class(GoogleSession):
|
||||||
|
url = 'https://apidata.googleusercontent.com/caldav/v2/'
|
||||||
|
scope = ['https://www.googleapis.com/auth/calendar']
|
||||||
|
|
||||||
|
class discovery_class(dav.CalDiscover):
|
||||||
|
@staticmethod
|
||||||
|
def _get_collection_from_url(url):
|
||||||
|
# Google CalDAV has collection URLs like:
|
||||||
|
# /user/foouser/calendars/foocalendar/events/
|
||||||
|
parts = url.rstrip('/').split('/')
|
||||||
|
parts.pop()
|
||||||
|
collection = parts.pop()
|
||||||
|
return utils.compat.urlunquote(collection)
|
||||||
|
|
||||||
|
storage_name = 'google_calendar'
|
||||||
|
|
||||||
|
def __init__(self, token_file, client_id=None, client_secret=None,
|
||||||
|
start_date=None, end_date=None, item_types=(), **kwargs):
|
||||||
|
super(GoogleContactsStorage, self).__init__(
|
||||||
|
token_file=token_file, client_id=client_id,
|
||||||
|
client_secret=client_secret, start_date=start_date,
|
||||||
|
end_date=end_date, item_types=item_types,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
# This is ugly: We define/override the entire signature computed for the
|
||||||
|
# docs here because the current way we autogenerate those docs are too
|
||||||
|
# simple for our advanced argspec juggling in `vdirsyncer.storage.dav`.
|
||||||
|
__init__._traverse_superclass = False
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleContactsStorage(dav.CarddavStorage):
|
||||||
|
__doc__ = '''Google contacts.
|
||||||
|
''' + GOOGLE_PARAMS_DOCS
|
||||||
|
|
||||||
|
class session_class(GoogleSession):
|
||||||
|
# Apparently Google wants us to submit a PROPFIND to the well-known
|
||||||
|
# URL, instead of looking for a redirect.
|
||||||
|
url = 'https://www.googleapis.com/.well-known/carddav/'
|
||||||
|
scope = ['https://www.googleapis.com/auth/carddav']
|
||||||
|
|
||||||
|
storage_name = 'google_contacts'
|
||||||
|
|
||||||
|
def __init__(self, token_file, client_id=None, client_secret=None,
|
||||||
|
**kwargs):
|
||||||
|
super(GoogleContactsStorage, self).__init__(
|
||||||
|
token_file=token_file, client_id=client_id,
|
||||||
|
client_secret=client_secret,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
# This is ugly: We define/override the entire signature computed for the
|
||||||
|
# docs here because the current way we autogenerate those docs are too
|
||||||
|
# simple for our advanced argspec juggling in `vdirsyncer.storage.dav`.
|
||||||
|
__init__._traverse_superclass = False
|
||||||
|
|
@ -82,16 +82,22 @@ def get_etag_from_fileobject(f):
|
||||||
return get_etag_from_file(f.name)
|
return get_etag_from_file(f.name)
|
||||||
|
|
||||||
|
|
||||||
def get_class_init_specs(cls, stop_at=object):
|
def get_storage_init_specs(cls, stop_at=object):
|
||||||
if cls is stop_at:
|
if cls is stop_at:
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
spec = getargspec_ish(cls.__init__)
|
spec = getargspec_ish(cls.__init__)
|
||||||
supercls = next(getattr(x.__init__, '__objclass__', x)
|
if getattr(cls.__init__, '_traverse_superclass', True):
|
||||||
for x in cls.__mro__[1:])
|
supercls = next(getattr(x.__init__, '__objclass__', x)
|
||||||
return (spec,) + get_class_init_specs(supercls, stop_at=stop_at)
|
for x in cls.__mro__[1:])
|
||||||
|
superspecs = get_storage_init_specs(supercls, stop_at=stop_at)
|
||||||
|
else:
|
||||||
|
superspecs = ()
|
||||||
|
|
||||||
|
return (spec,) + superspecs
|
||||||
|
|
||||||
|
|
||||||
def get_class_init_args(cls, stop_at=object):
|
def get_storage_init_args(cls, stop_at=object):
|
||||||
'''
|
'''
|
||||||
Get args which are taken during class initialization. Assumes that all
|
Get args which are taken during class initialization. Assumes that all
|
||||||
classes' __init__ calls super().__init__ with the rest of the arguments.
|
classes' __init__ calls super().__init__ with the rest of the arguments.
|
||||||
|
|
@ -102,7 +108,7 @@ def get_class_init_args(cls, stop_at=object):
|
||||||
requires.
|
requires.
|
||||||
'''
|
'''
|
||||||
all, required = set(), set()
|
all, required = set(), set()
|
||||||
for spec in get_class_init_specs(cls, stop_at=stop_at):
|
for spec in get_storage_init_specs(cls, stop_at=stop_at):
|
||||||
all.update(spec.args[1:])
|
all.update(spec.args[1:])
|
||||||
required.update(spec.args[1:-len(spec.defaults or ())])
|
required.update(spec.args[1:-len(spec.defaults or ())])
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue