REST APIでRedmineアクセス(2)
2025/05/04
今回は具体的な操作を
チケット(Issue): offset設定で全チケットのリスト取得
  • 前回レポートのREST APIチケット取得リストで、最後のIDが[06]であることにお気付きだったでしょうか。
  • INF: HTTP request is successful for [http://localhost:3000/issues.json]
    INF: id=30,     subject=Test 30,        status=新規
    ...中略...
    INF: id=6,      subject=Test 06,        status=新規
    INF: normally finished
  • これはデフォルトパラメータ設定(limit=25, offset=0)だと最後の25のみ取得するからです。limitを超えるチケット情報を取得するには、offsetパラーメータ設定が必須となります。

  • HTTPレスポンスのjsonデータに全チケット数データがあるか覗いてみると total_count であることがわかります。
  • {
        "issues": [
        ...
        ],
        "total_count": 29,
        "offset": 0,
        "limit": 25
    }
  • なので、total_countを超えるまでoffsetをlimit(=25)毎に増やせば(*1)全てのチケットの情報を取得できます。下記はコード(Python)の抜粋です。全体コードはこちら
  •   # getメソッド引数にパラメータ設定を追加する
        sUri     = 'http://localhost:3000/issues.json'
        dHeaders = {'X-Redmine-API-Key': '8dc1f63aうんぬんかんぬん'}
        dParams  = {'offset': 0, 'limit': 25}
        ...略...
        
        iTotalCount = dParams['limit'] # 全チケット(Issue)数初期値はlimitとする
        dIssues = None                 # 戻り値初期設定
        
      # iTotalCountがoffsetより大きい場合HTTPリクエストを繰り返す
        while iTotalCount > dParams['offset']:
          # HTTPリクエスト
            jIssues = requests.get(sUri, headers=dHeaders, params=dParams)
          # 取得データを辞書に変更して追加
            dIssuesCur = jIssues.json()
            dIssues['issues'] += dIssuesCur['issues'] # (注)None時の記述を略しています
          # 全チケット数を取得
            iTotalCount = dIssues['total_count'] # 1回目レスポンス結果で正確なチケット数に
          # offsetのインクリメント
            dParams['offset'] += dParams['limit']
    
    ...
    INF: id=[6],    subject=[Test 06],      status=[新規]
    INF: id=[4],    subject=[Test 04],      status=[新規]
    INF: id=[3],    subject=[Test 03],      status=[新規]
    INF: id=[2],    subject=[Test 02],      status=[新規]
    INF: id=[1],    subject=[Test 01],      status=[新規]
    INF: normally finished
    
  • よく見ると、06 の次が 04 ... 05が抜けていますね。実は終了/closedチケットなので表示されていません。

チケット(Issue): status_id設定で全ステータスのリスト取得
  • 終了/closedチケットも含めた全てをリスト取得したい場合は、パラメータ status_id に '*' を設定します。
  •     dParams  = {'offset': 0, 'limit': 25, 'status_id': '*'}
        ...略...
        jIssues = requests.get(sUri, headers=dHeaders, params=dParams)
    
    INF: id=[6],    subject=[Test 06],      status=[新規]
    INF: id=[5],    subject=[Test 05],      status=[終了]
    INF: id=[4],    subject=[Test 04],      status=[新規]
    INF: id=[3],    subject=[Test 03],      status=[新規]
    INF: id=[2],    subject=[Test 02],      status=[新規]
    INF: id=[1],    subject=[Test 01],      status=[新規]
    INF: normally finished
    

チケット(Issue): 指定ID/番号のチケット情報取得
  • 例えばチケット30番の情報を取得するとしましょう。

  • (※)クリックで拡大

  • これはリクエストURIを '/issues/番号.json' とするだけです。#note-*(コメント)の情報も取得するには、includeパラメータにjournalsを加えます。

  • journalsの辞書配列データを取得後、notesキーの値を参照します。
  • import requests
    import json
    
    # Issue 30 の情報をjsonのURLからjournals付きで取得
    strUrl     = 'http://localhost:3000/issues/30.json'
    dicHeaders = {'X-Redmine-API-Key': '8dc1f63aうんぬん'}
    dicParams  = {'include': 'journals'}
    
    # レスポンス取得(コンテントはjson)
    resIssue = requests.get(strUrl, headers=dicHeaders, params=dicParams)
    
    # ステータスコード表示
    print(f"request_status=[{resIssue.status_code}]")
    
    # レスポンスコンテントのjsonデータを辞書として取り出し
    # ※json()はrequestsのメソッド
    dicIssue = resIssue.json();
    
    # 整形json作成して取得データ全体dump表示(これでkeyとvalueの関係を把握する)
    jsoIssue = json.dumps(dicIssue, indent=2, ensure_ascii=False)
    print("=========================================================================")
    print(jsoIssue)
    
    # issue番号とコメント部を表示
    print("=========================================================================")
    print(f"issue_id=[{dicIssue['issue']['id']}]")
    for journal in dicIssue['issue']['journals']:
        print(f"  journal_id=[{journal['id']}], notes=[{journal['notes']}]")
    
    >test_d.py
    request_status=[200]
    =========================================================================
    {
      "issue": {
        "id": 30,
        ...
        "subject": "Test 30",
        "description": "[Test 30]の説明",
        ...
        "journals": [
          {
            "id": 5,
            ...
            "notes": "ここにまとめ記述予定",
            ...
          }
        ]
      }
    }
    =========================================================================
    issue_id=[30]
      journal_id=[5], notes=[ここにまとめ記述予定]
    
  • さて、このjournalのIDですが、チケットのコメント番号(#note-*)とは関係ありません#note-* のコメントを編集する場合 journal の ID を経由します。

チケット(Issue): コメント(journal)の編集
  • このセクションでは、既存コメントの編集を説明します。編集ではなく #note-* コメントを追加する場合は次のセクションを参照ください。

  • 先ほど見たようにチケットのコメントは、チケットに紐付いているだけの独立したjournalデータです。includeにjournalsを指定しないと紐付き情報が得られないことが、それを示しています。

  • 先の例では、チケット30の最初のコメント(#note-1)の journals id は '5'でした。このコメントを修正するには、'http://localhost:3000/journals/5.json'' にコメントのjsonデータをputします。
  • import requests
    import json
    
    # requests url(journal 5)
    strUrl = 'http://localhost:3000/journals/5.json'
    
    # requests header
    dicHeaders = {
        'X-Redmine-API-Key': '8dc1f63aうんぬん',
        'Content-Type': 'application/json'
    }
    
    # 変更データ(dic)
    dicJournal = {
        'journal': {
            'notes': "REST APIから変更したコメント"
        }
    }
    
    # dic->json変換
    jsoJournal = json.dumps(dicJournal)
    
    # 変更データをput(journal 5)
    resJournal = requests.put(strUrl, headers=dicHeaders, data=jsoJournal)
    
    # ステータス
    print(f"request_status=[{resJournal.status_code}]")
    
  • 成功すると status_code 204が戻ります。Redmine側を見るとコメント変更されていることがわかります。


  • 2025/06/08現在ではCopilot等のAIに聞いても上記が可能だという回答は得られません。しつこくWebを探して下記の記述を見つけ、実験したところうまくいきました。

チケット(Issue): 指定ID/番号にコメント追加
  • 既存チケットへのコメント追加は、対象チケットIDのURI '/issues/番号.json' にデータを put します。追加するデータは、putメソッドの引数datajsonデータを指定します。jsonデータの例は下記(*2)です。
  • {
        'issue': {
            'notes': 'REST APIで追加したコメント'
        }
    }
    
  • チケット30へコメントを追加するコード例は下記です。データ変更/追加時はデータ形式(json)を明示的に指定します。データはdictで作ってからdumps()メソッドでjson変換する方が楽かと思います。
  • import requests
    import json
    
    # requsts url(issue 30)
    strUrl = 'http://localhost:3000/issues/30.json'
    
    # requests header
    dicHeaders = {
        'X-Redmine-API-Key': '8dc1f63aうんぬん',
        'Content-Type': 'application/json'
    }
    
    # 追加コメント辞書データ
    dicData = {
        'issue':{
            'notes': 'REST APIで追加したコメント'
        }
    }
    
    # jsonへ変換(ASCII処理無し)
    jsoData = json.dumps(dicData)
    
    # put request
    resIssue = requests.put(strUrl, headers=dicHeaders, data=jsoData)
    
    print(f"return status_code = {resIssue.status_code}")
    
  • 成功すると status_code 204が戻ります。Redmine側を見るとコメント追加されていることがわかります。

  •   追加されたコメント
        
    (※)クリックで拡大

チケット(Issue): チケット(Issue)の追加
  • '/issues.json' URIにjsonデータを post します(putではありません)。jsonデータの例は下記です。
  • {
        'issue':{
            'project_id': 1,
            'subject': 'Test 31'
        }
    }
    
  • project_idキーに指定する値は、Projects を参照します。今回のコード例では project_id に 1(=Sandbox) を指定しています。subjectはチケットのタイトルです。他の値は Creating an issue を参照してください。
  • import requests
    import json
    
    strUri = 'http://localhost:3000/issues.json' # issuesのURI
    
    dicHeaders = {
        'X-Redmine-API-Key': '8dc1f63aうんぬん',
        'Content-Type': 'application/json'
    }
    
    # チケットdict
    dicData = {'issue':
      {'project_id': 1, 'subject': 'Test 31'}
    }
    
    # jsonへ変換
    jsoData = json.dumps(dicData)
    
    # REST APIでpost
    objRet = requests.post(strUri, headers=dicHeaders, data=jsoData)
    
    print(f"return status_code = {objRet.status_code}")
    
  • 成功すると status_code 201が戻ります。「チケット31」が指定題名[Test 31]で追加されていますね。

チケット(Issue): チケット(Issue)の削除
  • 最後はチケットの削除ですが、チケットを指すURI /issues/番号.jsondelete を送ります。下記は先程追加したチケット31を削除する例です。
  • import requests
    
    sUri = 'http://localhost:3000/issues/31.json' # チケット31のURI
    
    # ヘッダーdict
    dHeaders = {'X-Redmine-API-Key': '8dc1f63aうんぬんかんぬん'}
    
    # REST APIでdelete
    objRet = requests.delete(sUri, headers=dHeaders)
    
    print(f"return status_code = {objRet.status_code}")
    
  • 成功すると status_code 204が戻ります。「チケット31」が無くなりました。

次回は
  • 次回はWikiの編集と、添付ファイルの処理をレポートする予定です。
Notes
  1. limitは最大値100まで設定できます。今回は全チケット数が30のためlimitを変えていません。
  2. journalsキーを使わないのが、直感に逆らいます。
2025-05-04: 初版
2025-06-08: コードの可読性向上,コメントの修正を追加
Copyright(C) 2025 Altmo
本HPについて