[Python] Redmine APIで情報を読み込む

# 初めに
プロジェクト管理SWであるRedmineに関して、Pythonを用いて情報を読み込む操作をまとめた。
以下の環境で実施した。

|||
|---|---|
|os|windows|
|python|3.7.1|
|python-redmine|2.3.0|
|anaconda|4.8.5|

# python-redmineとは
PythonからRedmineを操作できるAPI。
チケットやプロジェクトの作成、読み込み、更新、削除が可能。
本記事は読み込みのみを対象とする。

# インストール方法
[anaconda cloud](https://anaconda.org/auto/python-redmine) からインストール。

```bash
$ conda install -c auto python-redmine
```

# 接続設定
Redmineへ接続するにはRemine側とpython側で設定する必要がある。

## Redmine側
管理者権限を有するユーザが、Redmine設定を行う。
[管理] → [設定] → [REST APIによるWebサービスを有効にする] を有効にする。

## Python側
アクセスするユーザの情報(ユーザ名およびパスワード)もしくは、APIアクセスキーを使用する。

``` python
from redminelib import Redmine
REDMINE_URL = r'http://user/redmine' # Redmineのパス
REDMINE_KEY = 'xxxxxx' # APIアクセスキー
redmine = Redmine(REDMINE_URL, key = REDMINE_KEY)
```

もしくは

``` python
from redminelib import Redmine
REDMINE_URL = r'http://user/redmine' # Redmineのパス
USER = 'hoge' # ユーザ名
PASS = 'hogehoge' # パスワード
redmine = Redmine(REDMINE_URL, username = USER, password = PASS)
```

# チケット情報の取得
Redmineのチケット情報を取得するには、get()かall()かfilter()の3通りで取得できる。
特定のチケットのみを取得する場合はredmine.issue.get(チケットID)で取得する。
取得したチケット内の情報は戻り値になっているオブジェクト内にある。
対象とするチケットIDのチケットが存在しない場合は、redmine.exceptions.ResourceNotFoundError例外が発生する。

``` python
from redminelib import Redmine
from redminelib.exceptions import ResourceNotFoundError
REDMINE_URL = r'http://user/redmine' # Redmineのパス
REDMINE_KEY = 'xxxxxx' # APIアクセスキー
redmine = Redmine(REDMINE_URL, key = REDMINE_KEY)

ticket_id = 4 # チケットのID
try:
    issue = redmine.issue.get(ticket_id)
except(ResourceNotFoundError):
    print('Ticket ID %s not found' % ticket_id)
```

# チケット情報のまとめ

python-redmineに詳細記載されている内容をまとめる。
https://python-redmine.com/resources/issue.html
なお、チケットに該当の情報が登録されていない場合、ISSUEのプロパティ自体が存在していない。
そのため、情報取得に関して該当のプロパティを所持しているか判定を行う必要がある。

```python
info = issue.property if hasattr(issue, 'property') else None
```

|Redmine情報|ISSUE内情報|
|---|---|---|
| チケットID | issue.id |
| チケットURL | issue.url|
| トラッカーID | issue.tracker.id |
| トラッカー | issue.tracker.name |
| 題名| issue.subject |
| 説明 | issue.description |
| ステータスID | issue.status.id |
| ステータス | issue.status.name |
| 優先度ID | issue.priority.id |
| 優先度 | issue.priority.name |
| 担当者ID | issue.assigned_to.id |
| 担当者名 | issue.assigned_to.name |
| 親チケットID | issue.parent |
| 開始日 | issue.start_date |
| 期日 | issue.due_date |
| 予定工数| issue.estimated_hours |
| プロジェクトID | issue.project.id |
| プロジェクト | issue.project.name |
| 作成者ID | issue.author.id |
| 作成者名 | issue.author.name |
| 進捗率 | issue.done_ratio |
| 作成日 | issue.created_on |
| 更新日 | issue.updated_on |
| 作業時間 | issue.spent_hours |

なお、複数の情報があるものはそれぞれのオブジェクトに各情報が入っている。

|                    |                     |                |
|---                 |---                  |---             |
| ウォッチャー       | issue.watchers      |                |
| ウォッチャーID     |                     | .id            |
| ウォッチャー名     |                     | .name          |
| カスタム領域       | issue.custom_fields |                |
| カスタムID         |                     | .id            |
| カスタム名         |                     | .name          |
| カスタム値         |                     | .value         |
| 送付ファイル       | issue.attachments   |                |
| ファイルID         |                     | .id            |
| ファイル名         |                     | .filename      |
| ファイル説明       |                     | .description   |
| ファイルサイズ     |                     | .filesize      |
| ファイル著者       |                     | .author        |
| ファイルURL        |                     | .content_url   |
| ファイル作成日     |                     | .created_on    |
| コミットログ       |  issue.changesets   |                |
| 子チケット         | issue.children      |                |
| 子チケットID       |                     | .id            |
| 子チケットタイトル |                     | .subject       |
| 関連するチケット   | issue.relations     |                |
| 関連ID             |                     | .id            |
| 関連する自分のID   |                     | .issue_id      |
| 関連する相手のID   |                     | .issue_to_id   |
| 関連のタイプ       |                     | .relation_type |

上記情報を以下で表示する。

``` python
from redminelib import Redmine
from redminelib.exceptions import ResourceNotFoundError
REDMINE_URL = r'http://user/redmine' # Redmineのパス
REDMINE_KEY = 'xxxxxx' # APIアクセスキー
redmine = Redmine(REDMINE_URL, key = REDMINE_KEY)

ticket_id = 4 # チケットのID
try:
    issue = redmine.issue.get(ticket_id)

    # チケット情報
    print('チケットID: %s' % issue.id if hasattr(issue, 'id') else None)
    print('チケットURL: %s' % issue.url if hasattr(issue, 'url') else None)
    print('トラッカーID: %s' % issue.tracker.id if hasattr(issue, 'tracker') else None)
    print('トラッカー: %s' % issue.tracker.name if hasattr(issue, 'tracker') else None)
    print('題名: %s' % issue.subject if hasattr(issue, 'subject') else None)
    print('説明: %s' % issue.description if hasattr(issue, 'description') else None)
    print('ステータスID: %s' % issue.status.id if hasattr(issue, 'status') else None)
    print('ステータス: %s' % issue.status.name if hasattr(issue, 'status') else None)
    print('優先度ID: %s' % issue.priority.id if hasattr(issue, 'priority') else None)
    print('優先度: %s' % issue.priority.name if hasattr(issue, 'priority') else None)
    print('担当者ID: %s' % issue.assigned_to.id if hasattr(issue, 'assigned_to') else None)
    print('担当者名: %s' % issue.assigned_to.name if hasattr(issue, 'assigned_to') else None)
    print('親チケットID: %s' % issue.parent if hasattr(issue, 'parent') else None)
    print('開始日: %s' % issue.start_date if hasattr(issue, 'start_date') else None)
    print('期日: %s' % issue.due_date if hasattr(issue, 'due_date') else None)
    print('予定工数: %s' % issue.estimated_hours if hasattr(issue, 'estimated_hours') else None)
    print('プロジェクトID: %s' % issue.project.id if hasattr(issue, 'project') else None)
    print('プロジェクト: %s' % issue.project.name if hasattr(issue, 'project') else None)
    print('作成者ID: %s' % issue.author.id if hasattr(issue, 'author') else None)
    print('作成者名: %s' % issue.author.name if hasattr(issue, 'author') else None)
    print('進捗率: %s' % issue.done_ratio if hasattr(issue, 'done_ratio') else None)
    print('作成日: %s' % issue.created_on if hasattr(issue, 'created_on') else None)
    print('更新日: %s' % issue.updated_on if hasattr(issue, 'updated_on') else None)
    print('作業時間: %s' % issue.spent_hours if hasattr(issue, 'spent_hours') else None)

    # 情報個別詳細
    print('ウォッチャー') # ウォッチャー
    for detail in issue.watchers:
        print('    ウォッチャーID: %s' % detail.id)
        print('    ウォッチャー名: %s' % detail.name)
    print('カスタム領域') # カスタム領域
    for detail in issue.custom_fields:
        print('    カスタムID: %s' % detail.id)
        print('    カスタム名: %s' % detail.name)
        print('    カスタム値: %s' % detail.value)
    print('送付ファイル') # 送付ファイル
    for detail in issue.attachments:
        print('    ファイルID: %s' % detail.id)
        print('    ファイル名: %s' % detail.filename)
        print('    ファイル説明: %s' % detail.description)
        print('    ファイルサイズ: %s' % detail.filesize)
        print('    ファイル著者: %s' % detail.author)
        print('    ファイルURL: %s' % detail.content_url)
        print('    ファイル作成日: %s' % detail.created_on)
    print('コミットログ') # コミットログ
    for detail in issue.changesets:
        print('    %s' % detail)
    print('子チケット') # 子チケット
    for detail in issue.children:
        print('    子チケットID: %s' % detail.id)
        print('    子チケットタイトル: %s' % detail.subject)
    print('関連するチケット') # 関連するチケット
    for detail in issue.relations:
        print('    関連ID: %s' % detail.id)
        print('    関連する自分のID: %s' % detail.issue_id)
        print('    関連する相手のID: %s' % detail.issue_to_id)
        print('    関連のタイプ: %s' % detail.relation_type)
except(ResourceNotFoundError):
    print('Ticket ID %s not found' % ticket_id)
```

# 参考

https://qiita.com/mima_ita/items/1a939db423d8ee295c85
https://python-redmine.com/resources/issue.html