Files
script_dev/TicketSend.py

215 lines
7.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
OpenProjectの条件付きチケットを取得して、TeamsへAdaptive Cardで自動投稿します。
必要環境: pip install requests
"""
import base64
import datetime
import json
import os
import sys
import urllib.parse
import requests
from requests.auth import HTTPBasicAuth
# ====== 設定config.ini / 環境変数 両対応) ======
import os, configparser
CONFIG_FILE = "op_config.ini"
config = configparser.ConfigParser()
read_ok = config.read(CONFIG_FILE)
# まず config.ini を試し、無ければ環境変数にフォールバック
OP_BASE_URL = None
API_KEY = None
PROJECT_IDENTIFIER = None
API_BASE = None
if read_ok and "openproject" in config:
section = config["openproject"]
OP_BASE_URL = section.get("base_url", "").strip().rstrip("/")
API_KEY = section.get("api_key", "").strip()
PROJECT_IDENTIFIER = section.get("project_identifier", "").strip()
# 環境変数OP_BASE_URL / OP_API_KEY / OP_PROJECT_IDENTIFIERがあれば上書き
OP_BASE_URL = os.getenv("OP_BASE_URL", OP_BASE_URL or "").strip().rstrip("/")
API_KEY = os.getenv("OP_API_KEY", API_KEY or "").strip()
PROJECT_IDENTIFIER = os.getenv("OP_PROJECT_IDENTIFIER", PROJECT_IDENTIFIER or "rmsisutemukai-fa").strip()
API_BASE = f"{OP_BASE_URL}/api/v3"
if not OP_BASE_URL or not API_KEY:
raise RuntimeError(
"API接続設定が不足しています。config.ini または環境変数OP_BASE_URL / OP_API_KEYを設定してください。"
)
OUTPUT_PREFIX = "openproject_hours"
TIMEOUT = 15
# ====== セッションBasic + APIキー ======
session = requests.Session()
session.auth = HTTPBasicAuth("apikey", API_KEY)
session.headers.update({
"Accept": "application/hal+json",
"Content-Type": "application/json"
})
# TeamsワークフローのWebhook URL作成したフローの「Webhookを受け取る」トリガーURL
TEAMS_WORKFLOW_WEBHOOK_URL = os.getenv("TEAMS_WEBHOOK_URL", "https://defaulta61f1847023b4699907f786bfcaee1.b2.environment.api.powerplatform.com:443/powerautomate/automations/direct/workflows/6c6e0154d3064b9384c064c1c83a43c4/triggers/manual/paths/invoke?api-version=1")
# ======= ここまで設定 =======
def op_headers_basic(api_key: str):
# OpenProject API v3のBasic認証ユーザー名にAPIキー、パスワードはダミーXなど
# Authorization: Basic base64("<api_key>:X")
token = base64.b64encode(f"{api_key}:X".encode("utf-8")).decode("ascii")
return {
"Authorization": f"Basic {token}",
"Accept": "application/json"
}
def build_filters_for_today(assignee_ids=None):
"""本日を含む期間startDate <= today AND dueDate >= today担当者条件のfiltersを構築"""
today = datetime.date.today().strftime("%Y-%m-%d")
filters = [
{"startDate": {"operator": "<=d", "values": [today]}},
{"dueDate": {"operator": ">=d", "values": [today]}},
]
"""
filters = [
{ "date": { "operator": "<>d", "values": [today, today] } }
]
"""
#if assignee_ids:
#filters.append({"assignee": {"operator": "=", "values": assignee_ids}})
#filters.append({"assignee": {"operator": "<>", "values": ""}})
# もし「担当者あり(未割当除外)」演算子が環境にあるなら queries/form で確認して置き換え可
return filters
def fetch_work_packages(base_url, api_key, project_identifier=None, assignee_ids=None, page_size=200):
"""条件に合致するWork packagesを取得必要に応じてページング"""
filters = build_filters_for_today(assignee_ids=assignee_ids)
print("filters:", filters)
# filters_q = urllib.parse.quote(json.dumps(filters))
if project_identifier:
url = f"{base_url}/api/v3/projects/{project_identifier}/work_packages"
else:
url = f"{base_url}/api/v3/work_packages"
params = {
"filters": json.dumps(filters), # URLエンコードはrequestsに任せる
"pageSize": 200
}
all_items = []
while url:
resp = requests.get(url, params=params, auth=(API_KEY, "X"), timeout=30)
try:
resp.raise_for_status()
except requests.HTTPError as e:
print("Status:", resp.status_code)
print("URL :", resp.url)
print("Body :", resp.text) # エラー本文の詳細を確認
raise
data = resp.json()
elements = data.get("_embedded", {}).get("elements", [])
all_items.extend(elements)
# 次ページ_links.next.hrefがあれば辿る
next_link = data.get("_links", {}).get("next", {})
url = next_link.get("href")
if url and not url.startswith("http"):
url = base_url + url
return all_items
def to_adaptive_card(items):
"""Work packagesをAdaptive CardのFactSetに整形"""
today = datetime.date.today().strftime("%Y-%m-%d")
facts = []
for wp in items:
_id = wp.get("id")
subj = wp.get("subject") or "(題名なし)"
due = wp.get("dueDate") or "-"
assg = (wp.get("_embedded", {})
.get("assignee", {})
.get("name")) or "(未割当)"
# 各WPの詳細URLselfリンク
href = (wp.get("_links", {})
.get("self", {})
.get("href"))
if href and not href.startswith("http"):
# APIのselfは相対のことがあるためベースURL付与
href = OPENPROJECT_BASE_URL + href
value = f"{subj} / 期日: {due} / 担当: {assg}"
if href:
value += f" / 詳細: {href}"
facts.append({"title": f"WP #{_id}", "value": value})
# カード本文
card = {
"type": "message",
"attachments": [{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": {
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{"type": "TextBlock", "size": "Large", "weight": "Bolder",
"text": "本日を含む担当割当チケット一覧"},
{"type": "TextBlock", "isSubtle": True, "spacing": "Small",
"text": today},
{"type": "FactSet", "facts": facts or [{"title": "該当なし", "value": "対象チケットはありません"}]}
]
}
}]
}
return card
def post_to_teams_workflow(webhook_url, adaptive_card):
"""TeamsワークフローのWebhookへAdaptive CardをPOST"""
headers = {"Content-Type": "application/json"}
resp = requests.post(webhook_url, headers=headers, data=json.dumps(adaptive_card), timeout=30)
resp.raise_for_status()
return resp.status_code
def main():
if not (OP_BASE_URL and OP_BASE_URL and PROJECT_IDENTIFIER):
print("環境変数 OP_BASE_URL / OP_API_KEY / TEAMS_WEBHOOK_URL を設定してください。", file=sys.stderr)
sys.exit(1)
# 1) OpenProjectから条件付きWP取得
items = fetch_work_packages(
base_url=OP_BASE_URL,
api_key=API_KEY,
project_identifier=PROJECT_IDENTIFIER,
assignee_ids=PROJECT_IDENTIFIER
)
# 2) Adaptive Cardに整形
card = to_adaptive_card(items)
# 3) Teamsへ投稿
#status = post_to_teams_workflow(TEAMS_WORKFLOW_WEBHOOK_URL, card)
print(f"Teams投稿完了HTTP {status}")
if __name__ == "__main__":
main()