210 lines
5.3 KiB
Python
210 lines
5.3 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Export Feishu view issues and enrich them with selected detail fields."""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import argparse
|
||
|
|
import json
|
||
|
|
import subprocess
|
||
|
|
import sys
|
||
|
|
from datetime import datetime
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
|
||
|
|
DEFAULT_EXTRA_FIELDS = [
|
||
|
|
"缺陷标签池",
|
||
|
|
"问题数据地址",
|
||
|
|
"问题数据地址_PDCL",
|
||
|
|
"问题发生frameid",
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
def run_fp(args: list[str]) -> dict:
|
||
|
|
result = subprocess.run(
|
||
|
|
["fp", *args],
|
||
|
|
check=False,
|
||
|
|
capture_output=True,
|
||
|
|
text=True,
|
||
|
|
encoding="utf-8",
|
||
|
|
)
|
||
|
|
if result.returncode != 0:
|
||
|
|
stderr = result.stderr.strip()
|
||
|
|
stdout = result.stdout.strip()
|
||
|
|
detail = stderr or stdout or "unknown error"
|
||
|
|
raise RuntimeError(f"fp command failed: {' '.join(args)}\n{detail}")
|
||
|
|
try:
|
||
|
|
return json.loads(result.stdout)
|
||
|
|
except json.JSONDecodeError as exc:
|
||
|
|
raise RuntimeError(
|
||
|
|
f"fp returned non-JSON output for command: {' '.join(args)}"
|
||
|
|
) from exc
|
||
|
|
|
||
|
|
|
||
|
|
def fetch_view(project_key: str, user_key: str, view_name: str, work_item_type: str) -> dict | None:
|
||
|
|
payload = run_fp(
|
||
|
|
[
|
||
|
|
"view",
|
||
|
|
"list",
|
||
|
|
"-o",
|
||
|
|
"json",
|
||
|
|
"-p",
|
||
|
|
project_key,
|
||
|
|
"-u",
|
||
|
|
user_key,
|
||
|
|
"-t",
|
||
|
|
work_item_type,
|
||
|
|
"--name",
|
||
|
|
view_name,
|
||
|
|
]
|
||
|
|
)
|
||
|
|
views = payload.get("data", [])
|
||
|
|
if not views:
|
||
|
|
return None
|
||
|
|
return views[0]
|
||
|
|
|
||
|
|
|
||
|
|
def fetch_items(project_key: str, user_key: str, view_name: str, work_item_type: str) -> dict:
|
||
|
|
return run_fp(
|
||
|
|
[
|
||
|
|
"workitem",
|
||
|
|
"list",
|
||
|
|
"-o",
|
||
|
|
"json",
|
||
|
|
"--all",
|
||
|
|
"-p",
|
||
|
|
project_key,
|
||
|
|
"-u",
|
||
|
|
user_key,
|
||
|
|
"-t",
|
||
|
|
work_item_type,
|
||
|
|
"--view",
|
||
|
|
view_name,
|
||
|
|
]
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def fetch_item_detail(project_key: str, user_key: str, item_id: int, work_item_type: str) -> dict:
|
||
|
|
payload = run_fp(
|
||
|
|
[
|
||
|
|
"workitem",
|
||
|
|
"get",
|
||
|
|
str(item_id),
|
||
|
|
"-o",
|
||
|
|
"json",
|
||
|
|
"-p",
|
||
|
|
project_key,
|
||
|
|
"-u",
|
||
|
|
user_key,
|
||
|
|
"-t",
|
||
|
|
work_item_type,
|
||
|
|
]
|
||
|
|
)
|
||
|
|
return payload["data"]
|
||
|
|
|
||
|
|
|
||
|
|
def enrich_items(
|
||
|
|
base_items: list[dict],
|
||
|
|
project_key: str,
|
||
|
|
user_key: str,
|
||
|
|
work_item_type: str,
|
||
|
|
extra_fields: list[str],
|
||
|
|
) -> list[dict]:
|
||
|
|
enriched = []
|
||
|
|
for item in base_items:
|
||
|
|
detail = fetch_item_detail(project_key, user_key, item["id"], work_item_type)
|
||
|
|
detail_fields = detail.get("fields", {})
|
||
|
|
merged = dict(item)
|
||
|
|
for field_name in extra_fields:
|
||
|
|
merged[field_name] = detail_fields.get(field_name)
|
||
|
|
enriched.append(merged)
|
||
|
|
return enriched
|
||
|
|
|
||
|
|
|
||
|
|
def build_output(
|
||
|
|
project_key: str,
|
||
|
|
user_key: str,
|
||
|
|
view_name: str,
|
||
|
|
view: dict | None,
|
||
|
|
items_payload: dict,
|
||
|
|
extra_fields: list[str],
|
||
|
|
work_item_type: str,
|
||
|
|
) -> dict:
|
||
|
|
return {
|
||
|
|
"exported_at": datetime.now().astimezone().isoformat(timespec="seconds"),
|
||
|
|
"source": {
|
||
|
|
"project_key": project_key,
|
||
|
|
"user_key": user_key,
|
||
|
|
"view_name": view_name,
|
||
|
|
"view_id": None if view is None else view.get("view_id"),
|
||
|
|
"work_item_type": work_item_type,
|
||
|
|
"included_detail_fields": extra_fields,
|
||
|
|
},
|
||
|
|
"summary": {
|
||
|
|
"success": items_payload.get("success", False),
|
||
|
|
"page": items_payload.get("page"),
|
||
|
|
"total": items_payload.get("total"),
|
||
|
|
},
|
||
|
|
"items": items_payload["data"],
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def parse_args() -> argparse.Namespace:
|
||
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
||
|
|
parser.add_argument("--project-key", required=True)
|
||
|
|
parser.add_argument("--user-key", required=True)
|
||
|
|
parser.add_argument("--view-name", required=True)
|
||
|
|
parser.add_argument(
|
||
|
|
"--output",
|
||
|
|
required=True,
|
||
|
|
type=Path,
|
||
|
|
help="Path to the exported JSON file.",
|
||
|
|
)
|
||
|
|
parser.add_argument(
|
||
|
|
"--extra-field",
|
||
|
|
action="append",
|
||
|
|
dest="extra_fields",
|
||
|
|
help="Extra detail field to include. Can be repeated.",
|
||
|
|
)
|
||
|
|
parser.add_argument(
|
||
|
|
"--work-item-type",
|
||
|
|
default="issue",
|
||
|
|
help="Feishu work item type. Defaults to issue.",
|
||
|
|
)
|
||
|
|
return parser.parse_args()
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> int:
|
||
|
|
args = parse_args()
|
||
|
|
extra_fields = args.extra_fields or list(DEFAULT_EXTRA_FIELDS)
|
||
|
|
|
||
|
|
view = fetch_view(args.project_key, args.user_key, args.view_name, args.work_item_type)
|
||
|
|
items_payload = fetch_items(args.project_key, args.user_key, args.view_name, args.work_item_type)
|
||
|
|
items_payload["data"] = enrich_items(
|
||
|
|
items_payload.get("data", []),
|
||
|
|
args.project_key,
|
||
|
|
args.user_key,
|
||
|
|
args.work_item_type,
|
||
|
|
extra_fields,
|
||
|
|
)
|
||
|
|
|
||
|
|
output = build_output(
|
||
|
|
args.project_key,
|
||
|
|
args.user_key,
|
||
|
|
args.view_name,
|
||
|
|
view,
|
||
|
|
items_payload,
|
||
|
|
extra_fields,
|
||
|
|
args.work_item_type,
|
||
|
|
)
|
||
|
|
|
||
|
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||
|
|
args.output.write_text(
|
||
|
|
json.dumps(output, ensure_ascii=False, indent=2) + "\n",
|
||
|
|
encoding="utf-8",
|
||
|
|
)
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
sys.exit(main())
|