Togglの記録をServerless + Pixelaで草化

Togglの記録をServerless + Pixelaで草化:


Togglの記録をServerless + Pixelaで草化

  • 作業時間などの時間管理ツールとしてTogglがある

    • いつ,どの作業をしたかを記録
    • 各作業をプロジェクトやタグで分類可能

    • Toggl Reportsで可視化も提供されており,特定の作業をどれくらい継続しているか,どのくらい時間をかけているかを見れる
    • でもとりあえず草化したい!
  • ToggleはAPIを提供しているので比較的用意にデータ抽出可能


作ったもの

  • 1日1回,前日に特定プロジェクトにかけた時間をTogglから抽出し,Pixelaに記録


toggl2pixela.png



結果

  • 自分の勉強時間を草化できた������


スクリーンショット 2018-11-08 11.24.36.png



環境

  • MacOS Mojave
  • Go 1.11.1
  • Serverless Framework 1.32.0


つまづきメモ

  • しょぼい内容だが備忘録として


Lambdaにて時間を扱う場合の注意

  • CloudWatch Eventsをcron式で時間指定する場合,UTCで指定すること

    • e.g. JSTで毎日午前1時に実行したい→UTCで午後4時(-9時間)を指定する cron( 0 16 * * ? * )
  • Lambda関数で日時を取得する場合(e.g. Goでのtime.Now()),標準ではUTCで取得する
  • 日本時間を使いたい場合はLambda関数の環境変数でタイムゾーンを指定すること

    • e.g. 変数TZ, 値Asia/Tokyo


Toggl APIの使い方

  • TogglのAPIを利用したい場合,リクエストにAPIトークンを含める

  • 今回は特定期間の記録を全取得し,特定プロジェクトの記録のみ加算していき合計時間を取得
  • 特定期間の記録を取得するAPIは以下

    • GET https://www.toggl.com/api/v8/time_entries?start_date=XXX&end_date=XXX
    • 日時はISO 8601形式
  • 今回はGoのwrapperであるdougEfresh/gtogglを利用

    • READMEの記載内容だとうまく行かず
import "github.com/dougEfresh/gtoggl" 
import "github.com/dougEfresh/gtoggl-api/gtproject" 
 
func main() { 
  // HTTP client作成 
  thc, err := gthttp.NewClient("your-api-token") 
  ... 
  // Togglの記録(time entry)取得用クライアント作成 
  tec := gttimeentry.NewClient(thc) 
  // 特定期間の記録を取得 
    entries, eerr := tec.GetRange(start_date, end_date) 
} 


開発詳細


Serverless framework + Goで開始


  • $GOHOME/src配下で作業
$ serverless create -t aws-go-dep -p <project-name> 
  • 東京リージョンにデプロイしたいのでserverless.ymlregionを追記
serverless.yml
provider: 
  name: aws 
  runtime: go1.x 
  region: ap-northeast-1 
  • 以下でひとまずデプロイテスト可能
$ cd <project-name> 
$ make 
$ sls deploy 


新規関数を作成

  • 関数を新規作成

    • 自動生成された関数は不要なので削除

    • toggl2pixelaフォルダを作成し,main.goを作成

    • Makefilebuild:に以下を追記
Makefile
env GOOS=linux go build -ldflags="-s -w" -o bin/toggl2pixela toggl2pixela/main.go 


serverless.ymlの修正


  • serverless.ymlの主な修正・追記点は以下

    • 新規作成した関数定義の追記 (+自動生成された関数定義の削除)

    • events下にschecule: ***を書くことで定期実行を定義 (下記では毎日午前1時に実行,上述の通りcron式の時間はUTC指定なので注意)
    • Lambda関数でJSTで日時取得したいので,環境変数TZ, 値Asia/Tokyoを指定
    • Lambda関数の環境変数(environment)にTogglのAPIキー/対象プロジェクトID,Pixelaのユーザ/トークン/グラフ情報を与える
serverless.yml
service: toggl2pixela 
 
frameworkVersion: ">=1.28.0 <2.0.0" 
 
provider: 
  name: aws 
  runtime: go1.x 
  region: ap-northeast-1 
 
