4040class GoogleSession (dav .DAVSession ):
4141 def __init__ (
4242 self ,
43- token_file ,
44- client_id ,
45- client_secret ,
43+ token_file = None ,
44+ client_id = None ,
45+ client_secret = None ,
46+ token_command = None ,
4647 url = None ,
4748 * ,
4849 connector : aiohttp .BaseConnector ,
4950 ):
50- if not have_oauth2 :
51- raise exceptions .UserError ("aiohttp-oauthlib not installed" )
51+ if token_command is not None :
52+ if isinstance (token_command , str ):
53+ self ._token_command = token_command
54+ self ._token_command_shell = True
55+ else :
56+ self ._token_command = list (token_command )
57+ self ._token_command_shell = False
58+ else :
59+ self ._token_command = None
60+ if not have_oauth2 :
61+ raise exceptions .UserError ("aiohttp-oauthlib not installed" )
5262
5363 # Required for discovering collections
5464 if url is not None :
@@ -58,7 +68,7 @@ def __init__(
5868 self ._settings = {}
5969 self .connector = connector
6070
61- self ._token_file = Path (expand_path (token_file ))
71+ self ._token_file = Path (expand_path (token_file )) if token_file else None
6272 self ._client_id = client_id
6373 self ._client_secret = client_secret
6474 self ._token = None
@@ -68,24 +78,52 @@ async def request(self, method, path, **kwargs):
6878 if not self ._token :
6979 await self ._init_token ()
7080
81+ if self ._token_command :
82+ kwargs ["headers" ] = dict (kwargs .get ("headers" , {}))
83+ kwargs ["headers" ]["Authorization" ] = f"Bearer { self ._token } "
84+ try :
85+ return await super ().request (method , path , ** kwargs )
86+ except aiohttp .ClientResponseError as e :
87+ if e .status == 401 :
88+ logger .debug ("Token expired, re-fetching via token_command" )
89+ await self ._fetch_token_via_command ()
90+ kwargs ["headers" ]["Authorization" ] = f"Bearer { self ._token } "
91+ return await super ().request (method , path , ** kwargs )
92+ raise
93+
7194 return await super ().request (method , path , ** kwargs )
7295
7396 async def _save_token (self , token ):
7497 """Helper function called by OAuth2Session when a token is updated."""
98+ if self ._token_command :
99+ return
100+ assert self ._token_file is not None
75101 checkdir (expand_path (os .path .dirname (self ._token_file )), create = True )
76102 with atomic_write (self ._token_file , mode = "w" , overwrite = True ) as f :
77103 json .dump (token , f )
78104
79105 @property
80106 def _session (self ):
81- """Return a new OAuth session for requests.
107+ """Return a new session for requests.
108+
109+ When token_command is set, returns a plain aiohttp session (the
110+ external command handles all OAuth concerns). Otherwise returns an
111+ OAuth2Session that manages the OAuth2 flow with Google.
82112
83- Accesses the self.redirect_uri field (str): the URI to redirect
113+ The OAuth2 path uses self.redirect_uri (str): the URI to redirect
84114 authentication to. Should be a loopback address for a local server that
85115 follows the process detailed in
86116 https://developers.google.com/identity/protocols/oauth2/native-app.
87117 """
88118
119+ if self ._token_command :
120+ return aiohttp .ClientSession (
121+ connector = self .connector ,
122+ connector_owner = False ,
123+ trust_env = True ,
124+ )
125+
126+ assert have_oauth2
89127 return OAuth2Session (
90128 client_id = self ._client_id ,
91129 token = self ._token ,
@@ -103,6 +141,10 @@ def _session(self):
103141 )
104142
105143 async def _init_token (self ):
144+ if self ._token_command :
145+ await self ._fetch_token_via_command ()
146+ return
147+
106148 try :
107149 with self ._token_file .open () as f :
108150 self ._token = json .load (f )
@@ -165,6 +207,30 @@ async def _init_token(self):
165207 # FIXME: Ugly
166208 await self ._save_token (self ._token )
167209
210+ async def _fetch_token_via_command (self ):
211+ import subprocess
212+
213+ assert self ._token_command is not None
214+
215+ try :
216+ if self ._token_command_shell :
217+ stdout = subprocess .check_output (
218+ self ._token_command , text = True , shell = True
219+ )
220+ else :
221+ stdout = subprocess .check_output (self ._token_command , text = True )
222+ except OSError as e :
223+ raise exceptions .UserError (
224+ f"Failed to execute token_command: { self ._token_command } \n { e !s} "
225+ )
226+
227+ self ._token = stdout .strip ()
228+ if not self ._token :
229+ raise exceptions .UserError (
230+ f"token_command returned empty output: { self ._token_command } "
231+ )
232+ logger .debug ("Successfully fetched token via token_command" )
233+
168234
169235class GoogleCalendarStorage (dav .CalDAVStorage ):
170236 class session_class (GoogleSession ):
@@ -185,9 +251,10 @@ def _get_collection_from_url(url):
185251
186252 def __init__ (
187253 self ,
188- token_file ,
189- client_id ,
190- client_secret ,
254+ token_file = None ,
255+ client_id = None ,
256+ client_secret = None ,
257+ token_command = None ,
191258 start_date = None ,
192259 end_date = None ,
193260 item_types = (),
@@ -200,6 +267,7 @@ def __init__(
200267 token_file = token_file ,
201268 client_id = client_id ,
202269 client_secret = client_secret ,
270+ token_command = token_command ,
203271 start_date = start_date ,
204272 end_date = end_date ,
205273 item_types = item_types ,
@@ -230,14 +298,16 @@ class discovery_class(dav.CardDiscover):
230298
231299 storage_name = "google_contacts"
232300
233- def __init__ (self , token_file , client_id , client_secret , ** kwargs ):
301+ def __init__ (self , token_file = None , client_id = None , client_secret = None ,
302+ token_command = None , ** kwargs ):
234303 if not kwargs .get ("collection" ):
235304 raise exceptions .CollectionRequired
236305
237306 super ().__init__ (
238307 token_file = token_file ,
239308 client_id = client_id ,
240309 client_secret = client_secret ,
310+ token_command = token_command ,
241311 ** kwargs ,
242312 )
243313
0 commit comments