Open stevenirby opened 7 years ago
Sorry @stevenirby the decumentation is a bit lacking on this (non-existent). What you need to do is supply an extra kwarg in the Fitbit
constructor called refresh_cb
. The value should be a function that accepts one argument: a token
. The value of that argument will be a dictionary with the keys ['access_token', 'refresh_token', 'expires_at']
.
When python fitbit makes a call and discovers the access token is expired, it will automatically refresh the token and pass the new token to the function specified by refresh_cb
. You will need to implement this function to store your new token somewhere persistent. The current fitbit client will automatically use the new token so there is no need to update anything there, but the next time you create a fitbit client, you will need to use the updated token that you saved. Does that make sense?
See the function we use in django-fitbit
for an example: https://github.com/orcasgit/django-fitbit/blob/3e696d45d58dac1a097d1ed7f82339895418705e/fitapp/models.py#L24
Oh wow, thanks for the quick reply! I figured it was close to that. I think I'm good now.
Do you need some help updating docs or the README?
@stevenirby we would totally appreciate help with the docs!
For those of us with little experience with code, could you possibly write out what lines need to be added and where? It'd be a massive help! thanks!
Hello!
First, thanks for putting together this api and all the help provided a long the way.
I'm still having trouble with refreshing the token and setting up refresh_cb
correctly. I've approached setting up refresh_cb
in several ways. Including:
refresh_cb
to an anonymous function. Like the following:lambda x: None
or
lambda x: {}
I also added more specific fields to the token argument to the refresh_cb constructor. I actually replicated the function shown in: https://github.com/orcasgit/django-fitbit/blob/3e696d45d58dac1a097d1ed7f82339895418705e/fitapp/models.py#L24
which makes a lot of sense! And so I replicated the function:
def refresh_cb(self, token):
""" Called when the OAuth token has been refreshed """
self.access_token = token['access_token']
self.refresh_token = token['refresh_token']
self.expires_at = token['expires_at']
self.save
For background, this is how initialize the fitbit object
authd = fitbit.Fitbit(consumer_key, consumer_secret, access_token=access_token, refresh_token=refresh_token, redirect_uri='http://localhost:8080', expires_at=expires_at, refresh_cb=refresh_cb)
But I still get the error. I've tried to add print statements the api.py to trace the error and the error is triggered in the call to _request
specifically whereresponse = self.session.request(method, url, **kwargs)
From what concerns me here is that code breaks before token_updater
in_request
is called.
Help would be appreciated!
in make request no url
in make_request with url
in _request
printing **kwargs
headers -> {'Accept-Language': 'en_US'}
data -> {}
client_id -> _client_id_ #not shown here for privacy
client_secret -> _client_secret_ #not shown here for privacy
Traceback (most recent call last):
File "simple_request.py", line 104, in <module>
sleep_data = authd.get_sleep(date)
File "/Users/lpina/Repositories/python-fitbit/fitbit/api.py", line 833, in get_sleep
return self.make_request(url)
File "/Users/lpina/Repositories/python-fitbit/fitbit/api.py", line 279, in make_request
response = self.client.make_request(*args, **kwargs)
File "/Users/lpina/Repositories/python-fitbit/fitbit/api.py", line 109, in make_request
**kwargs
File "/Users/lpina/Repositories/python-fitbit/fitbit/api.py", line 80, in _request
response = self.session.request(method, url, **kwargs)
File "/Library/Python/2.7/site-packages/requests_oauthlib/oauth2_session.py", line 341, in request
self.auto_refresh_url, auth=auth, **kwargs
File "/Library/Python/2.7/site-packages/requests_oauthlib/oauth2_session.py", line 309, in refresh_token
self.token = self._client.parse_request_body_response(r.text, scope=self.scope)
File "/Library/Python/2.7/site-packages/oauthlib/oauth2/rfc6749/clients/base.py", line 409, in parse_request_body_response
self.token = parse_token_response(body, scope=scope)
File "/Library/Python/2.7/site-packages/oauthlib/oauth2/rfc6749/parameters.py", line 376, in parse_token_response
validate_token_parameters(params)
File "/Library/Python/2.7/site-packages/oauthlib/oauth2/rfc6749/parameters.py", line 383, in validate_token_parameters
raise_from_error(params.get('error'), params)
File "/Library/Python/2.7/site-packages/oauthlib/oauth2/rfc6749/errors.py", line 325, in raise_from_error
raise cls(**kwargs)
oauthlib.oauth2.rfc6749.errors.InvalidGrantError: (invalid_grant)
For background here's my version of the python-fitbit repository:
I haven't had time to come back to this, but I too had problems making this work.
The call back function I passed along never fired. So I'm not sure how to make this work.
@stevenirby, thanks for the quick reply. Hopefully others can chime in.
Update! I think it I got it work. Here's how I got mine to work:
def r_cb(token):
""" Called when the OAuth token has been refreshed """
access_token = token['access_token']
refresh_token = token['refresh_token']
expires_at = token['expires_at']
where the variables on the left ( access_token, refresh_token, expires_at) are local variables that get updated when r_cb is called.
@lrpina That could work, but you need to make sure to save those values somewhere permanent (like a database) and use them the next time you create a Fitbit
object for the user. Does that make sense?
@brad, yes! I was just showing an example for others to view what the callback function looks like. Many of use couldn't figure out how to write the refresh_cb function properly. For example, it was unclear what the token dictionary needed to include.
But yes, thanks for clarification. Callback function (in my case r_cb) needs to save the access_token, refresh_token, expires_at to permanent place, such as a database.
Thanks for the notes on this issue. I was able to fix a problem this problem https://github.com/home-assistant/home-assistant/pull/9183 for Home Assistant.
This feature is incredibly valuable and, with the help of this thread, I'm able to successfully execute the callback. My issue is this: because the callback function can only take one variable, I'm at a loss as to how to store the tokens
dictionary in my database. I have multiple users and need to be able to insert the credentials with the correct user_id
.
I am fully aware that my [in]ability do this is directly proportional to my [lack of] skill as a python programmer so I'm not asking for a change in the code (or even the documentation), but for your advice on how you would solve this problem. Thanks!
For example:
def refresh_callback(tokens):
""" Called when the OAuth token has been refreshed """
fbm.store_tokens(study_id, tokens) # a db call in my model
if __name__ == "__main__":
result = []
for a in active_user_tokens:
study_id = a['study_id']
authd_client = fitbit.Fitbit(config.CLIENT_ID,
config.CLIENT_SECRET,
access_token=a['credentials']['access_token'],
refresh_token=a['credentials']['refresh_token'],
expires_at =a['credentials']['expires_in'],
refresh_cb=refresh_callback)
result.append(authd_client.sleep())
print(result)
This code gives me the sleep data (and I can print the new tokens inside the refresh_callback
function, but I need access to those variables inside the loop so that I can update the database by a['user_id']
.
::facepalm:: global variable code updated to reflect what actually works
@stuboo Take a look at how we did it in django-fitbit. The refresh callback is actually a method on the DB model class so it has access to everything it needs:
def refresh_cb(self, token):
""" Called when the OAuth token has been refreshed """
self.access_token = token['access_token']
self.refresh_token = token['refresh_token']
self.expires_at = token['expires_at']
self.save()
Will something like that work for your case?
Has anyone had any success getting the refresh callback function to fire? My code is as follows
import datetime
import fitbit
import json
def refresh_callback(token):
f_data = {
'access_token': token['access_token'],
'refresh_token': token['refresh_token'],
'expires_at': token['expires_at']
}
with open('access_token_cache_file.json', 'w') as outfile:
json.dump(f_data, outfile)
with open('fitbit_config.json') as json_file:
data = json.load(json_file)
fitbit_user_id = data['fitbit']['user_id']
fitbit_client_id = data['fitbit']['client_id']
with open('access_token_cache_file.json') as json_file:
data = json.load(json_file)
access_token = data['access_token']
refresh_token = data['refresh_token']
expires_at = data['expires_at']
client = fitbit.Fitbit(fitbit_user_id, fitbit_client_id, access_token=access_token, refresh_token=refresh_token,
expires_at=expires_at, refresh_cb=refresh_callback)
date = datetime.datetime.today()
print(client.activities(date))
After my token expires, I get the following exceptions (traceback below):
Traceback (most recent call last):
File "/home/pete7863/.local/lib/python3.5/site-packages/requests_oauthlib/oauth2_session.py", line 395, in request
http_method=method, body=data, headers=headers)
File "/home/pete7863/.local/lib/python3.5/site-packages/oauthlib/oauth2/rfc6749/clients/base.py", line 198, in add_token
raise TokenExpiredError()
oauthlib.oauth2.rfc6749.errors.TokenExpiredError: (token_expired)
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/home/pete7863/influxdbmylife/fitbit/main.py", line 102, in <module>
write_day(date, client, fb.activities(date))
File "/home/pete7863/.local/lib/python3.5/site-packages/fitbit/utils.py", line 38, in _curried
return _curried_func(*(args+moreargs), **dict(kwargs, **morekwargs))
File "/home/pete7863/.local/lib/python3.5/site-packages/fitbit/api.py", line 348, in _COLLECTION_RESOURCE
return self.make_request(url, data)
File "/home/pete7863/.local/lib/python3.5/site-packages/fitbit/api.py", line 256, in make_request
response = self.client.make_request(*args, **kwargs)
File "/home/pete7863/.local/lib/python3.5/site-packages/fitbit/api.py", line 96, in make_request
**kwargs
File "/home/pete7863/.local/lib/python3.5/site-packages/fitbit/api.py", line 68, in _request
response = self.session.request(method, url, **kwargs)
File "/home/pete7863/.local/lib/python3.5/site-packages/requests_oauthlib/oauth2_session.py", line 408, in request
self.auto_refresh_url, auth=auth, **kwargs
File "/home/pete7863/.local/lib/python3.5/site-packages/requests_oauthlib/oauth2_session.py", line 374, in refresh_token
self.token = self._client.parse_request_body_response(r.text, scope=self.scope)
File "/home/pete7863/.local/lib/python3.5/site-packages/oauthlib/oauth2/rfc6749/clients/base.py", line 415, in parse_request_body_response
self.token = parse_token_response(body, scope=scope)
File "/home/pete7863/.local/lib/python3.5/site-packages/oauthlib/oauth2/rfc6749/parameters.py", line 425, in parse_token_response
validate_token_parameters(params)
File "/home/pete7863/.local/lib/python3.5/site-packages/oauthlib/oauth2/rfc6749/parameters.py", line 432, in validate_token_parameters
raise_from_error(params.get('error'), params)
File "/home/pete7863/.local/lib/python3.5/site-packages/oauthlib/oauth2/rfc6749/errors.py", line 405, in raise_from_error
raise cls(**kwargs)
oauthlib.oauth2.rfc6749.errors.InvalidClientError: (invalid_client)
Is there something simple I am missing? I've tried to strip down my code to the barest form. Any help is greatly appreciated!
I think I figured my issue out. For some reason I was initializing with the user_id and the client_id as the first two parameters when initializing the Fitbit client. Once I fixed this issue, my callback started working properly.
My issue with refresh_token is that it is worded as something that is automatic, but it is never called within the code itself. The token_updater also doesn't exist or at least I couldn't figure out how to make it exist through passing some sort of function originally.
So my solution was to modify the source code and call it myself.
` def refresh_token(self):
token = {}
token = self.session.refresh_token(
self.refresh_token_url,
auth=HTTPBasicAuth(self.client_id, self.client_secret)
return token
`
In my main code I create the fitbit object with stored credentials and try to access data. If that doesn't work then I directly call the refresh_token() function. And if THAT doesn't work then I go through the entire reauthorization process.
I'm not sure how to keep my app authed for longer than one day.
Here is what I'm doing:
gather_keys_oauth2.py
to get tokens.How do I auth the app for much longer time?