""" Text formatters for LLM/human readable output. """ from typing import TYPE_CHECKING if TYPE_CHECKING: from .ticket import Attachment, Ticket, TicketDetail, TicketList def _fmt_size(size: int) -> str: """Format bytes to human readable.""" for unit in ["B", "KB", "MB", "GB"]: if size < 1024: return f"{size:.1f}{unit}" if unit != "B" else f"{size}{unit}" size /= 1024 return f"{size:.1f}TB" def _fmt_dt(dt) -> str: if not dt: return "-" return dt.strftime("%Y-%m-%d %H:%M") def format_ticket(t: "Ticket") -> str: lines = [ f"[{t.key}] {t.summary}", f" Project: {t.project} | Type: {t.issue_type} | Priority: {t.priority or '-'}", f" Status: {t.status} ({t.status_category or '-'})", f" Assignee: {t.assignee or '-'} | Reporter: {t.reporter or '-'}", f" Labels: {', '.join(t.labels) if t.labels else '-'}", f" Created: {_fmt_dt(t.created)} | Updated: {_fmt_dt(t.updated)}", f" URL: {t.url}", ] if t.description: lines.append(f" Description: {t.description}") return "\n".join(lines) def format_ticket_detail(t: "TicketDetail") -> str: lines = [ f"# {t.key}: {t.summary}", "", f"Project: {t.project}", f"Type: {t.issue_type}", f"Status: {t.status} ({t.status_category or '-'})", f"Priority: {t.priority or '-'}", f"Assignee: {t.assignee or '-'}", f"Reporter: {t.reporter or '-'}", f"Labels: {', '.join(t.labels) if t.labels else '-'}", f"Created: {_fmt_dt(t.created)}", f"Updated: {_fmt_dt(t.updated)}", f"Parent: {t.parent_key or '-'}", f"Subtasks: {', '.join(t.subtasks) if t.subtasks else '-'}", f"Linked issues: {', '.join(t.linked_issues) if t.linked_issues else '-'}", f"URL: {t.url}", "", "## Description", t.description or "(no description)", "", ] lines.append(f"## Comments ({len(t.comments)})") if t.comments: for c in t.comments: lines.append(f"### {c.get('author', 'Unknown')} ({c.get('created', '')[:16] if c.get('created') else '-'})") lines.append(c.get("body", "")) lines.append("") else: lines.append("(no comments)") lines.append("") lines.append(f"## Attachments ({len(t.attachments)})") if t.attachments: for a in t.attachments: has_content = "[downloaded]" if a.content_base64 else "" lines.append(f"- {a.filename} ({_fmt_size(a.size)}, {a.mimetype}) {has_content}") else: lines.append("(no attachments)") return "\n".join(lines) def format_ticket_with_children(parent: "TicketDetail", children: list) -> str: """Format a ticket with its children (subtasks/stories).""" lines = [ f"# {parent.key}: {parent.summary}", "", f"Project: {parent.project}", f"Type: {parent.issue_type}", f"Status: {parent.status} ({parent.status_category or '-'})", f"Priority: {parent.priority or '-'}", f"Assignee: {parent.assignee or '-'}", f"Reporter: {parent.reporter or '-'}", f"Labels: {', '.join(parent.labels) if parent.labels else '-'}", f"Created: {_fmt_dt(parent.created)}", f"Updated: {_fmt_dt(parent.updated)}", f"URL: {parent.url}", "", "## Description", parent.description or "(no description)", "", ] # Add children section if children: child_type = "Sub-tasks" if parent.issue_type in ("Story", "Task") else "Stories" lines.append(f"## {child_type} ({len(children)})") lines.append("=" * 60) lines.append("") for child in children: lines.append(f"[{child.key}] {child.summary}") lines.append(f" Type: {child.issue_type} | Status: {child.status} | Priority: {child.priority or '-'}") lines.append(f" Assignee: {child.assignee or '-'}") lines.append(f" URL: {child.url}") lines.append("") lines.append("-" * 60) lines.append("") lines.append(f"## Comments ({len(parent.comments)})") if parent.comments: for c in parent.comments: lines.append(f"### {c.get('author', 'Unknown')} ({c.get('created', '')[:16] if c.get('created') else '-'})") lines.append(c.get("body", "")) lines.append("") else: lines.append("(no comments)") lines.append("") lines.append(f"## Attachments ({len(parent.attachments)})") if parent.attachments: for a in parent.attachments: has_content = "[downloaded]" if a.content_base64 else "" lines.append(f"- {a.filename} ({_fmt_size(a.size)}, {a.mimetype}) {has_content}") else: lines.append("(no attachments)") return "\n".join(lines) def format_ticket_list(tl: "TicketList") -> str: # Sort for text output: stories with subtasks, then bugs stories = [] bugs = [] subtasks = [] for t in tl.tickets: if t.parent_key: subtasks.append(t) elif t.issue_type in ("Story", "Epic", "Task"): stories.append(t) elif t.issue_type == "Bug": bugs.append(t) else: stories.append(t) # fallback # Build sorted list: parent stories, then their subtasks, then bugs sorted_tickets = [] for story in sorted(stories, key=lambda t: t.key): sorted_tickets.append(story) # Add subtasks for this story story_subtasks = [st for st in subtasks if st.parent_key == story.key] sorted_tickets.extend(sorted(story_subtasks, key=lambda t: t.key)) # Add bugs at the end sorted_tickets.extend(sorted(bugs, key=lambda t: t.key)) lines = [ f"Total: {tl.total} | Page: {tl.page} | Page size: {tl.page_size}", f"Showing: {len(tl.tickets)} tickets", "=" * 60, "", ] for i, t in enumerate(sorted_tickets): lines.append(format_ticket(t)) if i < len(sorted_tickets) - 1: lines.append("") lines.append("-" * 60) lines.append("") return "\n".join(lines)