mirror of
https://github.com/samsonjs/vdirsyncer.git
synced 2026-04-27 14:57:41 +00:00
Refactor Google support into own storage types
This commit is contained in:
parent
eca9faad16
commit
2888757e1b
9 changed files with 221 additions and 125 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -213,22 +213,19 @@ frequently.
|
||||||
::
|
::
|
||||||
|
|
||||||
[storage cal]
|
[storage cal]
|
||||||
type = caldav
|
type = google_calendar
|
||||||
url = https://apidata.googleusercontent.com/caldav/v2/
|
token_file = ~/.google-token
|
||||||
auth = oauth2_google
|
|
||||||
|
|
||||||
[storage card]
|
[storage card]
|
||||||
type = carddav
|
type = google_contacts
|
||||||
url = https://www.googleapis.com/carddav/v1/principals/EMAIL/lists/default
|
token_file = ~/.google-token
|
||||||
auth = oauth2_google
|
|
||||||
|
|
||||||
At first run you will be asked to authorize application for google account
|
At first run you will be asked to authorize application for google account
|
||||||
access. Simply follow the instructions. You'll be asked to modify configuration
|
access. Simply follow the instructions.
|
||||||
file (save `refresh_token` as a password).
|
|
||||||
|
|
||||||
- Google's CardDav implementation is very limited, may lead to data loss, use
|
- Google's CardDav implementation is very limited, may lead to data loss, use
|
||||||
with care.
|
with care.
|
||||||
- You can select which calendars to sync on
|
- You can select which calendars to sync on `CalDav settings page
|
||||||
`CalDav settings page <https://calendar.google.com/calendar/syncselect>`_
|
<https://calendar.google.com/calendar/syncselect>`_
|
||||||
|
|
||||||
For more information see :gh:`202` and :gh:`8`.
|
For more information see :gh:`202` and :gh:`8`.
|
||||||
|
|
|
||||||
2
setup.py
2
setup.py
|
|
@ -81,7 +81,7 @@ setup(
|
||||||
install_requires=requirements,
|
install_requires=requirements,
|
||||||
extras_require={
|
extras_require={
|
||||||
'remotestorage': ['requests-oauthlib'],
|
'remotestorage': ['requests-oauthlib'],
|
||||||
'oauth2': ['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:]:
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import click
|
|
||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
@ -14,17 +12,8 @@ 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 getargspec_ish, text_type, to_native
|
from ..utils.compat import PY2, getargspec_ish, text_type, to_native
|
||||||
|
|
||||||
OAUTH2_GOOGLE_TOKEN_URL = 'https://accounts.google.com/o/oauth2/v2/auth'
|
|
||||||
OAUTH2_GOOGLE_REFRESH_URL = 'https://www.googleapis.com/oauth2/v4/token'
|
|
||||||
have_oauth2 = True
|
|
||||||
try:
|
|
||||||
from requests_oauthlib import OAuth2Session
|
|
||||||
oauth2_client_id = 'FIXME'
|
|
||||||
oauth2_client_secret = 'FIXME'
|
|
||||||
except ImportError:
|
|
||||||
have_oauth2 = False
|
|
||||||
|
|
||||||
dav_logger = logging.getLogger(__name__)
|
dav_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -114,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
|
||||||
|
|
@ -140,6 +124,11 @@ class Discover(object):
|
||||||
self.session = session
|
self.session = session
|
||||||
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(
|
||||||
|
|
@ -218,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:
|
||||||
|
|
@ -306,7 +295,6 @@ class DavSession(object):
|
||||||
@classmethod
|
@classmethod
|
||||||
def init_and_remaining_args(cls, **kwargs):
|
def init_and_remaining_args(cls, **kwargs):
|
||||||
argspec = getargspec_ish(cls.__init__)
|
argspec = getargspec_ish(cls.__init__)
|
||||||
argspec.args
|
|
||||||
self_args, remainder = \
|
self_args, remainder = \
|
||||||
utils.split_dict(kwargs, argspec.args.__contains__)
|
utils.split_dict(kwargs, argspec.args.__contains__)
|
||||||
|
|
||||||
|
|
@ -317,73 +305,23 @@ class DavSession(object):
|
||||||
auth_cert=None):
|
auth_cert=None):
|
||||||
self._settings = {
|
self._settings = {
|
||||||
'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()
|
||||||
self._token = None
|
|
||||||
self._use_oauth2_google = False
|
@utils.cached_property
|
||||||
if auth == 'oauth2_google':
|
def parsed_url(self):
|
||||||
if not have_oauth2:
|
return utils.compat.urlparse.urlparse(self.url)
|
||||||
raise exceptions.UserError("requests-oauthlib not installed")
|
|
||||||
if password:
|
|
||||||
self._token = {
|
|
||||||
'refresh_token': password,
|
|
||||||
# Will be derived from refresh_token
|
|
||||||
'access_token': 'dummy',
|
|
||||||
'expires_in': -30
|
|
||||||
}
|
|
||||||
self._use_oauth2_google = True
|
|
||||||
else:
|
|
||||||
self._settings['auth'] = prepare_auth(auth, username, password)
|
|
||||||
|
|
||||||
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()
|
|
||||||
if self._use_oauth2_google:
|
|
||||||
self._session = OAuth2Session(
|
|
||||||
client_id=oauth2_client_id,
|
|
||||||
token=self._token,
|
|
||||||
redirect_uri='urn:ietf:wg:oauth:2.0:oob',
|
|
||||||
scope=['https://www.googleapis.com/auth/calendar',
|
|
||||||
'https://www.googleapis.com/auth/carddav'],
|
|
||||||
auto_refresh_url=OAUTH2_GOOGLE_REFRESH_URL,
|
|
||||||
auto_refresh_kwargs={
|
|
||||||
'client_id': oauth2_client_id,
|
|
||||||
'client_secret': oauth2_client_secret,
|
|
||||||
},
|
|
||||||
token_updater=lambda x: None
|
|
||||||
)
|
|
||||||
if not self._token:
|
|
||||||
authorization_url, state = self._session.authorization_url(
|
|
||||||
OAUTH2_GOOGLE_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:
|
|
||||||
dav_logger.warning(str(e))
|
|
||||||
|
|
||||||
click.echo("Follow the instructions on the page.")
|
|
||||||
code = click.prompt("Paste obtained code")
|
|
||||||
self._token = self._session.fetch_token(
|
|
||||||
OAUTH2_GOOGLE_REFRESH_URL,
|
|
||||||
code=code,
|
|
||||||
# Google specific extra parameter used for client
|
|
||||||
# authentication
|
|
||||||
client_secret=oauth2_client_secret,
|
|
||||||
)
|
|
||||||
raise exceptions.UserError(
|
|
||||||
"Set the following token in a password field: {}".
|
|
||||||
format(self._token['refresh_token']))
|
|
||||||
|
|
||||||
more = dict(self._settings)
|
more = dict(self._settings)
|
||||||
more.update(kwargs)
|
more.update(kwargs)
|
||||||
|
|
@ -401,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::
|
||||||
|
|
||||||
|
|
@ -432,12 +368,15 @@ class DavStorage(Storage):
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
# defined for _repr_attributes
|
# defined for _repr_attributes
|
||||||
self.username = kwargs.get('username')
|
self.username = kwargs.get('username')
|
||||||
self.url = kwargs['url']
|
self.url = kwargs.get('url')
|
||||||
|
|
||||||
self.session, kwargs = \
|
self.session, kwargs = \
|
||||||
self.session_class.init_and_remaining_args(**kwargs)
|
self.session_class.init_and_remaining_args(**kwargs)
|
||||||
super(DavStorage, self).__init__(**kwargs)
|
super(DavStorage, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
if not PY2:
|
||||||
|
import inspect
|
||||||
|
__init__.__signature__ = inspect.signature(session_class.__init__)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def discover(cls, **kwargs):
|
def discover(cls, **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,10 +82,7 @@ HTTP_STORAGE_PARAMETERS = '''
|
||||||
information.
|
information.
|
||||||
:param auth: Optional. Either ``basic``, ``digest`` or ``guess``. Default
|
:param auth: Optional. Either ``basic``, ``digest`` or ``guess``. Default
|
||||||
``guess``. If you know yours, consider setting it explicitly for
|
``guess``. If you know yours, consider setting it explicitly for
|
||||||
performance. For caldav and carddav, additionaly ``oauth2_google`` is
|
performance.
|
||||||
supported. ``password`` setting should point a file for OAuth2 token
|
|
||||||
storage (directory must already exists, but file itself will be created
|
|
||||||
automatically).
|
|
||||||
:param auth_cert: Optional. Either a path to a certificate with a client
|
:param auth_cert: Optional. Either a path to a certificate with a client
|
||||||
certificate and the key or a list of paths to the files with them.
|
certificate and the key or a list of paths to the files with them.
|
||||||
:param useragent: Default ``vdirsyncer``.
|
:param useragent: Default ``vdirsyncer``.
|
||||||
|
|
|
||||||
|
|
@ -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