Backlogの課題をPythonでAPI使ってええ感じに出力する方法【添付ファイル・コメントも対応】

2023.03.30

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんちには。

データアナリティクス事業本部 インテグレーション部 機械学習チームの中村です。

今日はBacklogの課題をPythonでええ感じに出力(エクスポート)する方法を紹介します。

本記事の方法でできること

本記事の方法でできることは以下です。

  • 各課題をテキストファイルに落とす
  • 上記テキストファイルには、コメントと日時・作成者を含める
  • 添付ファイルもダウンロードする

結果としては以下のような形式で落とすようになっています。

{プロジェクト名}-001-{チケットのタイトル}/
  ├ body.backlog
  └ attachment/
    ├ hoge.png
    └ fuga.csv
{プロジェクト名}-002-{チケットのタイトル}/
  ├ body.backlog
  └ attachment/
    ├ hogehoge.png
    └ fugafuga.csv
...

body.backlogの中身は以下のように、課題自体とコメントをまとめたものになります。

以下のような仕様でクラスを作成する。

- クラス名:SampleClass
- メソッド
-- __init__()
-- hogehoge : hogehogeな処理をする
-- fugafuga : fugafugaな処理をする


---------------------------------------
comment created: 2023-02-16T02:42:27Z
author: 田中<tanaka@example.com>
---------------------------------------
SAMPLE_PRJ-1 クラスを実装

---------------------------------------
comment created: 2023-02-16T02:42:27Z
author: 田中<tanaka@example.com>
---------------------------------------
SAMPLE_PRJ-1 レビュー指摘事項を修正

---------------------------------------
comment created: 2023-02-16T03:09:14Z
author: 田中<tanaka@example.com>
---------------------------------------
Merge branch 'SAMPLE_PRJ-1 '

---------------------------------------
comment created: 2023-03-06T06:27:58Z
author: sato<sato@example.com>
---------------------------------------
報告済みのため完了とします。

課題一覧をエクスポートする先行事例

Backlog公式でも以下の方法が使用可能です。

ただしこの方法では、コメントが横方向に広がった形式の可変長テーブルとなったり、添付ファイルが取得できなかったりするため、今回はAPIを使って実装をしました。

また、その他弊社のブログでも添付ファイルを含めてダウンロードする例が、Go言語で紹介されていました。

こちらは私のやりたいことに近いですが、Go言語の経験がないため今回Pythonで書いてみました。

実装説明

準備

使用するライブラリは以下です。pipなどでインストールしてください。

(私はpoetry環境でやっていますが環境に合わせてお好みで実施ください)

requests==2.28.2
python-dotenv==1.0.0

あらかじめ以下の環境変数を.envに記述しておきます。

(環境変数の設定方法もお好みでOKです)

BACKLOG_API_KEY={APIキー}
BACKLOG_PROJECT_ID={プロジェクトID}
BACKLOG_BASE_URL={ベースURL}
BACKLOG_PROJECT_NAME={プロジェクト名}

APIキーは、「個人設定」の「API」からAPIキーを発行すればOKです。

プロジェクトIDは左メニューから以下のような「プロジェクト設定」をクリックし、

その際にブラウザのURLに表示される以下のような末尾の数字からわかります。

https://example.backlog.jp/ViewPermission.action?projectId={プロジェクトID}

ベースURLはhttps://example.backlog.jpのような部分です。

プロジェクト名は、課題番号に必ず付くプレフィックスと同じです。

コード

コードは以下のようになっています。

import os
import json
import pathlib
import re

from dotenv import load_dotenv
import requests
load_dotenv(verbose=True)

