"""
Markdown response formatters.
Provides builders for constructing consistent Markdown responses.
"""
from __future__ import annotations
from typing import Any, Sequence
class MarkdownBuilder:
"""
Builder for constructing Markdown responses.
Example:
>>> builder = MarkdownBuilder("Projects")
>>> builder.add_table(["ID", "Name"], [[1, "Project A"], [2, "Project B"]])
>>> builder.add_pagination(2, 50, 0)
>>> print(builder.build())
"""
def __init__(self, title: str | None = None):
"""
Initialize the builder.
Args:
title: Optional title for the document (adds # heading)
"""
self._lines: list[str] = []
if title:
self._lines.append(f"# {title}\n")
def add_heading(self, text: str, level: int = 2) -> "MarkdownBuilder":
"""
Add a heading.
Args:
text: Heading text
level: Heading level (1-6)
Returns:
Self for chaining
"""
prefix = "#" * min(max(level, 1), 6)
self._lines.append(f"\n{prefix} {text}\n")
return self
def add_text(self, text: str) -> "MarkdownBuilder":
"""
Add plain text.
Args:
text: Text to add
Returns:
Self for chaining
"""
self._lines.append(text)
return self
def add_line(self) -> "MarkdownBuilder":
"""
Add an empty line.
Returns:
Self for chaining
"""
self._lines.append("")
return self
def add_separator(self) -> "MarkdownBuilder":
"""
Add a horizontal separator.
Returns:
Self for chaining
"""
self._lines.append("\n---\n")
return self
def add_table(
self,
headers: Sequence[str],
rows: Sequence[Sequence[Any]],
alignments: Sequence[str] | None = None,
) -> "MarkdownBuilder":
"""
Add a table.
Args:
headers: Column headers
rows: Table rows (list of lists)
alignments: Column alignments ("left", "center", "right")
Returns:
Self for chaining
"""
if not headers:
return self
# Header row
header_row = "| " + " | ".join(str(h) for h in headers) + " |"
self._lines.append(header_row)
# Separator row with alignments
if alignments:
separators = []
for i, align in enumerate(alignments):
if i >= len(headers):
break
if align == "right":
separators.append("---:")
elif align == "center":
separators.append(":---:")
else:
separators.append("---")
# Pad with default alignment if needed
while len(separators) < len(headers):
separators.append("---")
else:
separators = ["---"] * len(headers)
self._lines.append("| " + " | ".join(separators) + " |")
# Data rows
for row in rows:
cells = [str(cell) if cell is not None else "" for cell in row]
# Pad row if needed
while len(cells) < len(headers):
cells.append("")
self._lines.append("| " + " | ".join(cells) + " |")
return self
def add_list(
self,
items: Sequence[str],
ordered: bool = False,
) -> "MarkdownBuilder":
"""
Add a list.
Args:
items: List items
ordered: Whether to use numbered list
Returns:
Self for chaining
"""
for i, item in enumerate(items, 1):
prefix = f"{i}." if ordered else "-"
self._lines.append(f"{prefix} {item}")
return self
def add_key_value(
self,
data: dict[str, Any],
bullet: bool = True,
) -> "MarkdownBuilder":
"""
Add key-value pairs.
Args:
data: Dictionary of key-value pairs
bullet: Whether to use bullet points
Returns:
Self for chaining
"""
for key, value in data.items():
if value is not None:
prefix = "- " if bullet else ""
self._lines.append(f"{prefix}**{key}**: {value}")
return self
def add_code_block(
self,
code: str,
language: str = "",
) -> "MarkdownBuilder":
"""
Add a code block.
Args:
code: Code content
language: Language for syntax highlighting
Returns:
Self for chaining
"""
self._lines.append(f"```{language}")
self._lines.append(code)
self._lines.append("```")
return self
def add_pagination(
self,
count: int,
limit: int,
offset: int,
total: int | None = None,
) -> "MarkdownBuilder":
"""
Add pagination information.
Args:
count: Number of items in current page
limit: Page size limit
offset: Current offset
total: Total count (if known)
Returns:
Self for chaining
"""
self._lines.append("")
if total is not None:
self._lines.append(f"*Showing {count} of {total} result(s)*")
else:
self._lines.append(f"*{count} result(s) displayed*")
if count >= limit:
next_offset = offset + limit
self._lines.append(f"*Use offset={next_offset} for more results*")
return self
def add_record_item(
self,
record_id: int,
name: str,
fields: dict[str, Any] | None = None,
) -> "MarkdownBuilder":
"""
Add a formatted record item.
Args:
record_id: Record ID
name: Record name/title
fields: Additional fields to display
Returns:
Self for chaining
"""
self._lines.append(f"\n- **ID {record_id}**: {name}")
if fields:
for key, value in fields.items():
if value is not None:
self._lines.append(f" - {key}: {value}")
return self
def build(self) -> str:
"""
Build the final Markdown string.
Returns:
Complete Markdown document
"""
return "\n".join(self._lines)
# =============================================================================
# Convenience Functions
# =============================================================================
def format_many2one(value: Any) -> str | None:
"""
Extract display name from Many2one field.
Args:
value: Many2one field value (tuple/list or False)
Returns:
Display name or None
"""
if isinstance(value, (list, tuple)) and len(value) >= 2:
return str(value[1])
return None
def format_money(amount: float | int, currency: str = "EUR") -> str:
"""
Format amount as money.
Args:
amount: Amount value
currency: Currency code
Returns:
Formatted money string
"""
return f"{amount:,.2f} {currency}"
def format_hours(hours: float) -> str:
"""
Format hours value.
Args:
hours: Hours value
Returns:
Formatted hours string
"""
return f"{hours:.1f}h"
def format_percentage(value: float, decimals: int = 1) -> str:
"""
Format value as percentage.
Args:
value: Value (0-100)
decimals: Number of decimal places
Returns:
Formatted percentage string
"""
return f"{value:.{decimals}f}%"