watermint.org - Takayuki Okazaki's note

Mathematicaの練習: Asanaタスクの集計

大学で使ってからもう十数年ぶりになりますがMathematicaをさわっています。数学とあとは機械学習関連を勉強するためMathematicaのHome版を購入してみました。まずはデータを分析したりするところから手始めにやってみようと思っていますが、 十数年ぶりということもありますし、大学時代もさほど深く使い込んでいなかったのでWolfram Languageの知識はほぼゼロの状態です。

慣れたプログラミング言語で書けばすぐできることですが、手始めにAsanaで管理しているタスクを集計したり分析する流れをやってみました。AsanaのタスクはJSON形式でエクスポートができますのでこれを読み込んで集計してみます。

週ごとのタスク数

JSONを読み込むにはImportを使いますがこのとき、”JSON”ではなく、”RawJSON”をつかうとすべてがAssociation(連想配列のようなもの)として読み込むことができます。ここまでくるだけで結構つまづきました。Asanaから得られるJSONは”data”というキーが最初にあるのでいろいろなプログラミング言語の連想配列と同じように["data"]のように書いて値を取得します。値が取り出せたので続いて集計です。

AsanaのJSON書式はAPI referenceあたりを参照しながら処理を進めましょう。たとえばタスクが作成された日時は、”created_at”というキーに対する値として設定されています。時刻の書式はISO 8601形式です。

ISO8601形式をMathematicaで読み込むには DateObject["2017-03-31T09:00:00Z"]のようにDateObjectへ渡してやればよいようです。

手元のAsanaデータは1月中旬に整理整頓してそれ以前はあまり正確ではありませんでしたから、1月中旬以降のものだけを集計対象としています。

asanaTasks = 
  Select[asanaJSON, 
   DateObject[#["created_at"]] > DateObject[{2017, 1, 15}] &];

集計対象としたいタスクだけを取り出すにはSelect関数を使えば良いようです。関数型言語でプログラミングした経験があれば比較的すんなり理解しやすいかと思いますが、&のような簡略書式はMathematica独特なのでこれはマニュアルや例をみながら見様見まねで覚えるしかなさそうですね。

集計をするときもCountsByのような関数で簡単に集計できます。関数がたくさんあるので、プロトタイピングするにはもってこいです。

週ごとに集計するために日付を丸めたかったのですがこれがなかなかわからず苦労しました。

taskCountPerWeek = CountsBy[
   asanaTasks,
   CurrentDate[
     DateObject[#["created_at"]],
     "Week"
     ] &
   ];

最近出た11.1というリリースで追加されたCurrentDateを使うと簡単にできるようです。CurrentDate[Now, "Week"]のように日付の粒度を指定すればその粒度で値がかえってきます。語感としてややこしいのは、CurrentDateとありますがDateObjectなどを渡してやれば与えた日時を基準に変換してくれます。

最後にDateListPlotでグラフを書けば週ごとにどれぐらいタスクが作成されているかわかりました。

Dropbox Business API: Properties API Proof of Concept

I wrote sample script of Properties API of Dropbox Business in Python.

Note: Properties APIs are alpha release (as of Feb 2017). Specification of APIs may change without notice.

Properties API

Dropbox API and Dropbox Business API provide store properties of files or folders. These properties are not displayed on desktop, mobile and web. Properties APIs are designed for application which integrate with Dropbox Business. Here are possible use cases.

  • Store security policies for each files/folders.
  • Store approval status of documents.
  • Store document ID which issued from other CMS.

Properties template

Before storing properties of files/folders. You need to create Properties template in Dropbox Business team. To perform properties template operation, application must have “Team member file access” token. See more detail about auth type at Access Type.

Properties template can have multiple fields. Field can have single string value.

Here are APIs for Properties template. No method for delete template. Applications should maintain properties template id. Because you can create same name properties template. Template name is just for human friendly purpose.

Here is example of list existing templates:

templates = client.team_properties_template_list()

for t in templates.template_ids:
    template = client.team_properties_template_get(t)
    print "Template Id: %s" % t
    print "Template Name: %s" % template.name
    print "Description: %s" % template.description
    for f in template.fields:
        print "Field[%s] Description[%s]" % (f.name, f.description)

Store/retrieve properties of files/folders

To retrieve properties of file/folder, use /files/alpha/get_metadata. For retrieve properties, specify template id in include_property_templates attribute.

To store properties of file/folder, use /files/properties/add or /files/properties/overwrite.

Code sample

Here is complete code sample of use case of properties APIs. This sample expect to store security policy and levels for each files. This sample code using latest Dropbox Python SDK.

import dropbox
from dropbox.files import PropertyGroupUpdate
from dropbox.properties import PropertyFieldTemplate, PropertyType, PropertyField, PropertyGroup

# Dropbox Business Team file access token
DROPBOX_TEAM_FILE = ''


def list_more_files(client, cursor):
    """
    :type client: dropbox.Dropbox
    :type cursor: str
    :rtype: list[dropbox.files.Metadata]
    """
    chunk = client.files_list_folder_continue(cursor)
    if chunk.has_more:
        return chunk.entries + list_more_files(client, chunk.cursor)
    else:
        return chunk.entries


def list_files(client, path):
    """
    :type client: dropbox.Dropbox
    :type path: str
    :rtype: list[dropbox.files.Metadata]
    """

    # Set recursive=False because files under shared folders are not listed.
    # call traverse_files for extract files recursively
    chunk = client.files_list_folder(path, recursive=False)
    if chunk.has_more:
        return traverse_files(client, chunk.entries + list_more_files(client, chunk.cursor))
    else:
        return traverse_files(client, chunk.entries)


def traverse_files(client, entries):
    """
    :type client: dropbox.Dropbox
    :type entries: list[dropbox.files.Metadata]
    :rtype: list[dropbox.files.Metadata]
    """
    all = []
    for f in entries:
        all.append(f)
        if isinstance(f, dropbox.files.FolderMetadata):
            all += list_files(client, f.path_lower)

    return all


def list_more_members(client, cursor):
    """
    :type client: dropbox.DropboxTeam
    :type cursor: str
    :rtype: list[dropbox.team.TeamMemberInfo]
    """
    chunk = client.team_members_list_continue(cursor)
    if chunk.has_more:
        return chunk.members + list_more_members(client, chunk.cursor)
    else:
        return chunk.members


def list_members(client):
    """
    :type client: dropbox.DropboxTeam
    :rtype: list[dropbox.team.TeamMemberInfo]
    """
    chunk = client.team_members_list()
    if chunk.has_more:
        return chunk.members + list_more_members(client, chunk.cursor)
    else:
        return chunk.members


def show_properties_templates(client):
    """
    :type client: dropbox.DropboxTeam
    """
    templates = client.team_properties_template_list()

    for t in templates.template_ids:
        template = client.team_properties_template_get(t)
        print "Template Id: %s" % t
        print "Template Name: %s" % template.name
        print "Description: %s" % template.description
        for f in template.fields:
            print "Field[%s] Description[%s]" % (f.name, f.description)


def find_properties_template_id_by_name(client, name):
    """
    :type client: dropbox.DropboxTeam
    :type name: str
    :rtype: str | None
    """
    templates = client.team_properties_template_list()
    for t in templates.template_ids:
        template = client.team_properties_template_get(t)
        if template.name == name:
            return t

    return None


def audit_file(client, template_id, file):
    """
    :type client: dropbox.Dropbox
    :type template_id: str
    :type file: dropbox.files.FileMetadata
    """
    print "Auditing file: %s" % file.path_display

    meta = client.files_alpha_get_metadata(file.path_lower, include_property_templates=[template_id])
    if isinstance(meta, dropbox.files.FileMetadata) and meta.property_groups is not None:
        for p in meta.property_groups:
            if p.template_id == template_id:
                for field in p.fields:
                    print "File[%s] Security Policy: %s = %s" % (file.path_display, field.name, field.value)

    # Mark as Confidential for every '.pdf'
    if file.path_lower.endswith('.pdf'):
        print "Updating security policy: %s : template_id=%s" % (file.path_display, template_id)
        level_field = PropertyField('Level', 'Confidential')
        prop_group = PropertyGroup(template_id, [level_field])
        client.files_properties_overwrite(file.path_lower, [prop_group])


def audit_member(client, template_id, member):
    """
    :type client: dropbox.Dropbox
    :type template_id: str
    :type member: dropbox.team.TeamMemberInfo
    """
    print "Auditing files of member: %s" % member.profile.email

    files = list_files(client, "")
    for f in files:
        if isinstance(f, dropbox.files.FileMetadata):
            audit_file(client, template_id, f)


if __name__ == '__main__':
    client_team = dropbox.DropboxTeam(DROPBOX_TEAM_FILE)

    # List existing properties template
    show_properties_templates(client_team)

    # Lookup template named 'Security Policy'
    tmpl_name = 'Security Policy'
    tmpl_desc = 'These properties describe how confidential this file is.'
    tmpl_field_level_name = 'Level'
    tmpl_field_level_desc = 'Level can be Confidential, Public or Internal.'

    security_policy_template_id = find_properties_template_id_by_name(client_team, tmpl_name)

    # Add template if not exist
    if security_policy_template_id is None:
        fields = [
            PropertyFieldTemplate(tmpl_field_level_name, tmpl_field_level_desc, PropertyType.string)
        ]
        security_policy_template_id = client_team.team_properties_template_add(tmpl_name, tmpl_desc, fields)
        print security_policy_template_id

    # Audit member files
    members = list_members(client_team)
    for m in members:
        # Create client as user
        c = client_team.as_user(m.profile.team_member_id)
        audit_member(c, security_policy_template_id, m)

Aerobaticから再びGithub Pages + CloudFlareへ

つい2週間ほど前にこのブログのホスティングサービスをAerobatic引っ越しをしたところですが、またGithub Pages + CloudFlareに切り戻すことにしました。サービス体系が大幅に見直されAerobaticの無償プランにカスタムドメインのサービスが含まれなくなってしまったためです。また、ツール群もBitbucketのアドインという形式から、別途CLIベースのものになりました。

無償プランがなくなったから、あるいはCLIになったからというだけで移行するには忍びないですが、有償プランが一本化され$15/月だけになってしまったのは残念です。「$2〜3/月でカスタムドメイン1つ」程度であれば充分利用する気持ちになったのですが、このような小規模ブログには新料金プランはオーバースペックでした。

このようなこともありまた元のGithub Pages + CloudFlareへ切り戻しをしました。幸いにしてJekyllで制作したブログのホスティング先切り替えは非常に簡単です。10分程度で終わってしまいました。

Go: Bandwidth limit for multiple Reader and Writer

Bandwidth limit is really important for stabilizing systems. Core business logic stops if low priority job consume entire bandwidth of the system. When writing bandwidth limit for Reader or Writer in Go, I found go-flowrate. This library enables easy way to limit bandwidth like the code below.

func main() {
    f, _ := os.Open("data.dat")

    // Create wrapper with 100 bytes per second
    f2 := flowrate.NewReader(f, 100)

    // Biz logic
    // ...
}

APIs are pretty simple and reliable. But go-flowrate can limit bandwidth only for single Reader or Writer. In my case, I was working on file uploader for Dropbox which has concurrent upload feature. This code require limiting bandwidth for multiple I/O. So I decided create new small library for bandwidth limit.

bwlimit

bwlimit is the name of my library. You can see or clone from github bwlimit. Here is code example of bwlimit.

func main() {
    // Limit 100 bytes per second
    bwlimit := NewBwlimit(100, false)

    // Prepare multiple readers
    f1, _ := os.Open("data1.dat")
    f2, _ := os.Open("data2.dat")

    // Create wrapper
    fr1 := bwlimit.Reader(f1)
    fr2 := bwlimit.Reader(f2)

    // Biz logic
    // ...

    // Wait for all Reader to be closed or reach EOF
    bwlimit.Wait()
}

1) Prepare bandwidth limiting object (bwlimit in above code) first. Second argument is the flag for block(true) or unblock(false) for Read or Write operation. 2) Then, create wrapper for each Reader or Writer.

Concept

The concept of bwlimit comes from Toyota’s Production System (TPS). In TPS, Takt time is the core concept for leveling production. Define time unit of ship defined quantity of products.

For example; 1) Takt time is 100ms, 2) bandwidth limit is 1,000 bytes per second. 100 bytes is maximum transferrable data size per takt time. If bwlimit object has two Readers, that mean 50 bytes per Reader per takt time.

Flow control

bwlimit does not carry bandwidth window to next takt time. If the Reader have 50 bytes window per takt time, and the Reader read nothing. Then, window size of next takt time will be 50 bytes. This is because if bwlimit carry unused window to next takt time. bwlimit may allow burst IO for certain takt time. That sometime cause buffer overflow of routers. To prevent incident caused by burst IO.

Github Pages + CloudFlareからAerobaticへ

つい先月、TumblrからJekyll + Github Pagesに移行したところですが、今度はJekyll + Aerobaticに移行しました。

これでこのブログのサーバお引っ越しは、次のように4回目になりました。

  1. WordPress + さくらインターネット (2005〜2013)
  2. Tumblr (2013〜2016)
  3. Jekyll + Github Pages + CloudFlare (2016〜2017)
  4. Jekyll + Aerobatic (2017〜)

WordPressからTumblrに移行したり、TumblrからJekyllに移行するのはコンテンツの調整などでさまざま面倒な点があります。たとえば、既存コンテンツに対するURLを引き継ぎたいといった要望に対して対応するのはコンテンツ個別に調整と動作確認が必要でした。

今回、コンテンツはそのままJekyllで運用するのでインフラ部分の移行のみです。

Github Pagesから移行した理由

基本的な使い方としてはGithub Pagesで充分満足が行くものでした。ただ、気になる点としては下記2点がありました。

  • HTTPSに対応できない
  • Minifierが使えない

Github Pagesでカスタムドメインとして登録した場合、現状ではHTTPSでコンテンツを提供することができません。ブログコンテンツの内容として、HTTPS通信の必要性はさほどありません。ただ、AppleによるApp Transport Securityなどでは2016年末までという期限こそ延長されたもののデフォルトでHTTPSを提供するようにとした方針に変更はありません。こういった状況を考えれば、HTTPSでのコンテンツ提供準備は早くできているに越したことはありません。

次にMinifierです。jekyll-minifierを使おうとしましたがGithub Pagesではうまく動作しませんでした。

Github Pages + CloudFlare

次にGithub Pagesだけではうまく2つの課題に対応できなかったので、CloudFlareを組み合わせることにしました。CloudFlareを経由して通信を行えば2つの課題は解消できます。もともと、watermint.orgドメインのDNS管理はCloudFlareで行っていたのでCloudFlareを経由するかどうかのオプションを変えるだけですみました。

[Client] ---<HTTP/HTTPS>--- [CloudFlare] ---<HTTP>--- [Github Pages]

CloudFlareを経由すると、HTTPSで接続要求があった場合CloudFlare側でHTTPSで処理してくれます。元のコンテンツであるGithub PagesとCloudFlare間は従来通りHTTPでの通信となります。CloudFlareから証明書も無償で発行してもらえます。

CloudFlareではCDNとしてコンテンツキャッシュをするだけでなく、Minifyのような処理も追加オプションとして用意されています。これも使い方は単純にオプションを有効化すればよいだけで、自動的にHTMLやCSSなどのコンテンツを最適化してくれます。

Aerobaticへの移行

上記のように課題は解決されたので移行の必要性は高くありませんでしたが、機能面でGithub Pagesよりも多彩であることと、Aerobaticサーバ側から直接HTTPS通信をサポートすることができるのでこちらに移行することにしました。AerobaticでもCloudFlareと同様に証明書は無償で発行してもらえます。

[Client] ---<HTTP/HTTPS>--- [Aerobatic]

AerobaticはGithubとGithub Pagesの関係と同様に、BitbucketとAerobaticというようにソースコードレポジトリと対にしてコンテンツを管理します。Aerobaticでは複数のブランチに対してサービスの設定が可能で、ステージング用のブランチとステージング用サブドメインを設定するといった運用ができます。

コンテンツは公開順ごとにバージョンが振られていきロールバックなどもgit操作なしで簡単に行えます。

他には、Githubでプライベートレポジトリは有償ですが、Bitbucketであればプライベートレポジトリも無償で利用できます。Jekyllのサイトデータであれば、公開してあっても特に困りませんが、あまりレポジトリ内のファイルが散らかっていると恥ずかしいので、、といった理由でプライベートにできたほうがうれしいですね。

Websiteバージョン

実はTumblrからJekyllに移行する際の本命がこちらのAerobaticだったのですが後述の問題があったので取り急ぎGithub Pagesに移行することにしていました。

Aerobaticでドメイン設定ができない

AerobaticではCDNにAWSのCloudFrontを使っています。証明書はCloudFront側で発行されるものを使うのですが、この処理でエラーとなってしまい、ホスティングの設定ができませんでした。

検索してもなかなか同様の報告が見当たらず解決の糸口がなかったのですが、今回とりあえずAerobaticのサポート宛てに問い合わせをしてみたところ、翌日には解決となりました。

Aerobaticでカスタムドメインの設定をする際、まずCloudFront側からドメイン所有者であることを確認するためのメールを受け取り、承認処理を行います。この操作のあと、Aerobatic上でverify処理を進めれば次はDNSの設定。となるはずなのですがAerobatic上のverify処理で「CNAME already registered with CloudFront」というエラーがでて一向に進みません。

このことをサポートに問い合わせたところ、すぐにエラーを解消してもらえたのですが、おそらく、ドメインwatermint.orgでCloudFrontを使ったことはなかったので承認処理手順の際に何かの拍子で2回処理が実行されてしまったのかもしれません。

jekyll-minifier + Aerobatic

jekyll-minifierとAerobaticの組み合わせは先月試したときにはうまく動作したのですが、移行したタイミングでは動作しなくなってしまっていました。

jekyll-minifierのリリース履歴を見ると2週間前に課題を解決するために、0.1.0がリリースされておりこの影響によるものでした。jekyll-minifierの参照については _config.yml に下記のように依存を書いておいただけだったのでバージョン指定はしていませんでした。

gems:
 - jekyll-paginate
 - jekyll-minifier

このように依存するライブラリのバージョンアップなどで急に動かなくなってしまっても不都合なので、GemfileGemfile.lockでバージョンを固定しておくことにしました。AerobaticのAutomated Buildsで説明がある通り、(1) _pluginsにある*.rbファイル、(2) GemfileあるいはGemfile.lockがあればbundlerによるインストール、(3) _config.ymlgems配列。といった順でライブラリが参照されます。

Aerobaticでの運用

Aerobaticでは無償プランにて2つのドメインに対してHTTPSを含むコンテンツをホスティングすることができます。一方で無償版の制約としては1日のデプロイ回数が5回までに制限されていることです。

一日に何度も記事を公開するようなブログであれば有償プランを選択する必要があります。また、上述のminifierのような処理を入れようとするタイミングではいろいろ試行錯誤するので5回という制約はかなり作業に影響します。Aerobaticは際立って高価なサービスではないので有償版にアップグレードしてもよいのですが、複数のドメインでサービスを提供したり、大量のコンテンツを持っているということもないので現状は見送って1日5回という制約の中で運用することにしています。