from dataclasses import dataclass
from typing import Any, Dict, Optional, Union, Iterable, Tuple
from ._base import MdElement
[docs]
@dataclass
class TableData:
"""Data model for markdown table content.
Attributes:
header (list[str]): Column headers of the table
row_labels (list[str]): Row labels (used when table is transposed)
values (list[list[Any]]): 2D array of table values
"""
header: list[str]
row_labels: list[str]
values: Iterable[Union[list[Any], Tuple]]
[docs]
@classmethod
def from_dict_list(
cls,
data: list[Dict[str, Any]],
header: Optional[list[str]] = None,
row_labels: Optional[list[str]] = None,
) -> "TableData":
"""Create TableData from a list of dictionaries.
Args:
data (list[dict[str, Any]]): List of dictionaries to convert to table data
header (Optional[list[str]], optional): Custom header labels. Defaults to None.
row_labels (Optional[list[str]], optional): Custom row labels. Defaults to None.
Returns:
TableData: Converted table data
"""
if not data:
return cls(header=[], row_labels=[], values=[])
# Get header from data if not provided
if header is None:
header = list(data[0].keys())
# Extract values using the header order
values = []
for row in data:
row_values = []
for key in data[0].keys(): # Use original keys to maintain order
value = row.get(key, "")
row_values.append(value)
values.append(row_values)
# Use provided row_labels or empty list
row_labels = row_labels or []
return cls(header=header, row_labels=row_labels, values=values)
[docs]
def transpose(self) -> "TableData":
"""Create a transposed version of the table data.
Returns:
TableData: Transposed table data
"""
if not self.values:
return TableData(header=[], row_labels=[], values=[])
# Get original keys and values
transposed_values = list(zip(*self.values)) # We only have one row in this case
return TableData(
header=self.row_labels
or [""] * len(transposed_values[0]), # First key-value pair becomes header
row_labels=self.header,
values=transposed_values,
)
[docs]
class MdTable(MdElement):
"""Converter for dict or list to markdown table.
Args:
data (dict or list): The data to convert.
header (list[str], optional): Custom header labels. If not provided, dictionary keys will be used.
row_labels (list[str], optional): Custom row labels. If not provided, no row labels will be shown.
transpose (bool, optional): If True, transpose the table. Defaults to False.
precision (Optional[int]): Number of decimal places for floats. If None, values are not formatted.
Examples:
>>> data = {
... "Name": "John Doe",
... "Age": 30,
... "Occupation": "Software Engineer",
... }
>>> table = MdTable(data)
>>> print(table)
| Name | Age | Occupation |
| --- | --- | --- |
| John Doe | 30 | Software Engineer |
>>> # Custom headers
>>> table = MdTable(data, header=["Full Name", "Years", "Job"])
>>> print(table)
| Full Name | Years | Job |
| --- | --- | --- |
| John Doe | 30 | Software Engineer |
>>> # With row labels
>>> data = [
... {"Name": "John Doe", "Age": 30},
... {"Name": "Jane Doe", "Age": 25}
... ]
>>> table = MdTable(data, row_labels=["Person 1", "Person 2"])
>>> print(table)
| | Name | Age |
| --- | --- | --- |
| Person 1 | John Doe | 30 |
| Person 2 | Jane Doe | 25 |
>>> # Transposed table
>>> print(MdTable(data, transpose=True))
| | | |
| --- | --- | --- |
| Name | John Doe | Jane Doe |
| Age | 30 | 25 |
"""
def __init__(
self,
data: Union[Dict[str, Any], list[Dict[str, Any]]],
header: Optional[list[str]] = None,
row_labels: Optional[list[str]] = None,
transpose: bool = False,
precision: Union[None, int] = None,
):
"""Initialize a MdTable instance.
Args:
data (Union[Dict[str, Any], list[Dict[str, Any]]]): The data to convert.
header (list[str], optional): Custom header labels. If not provided, dictionary keys will be used.
row_labels (list[str], optional): Custom row labels. If not provided, no row labels will be shown.
transpose (bool, optional): If True, transpose the table. Defaults to False.
precision (Optional[int]): Number of decimal places for floats. If None, values are not formatted.
"""
if isinstance(data, dict):
data = [data]
elif not isinstance(data, list):
raise ValueError(
"Provided data is not a dictionary or list of dictionaries"
)
self.data = self._flatten_data(data)
self.header = header
self.row_labels = row_labels
self.transpose = transpose
self.precision = precision
def _flatten_data(self, data: list[Dict[str, Any]]) -> list[Dict[str, Any]]:
"""Flatten nested dictionaries in the data.
Args:
data (list[Dict[str, Any]]): The data to flatten.
Returns:
list[Dict[str, Any]]: Flattened data
"""
flattened_data = []
for entry in data:
flattened_data.append(self._flatten_dict(entry))
return flattened_data
def _flatten_dict(
self, d: Dict, parent_key: str = "", sep: str = "."
) -> Dict[str, Any]:
"""Recursively flatten a nested dictionary.
Args:
d (Dict): Dictionary to flatten.
parent_key (str, optional): Key from parent dictionary. Defaults to ''.
sep (str, optional): Separator to use between keys. Defaults to '.'.
Returns:
Dict: Flattened dictionary.
"""
items = {}
for k, v in d.items():
new_key = f"{parent_key}{sep}{k}" if parent_key else k
if isinstance(v, dict):
items.update(self._flatten_dict(v, new_key, sep=sep))
else:
items[new_key] = v
return items
def _value_to_string(self, value: Any) -> str:
"""Convert the given value to a string. If it's a floating point number,
it will be formatted according to the precision attribute.
Args:
value (Any): The value to be converted.
Returns:
str: The string representation of the value.
"""
if isinstance(value, float) and self.precision is not None:
return f"{value:.{self.precision}f}"
return str(value)
def _build_markdown_table(self, table_data: TableData) -> str:
"""Build markdown table from TableData.
Args:
table_data (TableData): The table data to convert to markdown
Returns:
str: Markdown formatted table
"""
if not table_data.values:
return ""
has_row_labels = bool(table_data.row_labels)
num_columns = len(next(iter(table_data.values)))
# Build header row
header_parts = []
if has_row_labels:
# Empty cell for row label column
header_parts.append("")
if table_data.header:
header_parts.extend(table_data.header)
# Format header row with correct spacing
header_cells = []
for part in header_parts:
if part:
header_cells.append(f" {part} ")
else:
# Single space for empty cells
header_cells.append(" ")
header_row = "|" + "|".join(header_cells) + "|" if header_parts else None
# Build separator row
if has_row_labels:
num_columns += 1
separator_row = "|" + "|".join([" --- "] * num_columns) + "|"
# Build value rows
value_rows = []
for i, row in enumerate(table_data.values):
row_parts = []
if has_row_labels and i < len(table_data.row_labels):
row_parts.append(table_data.row_labels[i])
row_parts.extend(self._value_to_string(val) for val in row)
value_rows.append("| " + " | ".join(row_parts) + " |")
# Combine all parts
table_parts = []
if header_row:
table_parts.append(header_row)
table_parts.append(separator_row)
table_parts.extend(value_rows)
return "\n".join(table_parts)
def _to_md_table(self) -> str:
"""Convert the data to a Markdown formatted table.
Args:
transpose (bool, optional): If True, transpose the table. Defaults to False.
precision (Union[None, int], optional): The precision for floating point numbers.
Returns:
str: Markdown formatted table.
"""
if not self.data:
return ""
# Create table data
table_data = TableData.from_dict_list(
self.data, header=self.header, row_labels=self.row_labels
)
# Handle transposition
if self.transpose:
table_data = table_data.transpose()
# Build markdown table
md_table = self._build_markdown_table(table_data)
return md_table
def __str__(self) -> str:
return self._to_md_table()