package: 
 exclude: 
   - ./** 
 include: 
   - ./bin/** 
 
functions: 
  toggl2pixela: 
    handler: bin/toggl2pixela 
    events: 
      - schedule: cron(0 16 * * ? *) 
    # you need to fill the followings with your own 
    environment: 
      TZ: Asia/Tokyo 
      TOGGL_API_TOKEN: <your-api-token> 
      TOGGL_PROJECT_ID: <target-project-id>  
      PIXELA_USER: <user-id> 
      PIXELA_TOKEN: <your-token> 
      PIXELA_GRAPH: <your-graph-id-1> 
    timeout: 10 


関数本体を作成

  • 素直に実装しただけなので,特記事項なし…

    • データ元のToggl,データ投入先のPixelaの情報は環境変数(TOGGL_API_TOKEN, TOGGL_PROJECT_ID, PIXELA_USER, PIXELA_TOKEN, PIXELA_GRAPH)から取得
    • GoでのToggl操作にはdougEfresh/gtogglを利用
    • 利用方法は上述
    • GoでのPixela操作にはgainings/pixela-go-clientを利用
toggl2pixela/main.go
package main 
 
import ( 
    "context" 
    "errors" 
    "fmt" 
    "os" 
    "strconv" 
    "time" 
 
    "github.com/aws/aws-lambda-go/lambda" 
    "github.com/dougEfresh/gtoggl-api/gthttp" 
    "github.com/dougEfresh/gtoggl-api/gttimentry" 
    pixela "github.com/gainings/pixela-go-client" 
) 
 
// Handler is our lambda handler invoked by the `lambda.Start` function call 
func Handler(ctx context.Context) error { 
    // extract env var 
    apiToken := os.Getenv("TOGGL_API_TOKEN") 
    pjID, _ := strconv.ParseUint(os.Getenv("TOGGL_PROJECT_ID"), 10, 64) 
    user := os.Getenv("PIXELA_USER") 
    token := os.Getenv("PIXELA_TOKEN") 
    graph := os.Getenv("PIXELA_GRAPH") 
 
    // extract data from toggl 
    date, quantity := getDateAndTimeFromToggl(apiToken, pjID) 
    if date == "-1" || quantity == "-1" { 
        return errors.New("Error in accessing toggl") 
    } 
    fmt.Printf("date: %s, quantity: %s\n", date, quantity) 
 
    // record pixel 
    perr := recordPixel(user, token, graph, date, quantity) 
    if perr != nil { 
        return errors.New("Error in accessing pixela") 
    } 
 
    return nil 
} 
 
func getDateAndTimeFromToggl(apiToken string, pjID uint64) (string, string) { 
    // create toggl client 
    thc, err := gthttp.NewClient(apiToken) 
    if err != nil { 
        fmt.Println(err) 
        return "-1", "-1" 
    } 
 
    // set time range to be analyzed 
    y := time.Now().AddDate(0, 0, -1) 
    s := time.Date(y.Year(), y.Month(), y.Day(), 0, 0, 0, 0, time.Local) 
    e := time.Date(y.Year(), y.Month(), y.Day(), 23, 59, 59, 0, time.Local) 
    date := y.Format("20060102") 
 
    // get time entries 
    total := int64(0) 
    tec := gttimeentry.NewClient(thc) 
    entries, eerr := tec.GetRange(s, e) 
    if eerr != nil { 
        fmt.Println(eerr) 
        return "-1", "-1" 
    } 
 
    // sum durations with project pjID 
    for _, e := range entries { 
        if e.Pid == pjID { 
            total += e.Duration 
        } 
    } 
    totalMin := float64(total) / 60 
    quantity := strconv.FormatFloat(totalMin, 'f', 4, 64) 
 
    return date, quantity 
} 
 
func recordPixel(user, token, graph, date, quantity string) error { 
    c := pixela.NewClient(user, token) 
 
    // try to record 
    err := c.RegisterPixel(graph, date, quantity) 
    if err == nil { 
        fmt.Println("recorded") 
        return err 
    } 
 
    // if fail, try to update 
    err = c.UpdatePixelQuantity(graph, date, quantity) 
    if err == nil { 
        fmt.Println("updated") 
    } 
 
    return err 
} 
 
func main() { 
    lambda.Start(Handler) 
} 

コメント