Notion APIでWeb API操作を勉強してみた

本記事は[富士通クラウドテクノロジーズ Advent Calendar 2023](https://qiita.com/advent-calendar/2023/fjct) 22日目の記事です。
昨日の記事は@ystkfujiiの[人に運勢があるなら、HTTPリクエストにも運勢があるはず（謎](https://qiita.com/ystkfujii/items/3cde23395ee6570a9743)でした。

本日は、理解できているようでよく理解できていないと日頃感じていたWeb API操作について勉強してみたという記事です。

# Web APIとは？
Web APIという用語は聞いたことがあるが、どのように利用するがわかっていないので調べてみる。

そもそもWeb APIとは何かを調べてみた。
- HTTPを使用してデータのやり取りをするAPI
    - URLでリソース位置を指定
    - GET、POSTなどのHTTPメソッドでデータのやり取りをする
    - HTTPヘッダ、HTTPボディにデータを載せて情報の転送を行う
    - 有名なWeb APIに、REST APIがある
    - REST APIは、URI（URL）に対してCRUD（Create,Read,Update,Delete）操作を行う
- JSON形式でデータのやり取りすることが多い
- 認証のために、トークンが必要なことが多い 

# Requestモジュールの使い方確認
今回はNotion APIを使用して、Web APIの操作を学ぶ。
以下を参考に、使用するためのインテグレーションを作成し、トークンを発行は準備済み。
[Notion API を使用してデータベースを操作する](https://zenn.dev/kou_pg_0131/articles/notion-api-usage)

一番シンプルなbotユーザの取得を試してみる。
HTTPのヘッダーには、Notionのバージョン、認証トークン、Content-TypeにJSONを指定する。`{api_token}`は認証トークン。
```RequestsTest.py
import requests
import json

url = 'https://api.notion.com/v1/users/me'
api_key = '{api_key}'

def RequestsTest(url, api_key):
    headers = {
        "Notion-Version": "2022-06-28",
        "Authorization": "Bearer " + api_key,
        "Content-Type": "application/json"
    }

    response = requests.post(url, headers=headers)
    print(response)
    return

RequestsTest(url, api_key)
```

以下、実行結果。
```
<Response [200]>
```
あれ？APIの情報がJSON形式で返ってくるのでは？と思ったが、ステータスコードが返ってきた。

`response.json()`とするとJSON情報を確認できる。
```
{'object': 'user', 'id': '（記載省略）', 'name': 'notion_api', 'avatar_url': None, 'type': 'bot', 'bot': {'owner': {'type': 'workspace', 'workspace': True}, 'workspace_name': '○○さんのNotion'}, 'request_id': '（記載省略）'}
```

# JSONモジュールの使い方確認
次に取得したJSONの情報をファイルに書き込む方法を調べてみる。
[【Python】JSONの読書き｜dump、dumpsの違い](https://nashiza-blog.com/2020/08/16/python-json/)

`response.json()`で読み込んだものは`<class 'dict'>`である。
辞書型であるため、上記記事の「辞書型をJSON形式でファイルに出力する：json.dump( )」を参考にファイル出力を行う。

```RequestsTestJsonDump.py
import requests
import json

url = 'https://api.notion.com/v1/users/me'
api_key = '{api_key}'

def RequestsTestJsonDump(url, api_key):
    headers = {
        "Notion-Version": "2022-06-28",
        "Authorization": "Bearer " + api_key,
        "Content-Type": "application/json"
    }

    response = requests.get(url, headers=headers)
    json_dict = response.json()
    with open('getbotuser.json', mode='wt', encoding='utf-8') as f:
        json.dump(json_dict, f, ensure_ascii=False, indent=4)
    return

RequestsTestJsonDump(url, api_key)
```

```getbotuser.json
{
    "object": "user",
    "id": "（記載省略）",
    "name": "notion_api",
    "avatar_url": null,
    "type": "bot",
    "bot": {
        "owner": {
            "type": "workspace",
            "workspace": true
        },
        "workspace_name": "○○さんのNotion"
    },
    "request_id": "（記載省略）"
}
```
JSONファイルを綺麗に表示する方法は以下を参考にした。
[[python] JSONファイルのフォーマットを整えてDumpする](https://qiita.com/Hyperion13fleet/items/7129623ab32bdcc6e203)

# APIを実行してPageの情報を取得する
サンプルとして以下のPageを作成。
![スクリーンショット 2023-12-09 18.18.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626032/e0f287d6-b109-4ee6-51dd-18b6572aa72b.png)

Pageの情報を取得する。
```GetPageObject.py
import requests
import json

url = 'https://api.notion.com/v1/pages/{page_id}'
api_key = {api_token}

def GetPageObject(url, api_key):
    headers = {
        "Notion-Version": "2022-06-28",
        "Authorization": "Bearer " + api_key,
        "Content-Type": "application/json"
    }

    response = requests.get(url, headers=headers)
    json_dict = response.json()
    with open('getpage.json', mode='wt', encoding='utf-8') as f:
        json.dump(json_dict, f, ensure_ascii=False, indent=4)

    return

GetPageObject(url, api_key)
```
`{page_id}`はURLに記載されているもの。`{api_token}`はトークン情報。
取得したJSONの情報からNotion APIのデータモデルを確認してみる。
```getpage.json
{
    "object": "page",
    "id": "（記載省略）",
    "created_time": "2023-12-06T16:38:00.000Z",
    "last_edited_time": "2023-12-09T08:48:00.000Z",
    "created_by": {
        "object": "user",
        "id": "（記載省略）"
    },
    "last_edited_by": {
        "object": "user",
        "id": "（記載省略）"
    },
    "cover": null,
    "icon": null,
    "parent": {
        "type": "workspace",
        "workspace": true
    },
    "archived": false,
    "properties": {
        "title": {
            "id": "title",
            "type": "title",
            "title": [
                {
                    "type": "text",
                    "text": {
                        "content": "プロジェクトタスク",
                        "link": null
                    },
                    "annotations": {
                        "bold": false,
                        "italic": false,
                        "strikethrough": false,
                        "underline": false,
                        "code": false,
                        "color": "default"
                    },
                    "plain_text": "プロジェクトタスク",
                    "href": null
                }
            ]
        }
    },
    "url": "https://www.notion.so/（記載省略）",
    "public_url": null,
    "request_id": "（記載省略）"
}
```
上記ではPageに作成したDatabase情報とBlock情報は見えない。
Database情報とBlock情報は以下で取得できる。
```GetPageChidrenObject.py
import requests
import json

url = 'https://api.notion.com/v1/blocks/{page_id}/children'
api_key = '{api_token}'

def GetPageChidrenObject(url, api_key):
    headers = {
        "Notion-Version": "2022-06-28",
        "Authorization": "Bearer " + api_key,
        "Content-Type": "application/json"
    }

    response = requests.get(url, headers=headers)
    json_dict = response.json()
    with open('getchildren.json', mode='wt', encoding='utf-8') as f:
        json.dump(json_dict, f, ensure_ascii=False, indent=4)

    return

GetPageChidrenObject(url, api_key)
```
取得結果は以下。
```getchildren.json
{
    "object": "list",
    "results": [
        {
            "object": "block",
            "id": "（記載省略）",
            "parent": {
                "type": "page_id",
                "page_id": "（記載省略）"
            },
            "created_time": "2023-12-06T16:38:00.000Z",
            "last_edited_time": "2023-12-09T08:38:00.000Z",
            "created_by": {
                "object": "user",
                "id": "（記載省略）"
            },
            "last_edited_by": {
                "object": "user",
                "id": "（記載省略）"
            },
            "has_children": false,
            "archived": false,
            "type": "child_database",
            "child_database": {
                "title": "タスク一覧"
            }
        },
        {
            "object": "block",
            "id": "（記載省略）",
            "parent": {
                "type": "page_id",
                "page_id": "（記載省略）"
            },
            "created_time": "2023-12-09T06:05:00.000Z",
            "last_edited_time": "2023-12-09T08:39:00.000Z",
            "created_by": {
                "object": "user",
                "id": "（記載省略）"
            },
            "last_edited_by": {
                "object": "user",
                "id": "（記載省略）"
            },
            "has_children": false,
            "archived": false,
            "type": "child_database",
            "child_database": {
                "title": "メンバー"
            }
        },
        {
            "object": "block",
            "id": "（記載省略）",
            "parent": {
                "type": "page_id",
                "page_id": "（記載省略）"
            },
            "created_time": "2023-12-09T08:28:00.000Z",
            "last_edited_time": "2023-12-09T08:29:00.000Z",
            "created_by": {
                "object": "user",
                "id": "（記載省略）"
            },
            "last_edited_by": {
                "object": "user",
                "id": "（記載省略）"
            },
            "has_children": false,
            "archived": false,
            "type": "heading_1",
            "heading_1": {
                "rich_text": [
                    {
                        "type": "text",
                        "text": {
                            "content": "今週のトピック",
                            "link": null
                        },
                        "annotations": {
                            "bold": false,
                            "italic": false,
                            "strikethrough": false,
                            "underline": false,
                            "code": false,
                            "color": "default"
                        },
                        "plain_text": "今週のトピック",
                        "href": null
                    }
                ],
                "is_toggleable": false,
                "color": "yellow_background"
            }
        },
        {
            "object": "block",
            "id": "（記載省略）",
            "parent": {
                "type": "page_id",
                "page_id": "（記載省略）"
            },
            "created_time": "2023-12-09T08:29:00.000Z",
            "last_edited_time": "2023-12-09T08:29:00.000Z",
            "created_by": {
                "object": "user",
                "id": "（記載省略）"
            },
            "last_edited_by": {
                "object": "user",
                "id": "（記載省略）"
            },
            "has_children": false,
            "archived": false,
            "type": "divider",
            "divider": {}
        },
        {
            "object": "block",
            "id": "（記載省略）",
            "parent": {
                "type": "page_id",
                "page_id": "（記載省略）"
            },
            "created_time": "2023-12-09T08:29:00.000Z",
            "last_edited_time": "2023-12-09T08:29:00.000Z",
            "created_by": {
                "object": "user",
                "id": "（記載省略）"
            },
            "last_edited_by": {
                "object": "user",
                "id": "（記載省略）"
            },
            "has_children": false,
            "archived": false,
            "type": "bulleted_list_item",
            "bulleted_list_item": {
                "rich_text": [
                    {
                        "type": "text",
                        "text": {
                            "content": "あああああ",
                            "link": null
                        },
                        "annotations": {
                            "bold": false,
                            "italic": false,
                            "strikethrough": false,
                            "underline": false,
                            "code": false,
                            "color": "default"
                        },
                        "plain_text": "あああああ",
                        "href": null
                    }
                ],
                "color": "default"
            }
        },
        {
            "object": "block",
            "id": "（記載省略）",
            "parent": {
                "type": "page_id",
                "page_id": "（記載省略）"
            },
            "created_time": "2023-12-09T08:29:00.000Z",
            "last_edited_time": "2023-12-09T08:29:00.000Z",
            "created_by": {
                "object": "user",
                "id": "（記載省略）"
            },
            "last_edited_by": {
                "object": "user",
                "id": "（記載省略）"
            },
            "has_children": false,
            "archived": false,
            "type": "bulleted_list_item",
            "bulleted_list_item": {
                "rich_text": [
                    {
                        "type": "text",
                        "text": {
                            "content": "いいいいい",
                            "link": null
                        },
                        "annotations": {
                            "bold": false,
                            "italic": false,
                            "strikethrough": false,
                            "underline": false,
                            "code": false,
                            "color": "default"
                        },
                        "plain_text": "いいいいい",
                        "href": null
                    }
                ],
                "color": "default"
            }
        },
        {
            "object": "block",
            "id": "（記載省略）",
            "parent": {
                "type": "page_id",
                "page_id": "（記載省略）"
            },
            "created_time": "2023-12-09T08:29:00.000Z",
            "last_edited_time": "2023-12-09T08:29:00.000Z",
            "created_by": {
                "object": "user",
                "id": "（記載省略）"
            },
            "last_edited_by": {
                "object": "user",
                "id": "（記載省略）"
            },
            "has_children": false,
            "archived": false,
            "type": "bulleted_list_item",
            "bulleted_list_item": {
                "rich_text": [
                    {
                        "type": "text",
                        "text": {
                            "content": "ううううう",
                            "link": null
                        },
                        "annotations": {
                            "bold": false,
                            "italic": false,
                            "strikethrough": false,
                            "underline": false,
                            "code": false,
                            "color": "default"
                        },
                        "plain_text": "ううううう",
                        "href": null
                    }
                ],
                "color": "default"
            }
        }
    ],
    "next_cursor": null,
    "has_more": false,
    "type": "block",
    "block": {},
    "request_id": "（記載省略）"
}
```
`https://api.notion.com/v1/blocks/{page_id}/children`というエンドポイントにGETすることで、Paseにある全てのObjectが取得できる。

特定のDatabase情報を取得したい場合は、以下。
ここでは、`タスク一覧`を取得する。
`{database_id}`は、`getchildren.json`から確認した。
```GetDatabaseObject.py
import requests
import json

url = 'https://api.notion.com/v1/databases/{database_id}'
api_key = '{api_token}'

def GetDatabaseObject(url, api_key):
    headers = {
        "Notion-Version": "2022-06-28",
        "Authorization": "Bearer " + api_key,
        "Content-Type": "application/json"
    }

    response = requests.get(url, headers=headers)
    json_dict = response.json()
    with open('getdatabase.json', mode='wt', encoding='utf-8') as f:
        json.dump(json_dict, f, ensure_ascii=False, indent=4)

    return

GetDatabaseObject(url, api_key)
```

```getdatabase.json
{
    "object": "database",
    "id": "（記載省略）",
    "cover": null,
    "icon": null,
    "created_time": "2023-12-06T16:38:00.000Z",
    "created_by": {
        "object": "user",
        "id": "（記載省略）"
    },
    "last_edited_by": {
        "object": "user",
        "id": "（記載省略）"
    },
    "last_edited_time": "2023-12-09T08:38:00.000Z",
    "title": [
        {
            "type": "text",
            "text": {
                "content": "タスク一覧",
                "link": null
            },
            "annotations": {
                "bold": false,
                "italic": false,
                "strikethrough": false,
                "underline": false,
                "code": false,
                "color": "default"
            },
            "plain_text": "タスク一覧",
            "href": null
        }
    ],
    "description": [],
    "is_inline": true,
    "properties": {
        "担当者": {
            "id": "（記載省略）",
            "name": "担当者",
            "type": "relation",
            "relation": {
                "database_id": "（記載省略）",
                "type": "single_property",
                "single_property": {}
            }
        },
        "ステータス": {
            "id": "（記載省略）",
            "name": "ステータス",
            "type": "status",
            "status": {
                "options": [
                    {
                        "id": "（記載省略）",
                        "name": "未着手",
                        "color": "default"
                    },
                    {
                        "id": "（記載省略）",
                        "name": "進行中",
                        "color": "blue"
                    },
                    {
                        "id": "（記載省略）",
                        "name": "完了",
                        "color": "green"
                    }
                ],
                "groups": [
                    {
                        "id": "（記載省略）",
                        "name": "To-do",
                        "color": "gray",
                        "option_ids": [
                            "（記載省略）"
                        ]
                    },
                    {
                        "id": "（記載省略）",
                        "name": "In progress",
                        "color": "blue",
                        "option_ids": [
                            "（記載省略）"
                        ]
                    },
                    {
                        "id": "（記載省略）",
                        "name": "Complete",
                        "color": "green",
                        "option_ids": [
                            "（記載省略）"
                        ]
                    }
                ]
            }
        },
        "工程": {
            "id": "（記載省略）",
            "name": "工程",
            "type": "multi_select",
            "multi_select": {
                "options": [
                    {
                        "id": "（記載省略）",
                        "name": "運用",
                        "color": "green"
                    },
                    {
                        "id": "（記載省略）",
                        "name": "テスト",
                        "color": "red"
                    },
                    {
                        "id": "（記載省略）",
                        "name": "開発",
                        "color": "default"
                    },
                    {
                        "id": "（記載省略）",
                        "name": "設計",
                        "color": "pink"
                    }
                ]
            }
        },
        "期日": {
            "id": "（記載省略）",
            "name": "期日",
            "type": "date",
            "date": {}
        },
        "名前": {
            "id": "title",
            "name": "名前",
            "type": "title",
            "title": {}
        }
    },
    "parent": {
        "type": "page_id",
        "page_id": "（記載省略）"
    },
    "url": "https://www.notion.so/（記載省略）",
    "public_url": null,
    "archived": false,
    "request_id": "（記載省略）"
}
```
特定のBlock情報を取得したい場合は、以下。

ここでは、`コールアウトした"今週のトピック"`の情報を取得する。
`{block_id}`は、`getchildren.json`から確認した。
```GetBlockObject.py
import requests
import json

url = 'https://api.notion.com/v1/blocks/{block_id}'
api_key = '{api_key}'

def GetBlockObject(url, api_key):
    headers = {
        "Notion-Version": "2022-06-28",
        "Authorization": "Bearer " + api_key,
        "Content-Type": "application/json"
    }

    response = requests.get(url, headers=headers)
    json_dict = response.json()
    with open('getblock.json', mode='wt', encoding='utf-8') as f:
        json.dump(json_dict, f, ensure_ascii=False, indent=4)

    return

GetBlockObject(url, api_key)
```

```getblock.json
{
    "object": "block",
    "id": "（記載省略）",
    "parent": {
        "type": "page_id",
        "page_id": "（記載省略）"
    },
    "created_time": "2023-12-09T08:28:00.000Z",
    "last_edited_time": "2023-12-09T08:29:00.000Z",
    "created_by": {
        "object": "user",
        "id": "（記載省略）"
    },
    "last_edited_by": {
        "object": "user",
        "id": "（記載省略）"
    },
    "has_children": false,
    "archived": false,
    "type": "heading_1",
    "heading_1": {
        "rich_text": [
            {
                "type": "text",
                "text": {
                    "content": "今週のトピック",
                    "link": null
                },
                "annotations": {
                    "bold": false,
                    "italic": false,
                    "strikethrough": false,
                    "underline": false,
                    "code": false,
                    "color": "default"
                },
                "plain_text": "今週のトピック",
                "href": null
            }
        ],
        "is_toggleable": false,
        "color": "yellow_background"
    },
    "request_id": "（記載省略）"
}
```

# Databaseを更新してみる
以下のような学習テーブルに、"深層学習入門"という行をAPIを使用して追加してみる。
![スクリーンショット 2023-12-18 22.16.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626032/079356a5-ea6c-75c6-f0f9-1c5eec582a17.png)

対象のエンドポイントが何かを確認してみる。
https://developers.notion.com/reference/update-a-database
>**Interacting with database rows**
>This endpoint cannot be used to update database rows.
>
>To update the properties of a database row — rather than a column — use the Update page properties endpoint. To add a new row to a database, use the Create a page endpoint

公式リファレンスに上記の記載があり、Pageのエンドポイントを対象にPOSTをリクエストする必要がわかる。

レコードの追加方法とHTTPリクエストボディに記載する情報は以下にを参考にした。
[Notion DataBaseをAPIから操作する際の用途別リクエスト構成についてまとめてみた](https://dev.classmethod.jp/articles/request-parameters-for-notion-api-databases/)
[Notion APIからDBアイテムを追加するときのプロパティごとの書き方](https://www.6666666.jp/blog/post/20210617/)

```CreateDatabaseRow.py
import requests

url = 'https://api.notion.com/v1/pages/'
api_key = '{api_key}'

def CreateDatabaseRowObject(url, api_key):
    headers = {
        "Notion-Version": "2022-06-28",
        "Authorization": "Bearer " + api_key,
        "Content-Type": "application/json"
    }
    item_data = {
        "parent": {"database_id": "{database_id}"},
        "properties": {
            "ジャンル": {
                "multi_select": [
                    {
                        "name": "AI"
                    },
                ]
            },
            "チェックボックス": {
                "checkbox": False
            },
            "名前": {
                "title": [
                    {
                        "text": {
                            "content": "深層学習入門"
                        }
                    }
                ]
            }
        }
    }
    response = requests.post(url, headers=headers, json=item_data)
    return

CreateDatabaseRowObject(url, api_key)
```
追加することができた。
![スクリーンショット 2023-12-18 22.17.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626032/bb529b78-b5b6-115c-cb85-990c941de970.png)

# まとめ
- Web APIの概念
- Requestsモジュールの使い方
- JSON形式のデータの扱い方
- APIのデータモデルの一例

が理解できた。
今後は、Notion APIを利用して、Notionと別アプリケーションの連携プログラムを作成していきたい。

# その他参考資料
以下の書籍を参考にした。
- [Notion API活用術](https://www.c-r.com/book/detail/1492)

#
明日は@hasunumaの「社内CTFのFJCTF#2を開催した話」です。
お楽しみに！