Unclear reason for the backup
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Markdown rendering + pager helpers for cached Redmine tickets."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _nested_name(issue: dict[str, Any], key: str) -> str:
|
||||
value = issue.get(key)
|
||||
if isinstance(value, dict):
|
||||
name = value.get("name")
|
||||
return str(name) if name is not None else ""
|
||||
return "" if value is None else str(value)
|
||||
|
||||
|
||||
def _helpdesk(issue: dict[str, Any]) -> dict[str, Any]:
|
||||
hd = issue.get("helpdesk_ticket") or {}
|
||||
return hd if isinstance(hd, dict) else {}
|
||||
|
||||
|
||||
def _contact(issue: dict[str, Any]) -> dict[str, Any]:
|
||||
contact = _helpdesk(issue).get("contact") or {}
|
||||
return contact if isinstance(contact, dict) else {}
|
||||
|
||||
|
||||
def render_ticket_markdown(issue: dict[str, Any]) -> str:
|
||||
issue_id = issue.get("id")
|
||||
subject = str(issue.get("subject") or "").strip()
|
||||
status = _nested_name(issue, "status")
|
||||
project = _nested_name(issue, "project")
|
||||
category = _nested_name(issue, "category")
|
||||
assigned = _nested_name(issue, "assigned_to") or "(unassigned)"
|
||||
author = _nested_name(issue, "author")
|
||||
priority = _nested_name(issue, "priority")
|
||||
created = issue.get("created_on") or ""
|
||||
updated = issue.get("updated_on") or ""
|
||||
hd = _helpdesk(issue)
|
||||
contact = _contact(issue)
|
||||
from_addr = hd.get("from_address") or contact.get("email") or ""
|
||||
contact_id = contact.get("id")
|
||||
company = contact.get("company") or ""
|
||||
contact_name = contact.get("name") or ""
|
||||
description = str(issue.get("description") or "").strip()
|
||||
redmine_url = f"https://rm.ldrprep.com/issues/{issue_id}" if issue_id else ""
|
||||
|
||||
lines = [
|
||||
f"# #{issue_id} — {subject}",
|
||||
"",
|
||||
"## Meta",
|
||||
"",
|
||||
f"- **Project:** {project}",
|
||||
f"- **Status:** {status}",
|
||||
f"- **Priority:** {priority}",
|
||||
f"- **Category:** {category or '(none)'}",
|
||||
f"- **Assignee:** {assigned}",
|
||||
f"- **Author:** {author}",
|
||||
f"- **Created:** {created}",
|
||||
f"- **Updated:** {updated}",
|
||||
f"- **Redmine:** {redmine_url}",
|
||||
"",
|
||||
"## Helpdesk / contact",
|
||||
"",
|
||||
f"- **From:** {from_addr}",
|
||||
f"- **Contact ID:** {contact_id if contact_id is not None else '(none)'}",
|
||||
f"- **Contact name:** {contact_name or '(none)'}",
|
||||
f"- **Company:** {company or '(none)'}",
|
||||
"",
|
||||
"## Detected entities (heuristic)",
|
||||
"",
|
||||
]
|
||||
try:
|
||||
from redmine_ticket_entities import extract_entities_from_issue, format_entities_line
|
||||
|
||||
ents = extract_entities_from_issue(issue)
|
||||
if ents:
|
||||
for e in ents:
|
||||
conf = e.get("confidence") or "high"
|
||||
lines.append(f"- **{e.get('type')}:** `{e.get('value')}` ({conf})")
|
||||
else:
|
||||
lines.append("_(none)_")
|
||||
except Exception:
|
||||
lines.append("_(entity extractor unavailable)_")
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## Description",
|
||||
"",
|
||||
description or "_(empty)_",
|
||||
"",
|
||||
"## Journals",
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
journals = issue.get("journals") or []
|
||||
if not journals:
|
||||
lines.append("_(no journals)_")
|
||||
else:
|
||||
for journal in journals:
|
||||
if not isinstance(journal, dict):
|
||||
continue
|
||||
jid = journal.get("id")
|
||||
jcreated = journal.get("created_on") or ""
|
||||
user = journal.get("user") or {}
|
||||
uname = user.get("name") if isinstance(user, dict) else ""
|
||||
notes = str(journal.get("notes") or "").strip()
|
||||
details = journal.get("details") or []
|
||||
lines.append(f"### Journal #{jid} — {jcreated} — {uname}")
|
||||
lines.append("")
|
||||
if details:
|
||||
for detail in details:
|
||||
if not isinstance(detail, dict):
|
||||
continue
|
||||
lines.append(
|
||||
f"- change: {detail.get('property')}/{detail.get('name')}: "
|
||||
f"{detail.get('old_value')!r} → {detail.get('new_value')!r}"
|
||||
)
|
||||
lines.append("")
|
||||
if notes:
|
||||
lines.append(notes)
|
||||
else:
|
||||
lines.append("_(no notes)_")
|
||||
lines.append("")
|
||||
|
||||
lines.append("---")
|
||||
lines.append(f"_Rendered from cache; source updated_on={updated}_")
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def markdown_cache_path(cache_dir: Path, project: str, issue_id: int | str) -> Path:
|
||||
return Path(cache_dir) / project / "markdown" / f"{issue_id}.md"
|
||||
|
||||
|
||||
def write_ticket_markdown(
|
||||
issue: dict[str, Any],
|
||||
cache_dir: Path,
|
||||
project: str,
|
||||
*,
|
||||
force: bool = False,
|
||||
) -> Path:
|
||||
"""Write markdown cache if missing or ticket updated_on changed (via sidecar stamp)."""
|
||||
issue_id = issue["id"]
|
||||
path = markdown_cache_path(cache_dir, project, issue_id)
|
||||
stamp_path = path.with_suffix(".md.updated_on")
|
||||
updated_on = str(issue.get("updated_on") or "")
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if (
|
||||
not force
|
||||
and path.is_file()
|
||||
and stamp_path.is_file()
|
||||
and stamp_path.read_text(encoding="utf-8").strip() == updated_on
|
||||
):
|
||||
return path
|
||||
path.write_text(render_ticket_markdown(issue), encoding="utf-8")
|
||||
stamp_path.write_text(updated_on + "\n", encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
def resolve_pager(preferred: str = "auto") -> list[str]:
|
||||
"""Return argv prefix to display markdown (pager program + flags)."""
|
||||
preferred = (preferred or "auto").lower()
|
||||
if preferred == "cat":
|
||||
return ["cat"]
|
||||
if preferred in {"glow", "bat", "batcat"}:
|
||||
path = shutil.which(preferred)
|
||||
if not path:
|
||||
raise FileNotFoundError(f"Pager {preferred!r} not found on PATH")
|
||||
if preferred == "glow":
|
||||
return [path]
|
||||
return [path, "--language", "markdown", "--style", "plain"]
|
||||
# auto
|
||||
for name, args in (
|
||||
("glow", []),
|
||||
("bat", ["--language", "markdown", "--style", "plain"]),
|
||||
("batcat", ["--language", "markdown", "--style", "plain"]),
|
||||
):
|
||||
path = shutil.which(name)
|
||||
if path:
|
||||
return [path, *args]
|
||||
return ["cat"]
|
||||
|
||||
|
||||
def display_markdown_file(path: Path, pager: str = "auto") -> int:
|
||||
cmd = resolve_pager(pager) + [str(path)]
|
||||
# Prefer streaming to user terminal
|
||||
env = os.environ.copy()
|
||||
try:
|
||||
completed = subprocess.run(cmd, check=False, env=env)
|
||||
return int(completed.returncode)
|
||||
except FileNotFoundError:
|
||||
print(path.read_text(encoding="utf-8"))
|
||||
return 0
|
||||
Reference in New Issue
Block a user