External Network Accessを使ってSnowflakeとFitbitAPIを繋いでみた話

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のものに差し替えるだけで動いた。

外部ネットワークアクセスの例

外部ネットワークアクセスについては以下。

2016年6月に書いた記事。phpで検証をしていた。
この辺りからバッテリーがダメになる度に新しいFitbit Charge(1,2,3)を買って溜めてきた。
この間、FitbitがGoogleに買われてしまったり、スマホアプリが大幅に変わったり、色々あった。
基本的な機能はずっと動いているので、7年分のデータが溜まっているんじゃないかな、と期待。

Fitbit API側の準備

OAuth2連携に必要な情報を dev.fitbit.com から取得する必要がある。
Authorization Code Grant Flow with PKCE

こちらを参考にさせていただいた。

以下を準備すれば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)で可視化してみたりしたい。