1111from requests .exceptions import HTTPError
1212
1313from requests import Session
14- import urllib
1514
1615from feedly .data import FeedlyUser
1716from feedly .protocol import RateLimitedAPIError , BadRequestAPIError , UnauthorizedAPIError , ServerAPIError , APIClient , WrappedHTTPError
18- from feedly .stream import EnterpriseStreamId
17+
18+
19+ class Auth :
20+ """
21+ simple class to manage tokens
22+ """
23+ def __init__ (self , client_id :str = 'feedlydev' , client_secret :str = 'feedlydev' ):
24+ self .client_id :str = client_id
25+ self .client_secret :str = client_secret
26+ self ._auth_token :str = None
27+ self .refresh_token :str = None
28+
29+ @property
30+ def auth_token (self ):
31+ return self ._auth_token
32+
33+ @auth_token .setter
34+ def auth_token (self , token :str ):
35+ self ._auth_token = token
36+
37+
38+ class FileAuthStore (Auth ):
39+ """
40+ a file based token storage scheme
41+ """
42+ def __init__ (self , token_dir :Path , client_id :str = 'feedlydev' , client_secret :str = 'feedlydev' ):
43+ """
44+
45+ :param token_dir: the directory to store the tokens
46+ :param client_id: the client id to use when refreshing the auth token. the default value works for developer tokens.
47+ :param client_secret: the client secret to use when refreshing the auth token. the default value works for developer tokens.
48+ """
49+ super ().__init__ (client_id , client_secret )
50+ if not token_dir .is_dir ():
51+ raise ValueError (f'{ token_dir .absolute ()} does not exist!' )
52+
53+ refresh_path = token_dir / 'refresh.token'
54+ if refresh_path .is_file ():
55+ self .refresh_token = refresh_path .read_text ().strip ()
56+
57+ self .auth_token_path :Path = token_dir / 'access.token'
58+ self ._auth_token = self .auth_token_path .read_text ().strip ()
59+
60+ @Auth .auth_token .setter
61+ def auth_token (self , token :str ):
62+ self ._auth_token = token
63+ self .auth_token_path .write_text (token )
1964
2065
2166class FeedlySession (APIClient ):
22- def __init__ (self , auth_token :str , api_host :str = 'https://feedly.com' , name :str = 'feedly.python.client' ,
23- user_id :str = None , client_name = 'feedly.python.client' ):
67+ def __init__ (self , auth :Union [str , Auth ], api_host :str = 'https://feedly.com' , user_id :str = None , client_name = 'feedly.python.client' ):
68+ """
69+ :param auth: either the access token str to use when making requests or an Auth object to manage tokens
70+ :param api_host: the feedly api server host.
71+ :param user_id: the user id to use when making requests. If not set, a request will be made to determine the user from the auth token.
72+ :param client_name: the name of your client, set this to something that can identify your app.
73+ """
2474 super ().__init__ ()
2575 if not client_name :
2676 raise ValueError ('you must identify your client!' )
2777
28- self .auth_token :str = auth_token
78+ if isinstance (auth , str ):
79+ token :str = auth
80+ auth = Auth ()
81+ auth .auth_token = token
82+
83+ self .auth :Auth = auth
2984 self .api_host :str = api_host
3085 self .session = Session ()
3186 self .session .mount ('https://feedly.com' , HTTPAdapter (max_retries = 1 )) # as to treat feedly server and connection errors identically
3287 self .client_name = client_name
3388 self .timeout :int = 10
3489 self .max_tries :int = 3
35- self .name = urllib .parse .quote_plus (name )
3690
3791 user_data = {'id' : user_id } if user_id else {}
3892 self ._user :FeedlyUser = FeedlyUser (user_data , self )
3993 self ._valid :bool = None
94+ self ._last_token_refresh_attempt :float = 0
4095
4196 def __repr__ (self ):
4297 return f'<feedly client user={ self .user .id } >'
@@ -75,14 +130,13 @@ def do_api_request(self, relative_url:str, method:str=None, data:Dict=None,
75130 :raises: requests.exceptions.HTTPError on failure. An appropriate subclass may be raised when appropriate,
76131 (see the ones defined in this module).
77132 """
78-
79133 if self .timeout is None :
80134 timeout = self .timeout
81135
82136 if max_tries is None :
83137 max_tries = self .max_tries
84138
85- if self .auth_token is None :
139+ if self .auth . auth_token is None :
86140 raise ValueError ('authorization token required!' )
87141
88142 if relative_url [0 ] != '/' :
@@ -113,7 +167,7 @@ def do_api_request(self, relative_url:str, method:str=None, data:Dict=None,
113167 if self .rate_limiter .rate_limited :
114168 until = datetime .datetime .fromtimestamp (self .rate_limiter .until ).isoformat ()
115169 raise ValueError (f'Too many requests. Client is rate limited until { until } ' )
116- headers = {'Authorization' : self .auth_token }
170+ headers = {'Authorization' : self .auth . auth_token }
117171 if data :
118172 headers ['Content-Type' ] = 'application/json'
119173
@@ -142,6 +196,17 @@ def do_api_request(self, relative_url:str, method:str=None, data:Dict=None,
142196 if code == 400 :
143197 raise BadRequestAPIError (e )
144198 elif code == 401 :
199+ if not relative_url .startswith ('/v3/auth' ) and self .auth .refresh_token and time .time () - self ._last_token_refresh_attempt > 86400 :
200+ try :
201+ self ._last_token_refresh_attempt = time .time ()
202+ auth_data = {'refresh_token' : self .auth .refresh_token , 'grant_type' : 'refresh_token' ,
203+ 'client_id' : self .auth .client_id , 'client_secret' : self .auth .client_secret }
204+ token_data = self .do_api_request ('/v3/auth/token' , data = auth_data )
205+ self .auth .auth_token = token_data ['access_token' ]
206+ return self .do_api_request (relative_url = relative_url , method = method , data = data , timeout = timeout , max_tries = max_tries )
207+ except Exception as e2 :
208+ logging .info ('error refreshing access token' , exc_info = e2 )
209+ # fall through to raise auth error
145210 raise UnauthorizedAPIError (e )
146211 elif code == 429 :
147212 if not self .rate_limiter .rate_limited :
@@ -154,13 +219,13 @@ def do_api_request(self, relative_url:str, method:str=None, data:Dict=None,
154219
155220if __name__ == '__main__' :
156221 logging .basicConfig (level = 'DEBUG' )
157- token = (Path .home () / 'access.token' ).read_text ().strip ()
222+ # token = (Path.home() / 'access.token').read_text().strip()
223+ auth = FileAuthStore (Path .home ())
158224 # print(sess.user['fullName'])
159225
160- uid = 'uid'
161- sess = FeedlySession (auth_token = token , user_id = uid )
226+ sess = FeedlySession (auth )
162227
163- # sess.user.get_enterprise_tags()
228+ sess .user .get_enterprise_tags ()
164229 # sess.user.get_enterprise_categories()
165230
166231 # with FeedlySession(auth_token=token, user_id=uid) as sess:
0 commit comments