#!/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())