こんちには。
データアナリティクス事業本部 インテグレーション部 機械学習チームの中村です。
今日は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をお使いになられている方の参考になれば幸いです。