mirror of
https://github.com/samsonjs/vdirsyncer.git
synced 2026-04-27 14:57:41 +00:00
Port google storage to use asyncio
This commit is contained in:
parent
8d69b73c9e
commit
dfed9794cb
4 changed files with 80 additions and 53 deletions
|
|
@ -14,6 +14,7 @@ packages:
|
||||||
- python-click-threading
|
- python-click-threading
|
||||||
- python-requests
|
- python-requests
|
||||||
- python-requests-toolbelt
|
- python-requests-toolbelt
|
||||||
|
- python-aiohttp-oauthlib
|
||||||
# Test dependencies:
|
# Test dependencies:
|
||||||
- python-hypothesis
|
- python-hypothesis
|
||||||
- python-pytest-cov
|
- python-pytest-cov
|
||||||
|
|
|
||||||
2
setup.py
2
setup.py
|
|
@ -56,7 +56,7 @@ setup(
|
||||||
install_requires=requirements,
|
install_requires=requirements,
|
||||||
# Optional dependencies
|
# Optional dependencies
|
||||||
extras_require={
|
extras_require={
|
||||||
"google": ["requests-oauthlib"],
|
"google": ["aiohttp-oauthlib"],
|
||||||
"etesync": ["etesync==0.5.2", "django<2.0"],
|
"etesync": ["etesync==0.5.2", "django<2.0"],
|
||||||
},
|
},
|
||||||
# Build dependencies
|
# Build dependencies
|
||||||
|
|
|
||||||
|
|
@ -411,12 +411,18 @@ class DAVSession:
|
||||||
|
|
||||||
# XXX: This is a temporary hack to pin-point bad refactoring.
|
# XXX: This is a temporary hack to pin-point bad refactoring.
|
||||||
assert self.connector is not None
|
assert self.connector is not None
|
||||||
async with aiohttp.ClientSession(
|
async with self._session as session:
|
||||||
|
return await http.request(method, url, session=session, **more)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _session(self):
|
||||||
|
"""Return a new session for requests."""
|
||||||
|
|
||||||
|
return aiohttp.ClientSession(
|
||||||
connector=self.connector,
|
connector=self.connector,
|
||||||
connector_owner=False,
|
connector_owner=False,
|
||||||
# TODO use `raise_for_status=true`, though this needs traces first,
|
# TODO use `raise_for_status=true`, though this needs traces first,
|
||||||
) as session:
|
)
|
||||||
return await http.request(method, url, session=session, **more)
|
|
||||||
|
|
||||||
def get_default_headers(self):
|
def get_default_headers(self):
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import urllib.parse as urlparse
|
import urllib.parse as urlparse
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import click
|
import click
|
||||||
|
|
@ -21,7 +22,7 @@ TOKEN_URL = "https://accounts.google.com/o/oauth2/v2/auth"
|
||||||
REFRESH_URL = "https://www.googleapis.com/oauth2/v4/token"
|
REFRESH_URL = "https://www.googleapis.com/oauth2/v4/token"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from requests_oauthlib import OAuth2Session
|
from aiohttp_oauthlib import OAuth2Session
|
||||||
|
|
||||||
have_oauth2 = True
|
have_oauth2 = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|
@ -37,6 +38,9 @@ class GoogleSession(dav.DAVSession):
|
||||||
url=None,
|
url=None,
|
||||||
connector: aiohttp.BaseConnector = None,
|
connector: aiohttp.BaseConnector = None,
|
||||||
):
|
):
|
||||||
|
if not have_oauth2:
|
||||||
|
raise exceptions.UserError("aiohttp-oauthlib not installed")
|
||||||
|
|
||||||
# Required for discovering collections
|
# Required for discovering collections
|
||||||
if url is not None:
|
if url is not None:
|
||||||
self.url = url
|
self.url = url
|
||||||
|
|
@ -45,45 +49,60 @@ class GoogleSession(dav.DAVSession):
|
||||||
self._settings = {}
|
self._settings = {}
|
||||||
self.connector = connector
|
self.connector = connector
|
||||||
|
|
||||||
if not have_oauth2:
|
self._token_file = Path(expand_path(token_file))
|
||||||
raise exceptions.UserError("requests-oauthlib not installed")
|
self._client_id = client_id
|
||||||
|
self._client_secret = client_secret
|
||||||
|
self._token = None
|
||||||
|
|
||||||
token_file = expand_path(token_file)
|
async def request(self, method, path, **kwargs):
|
||||||
return self._init_token(token_file, client_id, client_secret)
|
if not self._token:
|
||||||
|
await self._init_token()
|
||||||
|
|
||||||
def _init_token(self, token_file, client_id, client_secret):
|
return await super().request(method, path, **kwargs)
|
||||||
token = None
|
|
||||||
try:
|
|
||||||
with open(token_file) as f:
|
|
||||||
token = json.load(f)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
except ValueError as e:
|
|
||||||
raise exceptions.UserError(
|
|
||||||
"Failed to load token file {}, try deleting it. "
|
|
||||||
"Original error: {}".format(token_file, e)
|
|
||||||
)
|
|
||||||
|
|
||||||
def _save_token(token):
|
def _save_token(self, token):
|
||||||
checkdir(expand_path(os.path.dirname(token_file)), create=True)
|
"""Helper function called by OAuth2Session when a token is updated."""
|
||||||
with atomic_write(token_file, mode="w", overwrite=True) as f:
|
checkdir(expand_path(os.path.dirname(self._token_file)), create=True)
|
||||||
|
with atomic_write(self._token_file, mode="w", overwrite=True) as f:
|
||||||
json.dump(token, f)
|
json.dump(token, f)
|
||||||
|
|
||||||
self._session = OAuth2Session(
|
@property
|
||||||
client_id=client_id,
|
def _session(self):
|
||||||
token=token,
|
"""Return a new OAuth session for requests."""
|
||||||
|
|
||||||
|
return OAuth2Session(
|
||||||
|
client_id=self._client_id,
|
||||||
|
token=self._token,
|
||||||
redirect_uri="urn:ietf:wg:oauth:2.0:oob",
|
redirect_uri="urn:ietf:wg:oauth:2.0:oob",
|
||||||
scope=self.scope,
|
scope=self.scope,
|
||||||
auto_refresh_url=REFRESH_URL,
|
auto_refresh_url=REFRESH_URL,
|
||||||
auto_refresh_kwargs={
|
auto_refresh_kwargs={
|
||||||
"client_id": client_id,
|
"client_id": self._client_id,
|
||||||
"client_secret": client_secret,
|
"client_secret": self._client_secret,
|
||||||
},
|
},
|
||||||
token_updater=_save_token,
|
token_updater=lambda token: self._save_token(token),
|
||||||
|
connector=self.connector,
|
||||||
|
connector_owner=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not token:
|
async def _init_token(self):
|
||||||
authorization_url, state = self._session.authorization_url(
|
try:
|
||||||
|
with self._token_file.open() as f:
|
||||||
|
self._token = json.load(f)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
except ValueError as e:
|
||||||
|
raise exceptions.UserError(
|
||||||
|
"Failed to load token file {}, try deleting it. "
|
||||||
|
"Original error: {}".format(self._token_file, e)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not self._token:
|
||||||
|
# Some times a task stops at this `async`, and another continues the flow.
|
||||||
|
# At this point, the user has already completed the flow, but is prompeted
|
||||||
|
# for a second one.
|
||||||
|
async with self._session as session:
|
||||||
|
authorization_url, state = session.authorization_url(
|
||||||
TOKEN_URL,
|
TOKEN_URL,
|
||||||
# access_type and approval_prompt are Google specific
|
# access_type and approval_prompt are Google specific
|
||||||
# extra parameters.
|
# extra parameters.
|
||||||
|
|
@ -98,15 +117,16 @@ class GoogleSession(dav.DAVSession):
|
||||||
|
|
||||||
click.echo("Follow the instructions on the page.")
|
click.echo("Follow the instructions on the page.")
|
||||||
code = click.prompt("Paste obtained code")
|
code = click.prompt("Paste obtained code")
|
||||||
token = self._session.fetch_token(
|
|
||||||
|
self._token = await session.fetch_token(
|
||||||
REFRESH_URL,
|
REFRESH_URL,
|
||||||
code=code,
|
code=code,
|
||||||
# Google specific extra parameter used for client
|
# Google specific extra param used for client authentication:
|
||||||
# authentication
|
client_secret=self._client_secret,
|
||||||
client_secret=client_secret,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# FIXME: Ugly
|
# FIXME: Ugly
|
||||||
_save_token(token)
|
self._save_token(self._token)
|
||||||
|
|
||||||
|
|
||||||
class GoogleCalendarStorage(dav.CalDAVStorage):
|
class GoogleCalendarStorage(dav.CalDAVStorage):
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue