📔

NotionAPIを使ってGrasshopperからNotionにアクセスする

2021/05/16に公開

ついにNotion APIがPublic Betaに!

ずっとprivate betaだったNotionの公式APIが5/14ついにpublic betaになりました!
すでにいろんなチュートリアル記事が公開されてますが、今回は建築界隈向けにGrasshopperからNotionにアクセスするというニッチな記事を書いていきます。

今回作るもの

Rhinoにあるオブジェクトの情報をNotionのデータベースに書き込んでいくプログラムを作っていきます。
下記のようにオブジェクトのGUID、Layer、Typeがリストアップされる予定です。

環境

  • Windows10
  • Rhino7
  • GhPython
  • Notion API : 2021-05-13

Notion APIを導入

まずはNotion APIを使えるようにセットアップしていきます。
公式ドキュメントをもとに作っていきます。

ワークスペースを準備

Admin権限のあるワークスペースを準備してください。
不安な方はテスト用に無料プランで新しくワークスペースを作っておくといいと思います。

インテグレーションの作成

公式ドキュメントの「Create a new integration」に書かれているようにintegrationを作成します。

  1. your integrationsにアクセス
  2. 「+ New Integration」をクリック
  3. 「Name」と「Associated workspace」を入力→「Submit」
  4. 「Secrets -> Internal Integration Token」の「Show」を押してトークンを確認
    ※トークンはセキュアに管理してください。
  5. 「Integration type」は今回は「Internal Integration」のまま

データベースを作成

次にデータを書き込むテーブルを作っていきます。

テーブルを作成

  1. 適当なところに新規ページを作成
    ページ名は適当に入力してください。
  2. 「Table」を選択してテーブルを作成
  3. カラムを設定
    今回は下記のようにすべてテキストのカラムにしておきます。
    • GUID: Text
    • Type: Text
    • Layer: Text

APIからデータベースにアクセスできるようにする

データベースをIntegrationと共有して、クライアントから書き込む先のデータベースを指定するためにIDを取得します。

  1. 「Share」→「Add people...」→先ほど作成したIntegrationを選択→「Invite」
    これでIntegrationが作成したデータベースにアクセスできるようになります。
  2. 「Copy Link」でURLをコピーしDatabase ID部分を取り出す
    下記の部分がDatabbase IDになるので、どこかにコピーしておきます。
    https://www.notion.so/myworkspace/a8aec43384f447ed84390e8e42c2e089?v=...
                                      |--------- Database ID --------|
    

Grasshopperでクライアントコードを作成

Rhinoファイルの準備

  1. 適当にオブジェクトを配置していきます
  2. 適当にレイヤー分けしておきます

Grasshopper

Grasshopperのコードは下記のとおりです。

  • Geo: 「Set Multiple Geometries」でRhinoのオブジェクトを読込んでおきます。
  • ID: GeoをGUIDに変換しています。
  • Button: ボタンを押したときにリクエストを送るようにします。
  • Python:
    Input Access Type hint
    run Item bool
    geometries List ghdoc

Pythonコード

最終的なコードは最後にまとめて記載します。

  1. Rhino上のオブジェクトを扱えるようにする
    import scriptcontext as sc
    import Rhino
    
    sc.doc = Rhino.RhinoDoc.ActiveDoc
    
  2. Rhinoのオブジェクト情報をリストに格納する
    # オブジェクト情報を整理
    objects = []
    for geo in geometries:
        attr = {}
        obj = sc.doc.Objects.Find(geo)
        attr["GUID"] = str(obj.Attributes.ObjectId)
        attr["Layer"] = sc.doc.Layers.FindIndex(obj.Attributes.LayerIndex).FullPath
        attr["Type"] = str(obj.GetType())
        objects.append(attr)
    
    それぞれ下記のようなdictionaryがリストに格納されます。
    [
    {'GUID': 'de47bfba-5792-4571-8f1e-527e3d6eee90', 'Layer': 'geometry::floor', 'Type': 'Rhino.DocObjects.BrepObject'},
    {'GUID': '883101d0-d384-4536-8b61-b253ee8b71f9', 'Layer': 'geometry::wall', 'Type': 'Rhino.DocObjects.BrepObject'}
    ]
    
  3. HTTPリクエストを送る関数を作成
    IronPythonではurllibなどを使うことが多いのですが、今回なぜかurllibだと403エラーでNotionAPIにアクセスできなかったのでこちらのリポジトリを参考に.NETの関数を使いました。
    理由わかる方いたら教えてください。
    • ライブラリのインポート
      from System.Net import WebRequest
      from System.Text import ASCIIEncoding
      from System.IO import StreamReader
      from System.Net import ServicePointManager
      from System.Net import SecurityProtocolType
      from System.Net import WebException
      import time, json
      
    • HTTPリクエストの関数
      少しだけ変更しています。
      def http(method, url, data_string=None, header={}, retry=3):
          ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12
          request = WebRequest.Create(url)
          request.UseDefaultCredentials = True
          request.Method = method
          request.ContentLength = 0
          
          for k,v in header.items():
              request.Headers.Add(k,v)
          
          if data_string:
              request.ContentType = "application/json"
              encoding = ASCIIEncoding()
              data = encoding.GetBytes(data_string)
              request.ContentLength = data.Length
              stream = request.GetRequestStream()
              stream.Write(data, 0, data.Length)
              stream.Close()
      
          try:
              response = request.GetResponse()
              print (response.StatusDescription)
              dataStream = response.GetResponseStream()
              reader = StreamReader(dataStream)
          
              responseFromServer = reader.ReadToEnd()
                  
              reader.Close()
              dataStream.Close()
              response.Close()
              return responseFromServer
      
          except WebException as e : 
              if e.Response.StatusDescription == "Not Found" and retry>0:
                  time.sleep(10)
                  return http(method, url, data_string, header, retry-1)
      
              print (e.Response.StatusDescription)
              dataStream = e.Response.GetResponseStream()
              reader = StreamReader(dataStream)
          
              responseFromServer = reader.ReadToEnd()
              print (responseFromServer)
          
              reader.Close()
              dataStream.Close()
      
  4. URL、Token、Headersを設定
    {{YOUR_TOKEN}}の部分に先ほど取得したTokenを入力してください。
    # URL
    url = "https://api.notion.com/v1/pages"
    # Token
    token = "Bearer {{YOUR_TOKEN}}}"
    # headers
    headers = {
                'Authorization': token,
                'Notion-Version': "2021-05-13",
                }
    
  5. Bodyを設定してリクエストを送る
    {{DATEBASE_ID}}の部分に先ほどコピーしたDatabase IDを入力してください。
    効率悪いですが今回はfor文でオブジェクト毎にリクエストを送っていきます。
    Notionの各プロパティごとにいろいろ設定できるようになっているようで、細かく設定したい場合はこちらをご覧ください。
    for obj in objects:
        values = {
            "parent": {"database_id": "{{DATEBASE_ID}}"},
            "properties": {
                "GUID": {
                    "title": [
                        {
                        "text": {
                            "content": obj["GUID"]
                            }
                        }
                    ]
                },
                "Layer": {
                    "rich_text": [
                        {
                            "text": {
                                "content": obj["Layer"]
                            }
                        }
                    ]
                },
                "Type": {
                    "rich_text": [
                        {
                            "text": {
                                "content": obj["Type"]
                            }
                        }
                    ]
                },
            }
            }
        # bodyを文字列化
        data = json.dumps(values)
        # POST
        http("POST",url,data,headers)
    
  6. 最終的なコード
    # coding: -*- utf-8 -*-
    import scriptcontext as sc
    import Rhino
    
    sc.doc = Rhino.RhinoDoc.ActiveDoc
    
    # APIリクエスト用のimport
    from System.Net import WebRequest
    from System.Text import ASCIIEncoding
    from System.IO import StreamReader
    from System.Net import ServicePointManager
    from System.Net import SecurityProtocolType
    from System.Net import WebException
    import time, json
    
    # APIリクエストを送る関数
    def http(method, url, data_string=None, header={}, retry=3):
        ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12
        request = WebRequest.Create(url)
        request.UseDefaultCredentials = True
        request.Method = method
        request.ContentLength = 0
        
        for k,v in header.items():
            request.Headers.Add(k,v)
        
        if data_string:
            request.ContentType = "application/json"
            encoding = ASCIIEncoding()
            data = encoding.GetBytes(data_string)
            request.ContentLength = data.Length
            stream = request.GetRequestStream()
            stream.Write(data, 0, data.Length)
            stream.Close()
    
        try:
            response = request.GetResponse()
            print (response.StatusDescription)
            dataStream = response.GetResponseStream()
            reader = StreamReader(dataStream)
        
            responseFromServer = reader.ReadToEnd()
                
            reader.Close()
            dataStream.Close()
            response.Close()
            return responseFromServer
    
        except WebException as e : 
            if e.Response.StatusDescription == "Not Found" and retry>0:
                time.sleep(10)
                return http(method, url, data_string, header, retry-1)
    
            print (e.Response.StatusDescription)
            dataStream = e.Response.GetResponseStream()
            reader = StreamReader(dataStream)
        
            responseFromServer = reader.ReadToEnd()
            print (responseFromServer)
        
            reader.Close()
            dataStream.Close()
    
    if __name__ == "__main__":
        # オブジェクト情報を整理
        objects = []
        for geo in geometries:
            attr = {}
            obj = sc.doc.Objects.Find(geo)
            attr["GUID"] = str(obj.Attributes.ObjectId)
            attr["Layer"] = sc.doc.Layers.FindIndex(obj.Attributes.LayerIndex).FullPath
            attr["Type"] = str(obj.GetType())
            objects.append(attr)
        # HTTPリクエストを作成
        # URL
        url = "https://api.notion.com/v1/pages"
        # Token
        token = "Bearer {{YOUR_TOKEN}}"
        # headers
        headers = {
                    'Authorization': token,
                    'Notion-Version': "2021-05-13",
                    }
        # オブジェクトごとにbodyを作ってPOST
        if run:
            for obj in objects:
                values = {
                    "parent": {"database_id": "{{DATEBASE_ID}}"},
                    "properties": {
                        "GUID": {
                            "title": [
                                {
                                "text": {
                                    "content": obj["GUID"]
                                    }
                                }
                            ]
                        },
                        "Layer": {
                            "rich_text": [
                                {
                                    "text": {
                                        "content": obj["Layer"]
                                    }
                                }
                            ]
                        },
                        "Type": {
                            "rich_text": [
                                {
                                    "text": {
                                        "content": obj["Type"]
                                    }
                                }
                            ]
                        },
                    }
                    }
                # bodyを文字列化
                data = json.dumps(values)
                # POST
                http("POST",url,data,headers)
    

リクエストを送ってみる

コードができたのでButtonを押してリクエストを送ってみます。
リクエストの関数の中にsleepが入っているので少し時間かかると思います。
Notionを見てみると無事データベースにアイテムが追加されているのがわかると思います。

夢が広がりますね!

今回サクッとやるために簡単なデータしか送ってませんが、いろんなことに応用できそうです。
また思いついたら記事化していこうと思います。

  • NotionのデータベースもとにGHのコードを動かす
  • シミュレーションや最適化の結果をNotionでレポート化する
  • BIMデータを集計する

参考

Notion Developers
aictrl/IronPythonHttpRequest.py

Discussion