def main(output_dir="./output"):

    # 環境変数からの取得
    BACKLOG_API_KEY = os.environ.get("BACKLOG_API_KEY")
    BACKLOG_PROJECT_ID = os.environ.get("BACKLOG_PROJECT_ID")
    BACKLOG_PROJECT_NAME = os.environ.get("BACKLOG_PROJECT_NAME")
    BACKLOG_BASE_URL = os.environ.get("BACKLOG_BASE_URL")

    # クエリパラメータにプロジェクトIDとAPIキーを含める
    payload = {
        'projectId[]': f'{BACKLOG_PROJECT_ID}'
        , 'apiKey': f'{BACKLOG_API_KEY}'
    }

    # 課題一覧を取得
    response = requests.get(BACKLOG_BASE_URL + "/api/v2/issues", params=payload)
    issues = json.loads(response.text)

    # 課題番号の昇順にソート
    issues = sorted(issues, key=lambda v: v["keyId"])

    # 課題のループ
    for issue in issues:

        keyId = issue['keyId'] # 課題番号(プロジェクト名なし)
        summary: str = issue['summary'] # タイトル
        description = issue['description'] # 記載内容

        # ファイル名に使えないものを置換する
        summary = re.sub(r'[\\|/|:|?|.|"|<|>|\|]', '-', summary)

        print(f"{BACKLOG_PROJECT_NAME}-{keyId:03d}-{summary}")

        # 課題毎に出力フォルダを作成
        output_path = pathlib.Path(output_dir)\
            .joinpath(f"{BACKLOG_PROJECT_NAME}-{keyId:03d}-{summary}")
        output_path.mkdir(parents=True, exist_ok=True)

        # 課題本文のファイル名
        output_body_path = output_path.joinpath(f"body.backlog")

        # 課題本文への出力
        with open(output_body_path, "wt") as f:

            # 課題自体の内容はそのまま出力
            f.writelines(description)

            # 課題のコメントをすべて取得
            issue_id = issue['id']
            response = requests\
                .get(BACKLOG_BASE_URL + f"/api/v2/issues/{issue_id}/comments", params=payload)
            comments = json.loads(response.text)

            # コメントを古い順(昇順)となるようソート(そのままだと新しいものが先頭にきていた)
            comments = sorted(comments, key=lambda v: v["created"])

            # 見やすさのためのpadding
            f.writelines(["\n", "\n"])

            # コメントのループ
            for comment in comments:
                # コメント内容
                content = comment['content']
                # コメント作成者の名前
                createdUserName = comment['createdUser']['name']
                # コメント作成者のメールアドレス
                createdUserMailAddress = comment['createdUser']['mailAddress']
                # コメント作成日
                created = comment['created']

                # Noneの場合があったためケア
                if content is None:
                    continue

                # コメントに関するメタデータを出力
                f.writelines([
                    "\n---------------------------------------"
                    , f"\ncomment created: {created}"
                    , f"\nauthor: {createdUserName}<{createdUserMailAddress}>"
                    , "\n---------------------------------------"
                ])

                # コメントそのものを出力
                f.writelines(["\n"])
                f.writelines(content)
                f.writelines(["\n"])

        # 添付ファイルの情報を取得
        response = requests\
            .get(BACKLOG_BASE_URL + f"/api/v2/issues/{issue_id}/attachments", params=payload)
        attachments = json.loads(response.text)

        # 添付ファイル情報のループ
        for attachment in attachments:

            attachment_id = attachment['id']
            attachment_name = attachment['name']

            # 出力先のファイル名を合成
            output_attachment_path = output_path.joinpath("attachment", f"{attachment_name}")
            output_attachment_path.parent.mkdir(parents=True, exist_ok=True)

            # 添付ファイルのデータを取得
            response = requests.get(
                BACKLOG_BASE_URL + f"/api/v2/issues/{issue_id}/attachments/{attachment_id}"
                , params=payload)

            # 添付ファイルはバイナリとして出力
            with open(output_attachment_path, "wb") as f:
                f.write(response.content)

    return

if __name__ == "__main__":
    main()

コメントに説明を書いておきました。ほぼ難しい部分はなく、requestsライブラリに慣れていればすぐに理解できると思います。

敢えてポイントをいくつかあげると、以下でファイル名に使えない文字列をハイフンに置き換えます。

        # ファイル名に使えないものを置換する
        summary = re.sub(r'[\\|/|:|?|.|"|<|>|\|]', '-', summary)

またコメントはそのままでは新しいものが先頭に来るため、古い順に並べなおしました。

            # コメントを古い順(昇順)となるようソート(そのままだと新しいものが先頭にきていた)
            comments = sorted(comments, key=lambda v: v["created"])

こちらのコードを実行すると、./output/にエクスポートした結果が格納されます。

まとめ

いかがでしたでしょうか。

本記事が、Backlogをお使いになられている方の参考になれば幸いです。