FitbitはAPIがしっかり整備されていて、OAuth2 endpoint経由でデータが取り放題。
せっかくなので、話題のExternal Network Access(2023年12月現在 PuPr)を試してみようと思う。
つまり、FitbitAPI→Snowflakeをやってみようと思う。
Fitbit APIを使用するにはOAuth2.0 Authorizationを通す必要がある。
Snowflakeの公式にOAuth2.0 Endpoint経由でGoogle翻訳APIと連携する段取りが書かれていて、
それをそのままFitbit APIのものに差し替えるだけで動いた。
外部ネットワークアクセスの例
外部ネットワークアクセスについては以下。
[clink implicit=\"false\" url=\"https://docs.snowflake.com/ja/developer-guide/external-network-access/creating-using-external-network-access#label-creating-using-external-access-integration-network-rule\" imgurl=\"https://www.snowflake.com/wp-content/uploads/2017/01/snowflake-logo-color-300x69.png\" title=\"外部アクセス統合の作成と使用\" excerpt=\"特定の外部ネットワークロケーションへのアクセスを有効にするには、外部ロケーションのリストと使用を許可されるシークレットのリストを指定する外部アクセス統合を作成します。UDF の作成時、あるいは CREATEFUNCTION または CREATEPROCEDURE でプロシージャを作成する際に、 EXTERNAL_ACCESS_INTEGRATIONS 句を使用してこの統合を参照することで、ハンドラーコードが外部ロケーションとの認証コードにシークレットを使用できるようになります。\"]
2016年6月に書いた記事。phpで検証をしていた。
この辺りからバッテリーがダメになる度に新しいFitbit Charge(1,2,3)を買って溜めてきた。
この間、FitbitがGoogleに買われてしまったり、スマホアプリが大幅に変わったり、色々あった。
基本的な機能はずっと動いているので、7年分のデータが溜まっているんじゃないかな、と期待。
[clink url=\"https://ikuty.com/2016/06/07/fitbitapi-authenticate-grant-flow/\"]
Fitbit API側の準備
OAuth2連携に必要な情報を dev.fitbit.com から取得する必要がある。
Authorization Code Grant Flow with PKCE
こちらを参考にさせていただいた。
[clink implicit=\"false\" url=\"https://www.zenryoku-kun.com/post/fitbit-api#register-app\" imgurl=\"https://www.zenryoku-kun.com/home/sakura-400w.jpg\" title=\"FitbitのWeb APIを実行する方法\" excerpt=\"Fitbit Sense2を購入しました。はじめてのスマートウォッチです。Fitbitデバイスでは、心拍数や歩数等、収集したデータをWeb APIで取得することが可能です。さっそく使って遊んでみようと思ったら、Web APIの認証がなかなか通らない、、、ドキュメントはとても充実しているのですが、OAuth2.0の認証パターンがImplicit Grant Flowの場合、Authorization Code Grant Flowの場合、PKCEを使う場合、、、などなど、情報量がとにかく多く混乱してしまいました。何はともあれ、何とか認証を通して、こんな感じで歩数などのアクティビティ情報や、心拍数や血中酸素濃度(SpO2)を取得することが出来ました。\"]
以下を準備すればOK。
access-token
refresh-token
client-id
Snowflakeでリソース作り
Snowsightでポチポチとリソースを作っていく。
USE ROLE SYSADMIN;
-- 外部ロケーションを表すネットワークルールの作成
--
CREATE OR REPLACE NETWORK RULE fitbit_apis_network_rule
MODE = EGRESS
TYPE = HOST_PORT
VALUE_LIST = (\'api.fitbit.com\');
-- 外部ロケーションとの認証に必要なOAuth認証情報を保持するセキュリティ統合の作成
--
CREATE OR REPLACE SECURITY INTEGRATION fitbit_api_oauth
TYPE = API_AUTHENTICATION
AUTH_TYPE = OAUTH2
OAUTH_CLIENT_ID = \'\'
OAUTH_CLIENT_SECRET = \'\'
OAUTH_TOKEN_ENDPOINT = \'https://api.fitbit.com/oauth2/token\'
OAUTH_AUTHORIZATION_ENDPOINT = \'https://www.fitbit.com/oauth2/authorize\'
ENABLED = TRUE;
-- セキュリティ統合に含まれる認証情報を表すシークレットの作成
--
CREATE OR REPLACE SECRET fitbit_api_oauth_token
TYPE = oauth2
API_AUTHENTICATION = fitbit_api_oauth
OAUTH_REFRESH_TOKEN = \'\';
最後に外部アクセス統合を作成する。
ストレージ統合や、Notification統合など、統合の作成にはACCOUNTADMINが必要で、
同様に外部アクセス統合の作成にはACCOUNTADMINが必要とのこと。
USE ROLE ACCOUNTADMIN;
CREATE OR REPLACE EXTERNAL ACCESS INTEGRATION fitbit_apis_access_integration
ALLOWED_NETWORK_RULES = (fitbit_apis_network_rule)
ALLOWED_AUTHENTICATION_SECRETS = (fitbit_api_oauth_token)
ENABLED = TRUE;
外部ロケーション(ネットワーク)にアクセスするUDFsを書くロールを作成する。
UDFsを書く際に、シークレットを参照する必要がある。
UDFsを書けるロールにシークレットのREAD権限を付与しておく必要がある。
以下、そのままでは SECURITYADMINがDB・スキーマに触れないので環境により修正が必要。
USE ROLE USERADMIN;
CREATE OR REPLACE ROLE ikuty_fitbitapi_developer;
USE ROLE SECURITYADMIN;
USE SCHEMA IKUTY_DB.PUBLIC;
GRANT READ ON SECRET IKUTY_DB.PUBLIC.fitbit_api_oauth_token TO ROLE ikuty_fitbitapi_developer;
GRANT USAGE ON INTEGRATION fitbit_apis_access_integration TO ROLE ikuty_fitbitapi_developer;
GRANT ROLE ikuty_fitbitapi_developer TO role SYSADMIN;
本体の実装
PythonでOAuth2 Endpoint経由でFitbit APIにGETリクエストを投げるFunctionを書く。
最初、トークンのexpire時のrefreshを自力で書いていたが、get_oauth_access_token(\'cred\')により、
自動的にrefreshしてくれていることに気づいた。
use role sysadmin;
use schema IKUTY_DB.PUBLIC;
CREATE OR REPLACE FUNCTION fitbit_python()
RETURNS STRING
LANGUAGE PYTHON
RUNTIME_VERSION = 3.8
HANDLER = \'hello_fitbit\'
EXTERNAL_ACCESS_INTEGRATIONS = (fitbit_apis_access_integration)
PACKAGES = (\'snowflake-snowpark-python\',\'requests\')
SECRETS = (\'cred\' = fitbit_api_oauth_token )
AS
$$
import _snowflake
import requests
import json
def hello_fitbit():
with requests.Session() as s:
access_token = _snowflake.get_oauth_access_token(\'cred\')
url = \"https://api.fitbit.com/1/user/-/activities/steps/date/today/1m.json\"
res = s.get(url,headers={\"Authorization\": \"Bearer \" + access_token})
res_data = res.json()
return res_data
$$;
実行結果は以下。1日毎の歩数を1ヶ月分取得できた(恥...)。
select parse_json(fitbit_python());
{
\"activities-steps\": [
{
\"dateTime\": \"2023-11-23\",
\"value\": \"15570\"
},
{
\"dateTime\": \"2023-11-24\",
\"value\": \"5392\"
},
{
\"dateTime\": \"2023-11-25\",
\"value\": \"8993\"
},
{
\"dateTime\": \"2023-11-26\",
\"value\": \"10525\"
},
{
\"dateTime\": \"2023-11-27\",
\"value\": \"6371\"
},
{
\"dateTime\": \"2023-11-28\",
\"value\": \"2713\"
},
{
\"dateTime\": \"2023-11-29\",
\"value\": \"9252\"
},
{
\"dateTime\": \"2023-11-30\",
\"value\": \"0\"
},
{
\"dateTime\": \"2023-12-01\",
\"value\": \"7947\"
},
{
\"dateTime\": \"2023-12-02\",
\"value\": \"11265\"
},
{
\"dateTime\": \"2023-12-03\",
\"value\": \"8557\"
},
{
\"dateTime\": \"2023-12-04\",
\"value\": \"2366\"
},
{
\"dateTime\": \"2023-12-05\",
\"value\": \"7985\"
},
{
\"dateTime\": \"2023-12-06\",
\"value\": \"8109\"
},
{
\"dateTime\": \"2023-12-07\",
\"value\": \"6852\"
},
{
\"dateTime\": \"2023-12-08\",
\"value\": \"3707\"
},
{
\"dateTime\": \"2023-12-09\",
\"value\": \"12640\"
},
{
\"dateTime\": \"2023-12-10\",
\"value\": \"7122\"
},
{
\"dateTime\": \"2023-12-11\",
\"value\": \"7190\"
},
{
\"dateTime\": \"2023-12-12\",
\"value\": \"8034\"
},
{
\"dateTime\": \"2023-12-13\",
\"value\": \"5228\"
},
{
\"dateTime\": \"2023-12-14\",
\"value\": \"2861\"
},
{
\"dateTime\": \"2023-12-15\",
\"value\": \"6785\"
},
{
\"dateTime\": \"2023-12-16\",
\"value\": \"11720\"
},
{
\"dateTime\": \"2023-12-17\",
\"value\": \"11021\"
},
{
\"dateTime\": \"2023-12-18\",
\"value\": \"0\"
},
{
\"dateTime\": \"2023-12-19\",
\"value\": \"11021\"
},
{
\"dateTime\": \"2023-12-20\",
\"value\": \"0\"
},
{
\"dateTime\": \"2023-12-21\",
\"value\": \"2703\"
},
{
\"dateTime\": \"2023-12-22\",
\"value\": \"3336\"
},
{
\"dateTime\": \"2023-12-23\",
\"value\": \"7497\"
}
]
}
結論
PuPrのExternal Network Accessを使用して、FitbitAPI→Snowflakeが出来ることを確認した。
(途中、自動的にトークンをrefreshしてくれている、と書いたが、何度かExpireさせないと良くわからない。)
相手がOAuth2.0ならとても簡単に繋ぐことができると思う。
次は、せっかくなのでSiS(Streamlit in Snowflake)で可視化してみたりしたい。