スクリプトファイルを追加

This commit is contained in:
2026-04-03 18:22:33 +09:00
commit 809d3bc3e4
14 changed files with 1956 additions and 0 deletions

214
TicketSend.py Normal file
View File

@@ -0,0 +1,214 @@
# -*- 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()