Skip to content

tempestweb.export

CSV and XLSX generation in pure Python, for native.file.save to deliver. Modes A and B (Mode C refuses the import at build time). Installs nothing — zipfile and xml.etree from the standard library are enough for XLSX.

Guide with examples: Export CSV and XLSX.

tempestweb.export

Generate CSV and XLSX bytes in Python, for native.file.save to deliver.

native.file.save hands bytes to the user. Nothing produced those bytes: an app showing a DataTable with an "Export CSV" button had to write the encoder by hand, and hand-rolled encoders fail in the same four places every time — a separator inside a field, a quote inside the text, the missing UTF-8 BOM that makes Excel read João as João, and an XLSX date written as a bare number.

This is byte generation, so it needs no browser: it runs in Python, and the file.save that already exists delivers the result.

Modes A and B only

Mode C transpiles the app's own Python into JavaScript and serves a fixed set of modules — tempest_core, tempestweb.components and tempestweb.native. Importing this package from a Mode C app is refused at build time with a named error. A Mode C app exports by asking the server for the file.

Example
from tempestweb import native
from tempestweb.export import Column, to_csv, to_xlsx

COLUMNS = [
    Column("id", "ID"),
    Column("name", "Nome"),
    Column("created_at", "Criado em"),
]


async def export_csv(rows: list[dict[str, object]]) -> None:
    await native.file.save(
        "usuarios.csv",
        to_csv(rows, COLUMNS),
        mime_type="text/csv",
    )


async def export_xlsx(rows: list[dict[str, object]]) -> None:
    await native.file.save(
        "usuarios.xlsx",
        to_xlsx(rows, COLUMNS, sheet="Usuários"),
        mime_type=XLSX_MIME_TYPE,
    )

Import everything from this package level rather than from submodules.

Column dataclass

One column of an export.

Attributes:

Name Type Description
field str

The mapping key (or attribute name) the value is read from.

header str

The text written in the header row.

format Formatter | None

An optional callable turning the raw value into the cell value. Use it for presentation — lambda v: v.strftime("%d/%m/%Y") — and note that returning a string makes the XLSX cell a text cell.

Source code in tempestweb/export/columns.py
@dataclass(frozen=True)
class Column:
    """One column of an export.

    Attributes:
        field: The mapping key (or attribute name) the value is read from.
        header: The text written in the header row.
        format: An optional callable turning the raw value into the cell value.
            Use it for presentation — ``lambda v: v.strftime("%d/%m/%Y")`` — and
            note that returning a string makes the XLSX cell a text cell.
    """

    field: str
    header: str
    format: Formatter | None = None

    def value_of(self, row: object) -> object:
        """Read this column's value out of one row.

        Args:
            row: The row to read, either a mapping or an object with attributes.

        Returns:
            The raw value, or the result of :attr:`format` when one is set.

        Raises:
            ColumnFieldError: If the row has neither the key nor the attribute.
        """
        raw = _read_field(row, self.field)
        return self.format(raw) if self.format is not None else raw

value_of

value_of(row: object) -> object

Read this column's value out of one row.

Parameters:

Name Type Description Default
row object

The row to read, either a mapping or an object with attributes.

required

Returns:

Type Description
object

The raw value, or the result of :attr:format when one is set.

Raises:

Type Description
ColumnFieldError

If the row has neither the key nor the attribute.

Source code in tempestweb/export/columns.py
def value_of(self, row: object) -> object:
    """Read this column's value out of one row.

    Args:
        row: The row to read, either a mapping or an object with attributes.

    Returns:
        The raw value, or the result of :attr:`format` when one is set.

    Raises:
        ColumnFieldError: If the row has neither the key nor the attribute.
    """
    raw = _read_field(row, self.field)
    return self.format(raw) if self.format is not None else raw

ColumnFieldError

Bases: ExportError

A :class:~tempestweb.export.Column names a field the row does not have.

Raised instead of writing an empty cell: a typo in Column("nmae", "Nome") would otherwise export a full column of blanks, and nothing in the pipeline would say so.

Source code in tempestweb/export/errors.py
class ColumnFieldError(ExportError):
    """A :class:`~tempestweb.export.Column` names a field the row does not have.

    Raised instead of writing an empty cell: a typo in ``Column("nmae", "Nome")``
    would otherwise export a full column of blanks, and nothing in the pipeline
    would say so.
    """

ExportError

Bases: ValueError

Base class for every export failure.

Source code in tempestweb/export/errors.py
class ExportError(ValueError):
    """Base class for every export failure."""

SheetNameError

Bases: ExportError

The sheet name is one Excel refuses to open.

Excel caps sheet names at 31 characters and rejects [ ] : * ? / \\, a leading or trailing apostrophe, an empty name, and the reserved name History. A workbook carrying an invalid name opens as "unreadable content", with no hint about which part is at fault.

Source code in tempestweb/export/errors.py
class SheetNameError(ExportError):
    r"""The sheet name is one Excel refuses to open.

    Excel caps sheet names at 31 characters and rejects ``[ ] : * ? / \\``, a
    leading or trailing apostrophe, an empty name, and the reserved name
    ``History``. A workbook carrying an invalid name opens as "unreadable
    content", with no hint about which part is at fault.
    """

to_csv

to_csv(rows: Iterable[object], columns: Sequence[Column], *, bom: bool = True, delimiter: str = ',', header: bool = True) -> bytes

Render rows as CSV bytes.

Parameters:

Name Type Description Default
rows Iterable[object]

The rows to write, in order. Consumed once, so a generator works.

required
columns Sequence[Column]

The columns to write, in order.

required
bom bool

Whether to prefix the UTF-8 byte order mark. On by default because the common destination is Excel, which misreads accented text without it.

True
delimiter str

The field separator. Excel in a pt-BR locale expects ";" and puts a comma-separated file into a single column — see the warning in the recipe.

','
header bool

Whether to write the header row.

True

Returns:

Type Description
bytes

The encoded file, UTF-8, ready to hand to

bytes

func:tempestweb.native.file.save.

Raises:

Type Description
ExportError

If columns is empty or delimiter is not exactly one character.

ColumnFieldError

If a column names a field a row does not have.

Source code in tempestweb/export/csv_writer.py
def to_csv(
    rows: Iterable[object],
    columns: Sequence[Column],
    *,
    bom: bool = True,
    delimiter: str = ",",
    header: bool = True,
) -> bytes:
    """Render rows as CSV bytes.

    Args:
        rows: The rows to write, in order. Consumed once, so a generator works.
        columns: The columns to write, in order.
        bom: Whether to prefix the UTF-8 byte order mark. On by default because
            the common destination is Excel, which misreads accented text
            without it.
        delimiter: The field separator. Excel in a pt-BR locale expects ``";"``
            and puts a comma-separated file into a single column — see the
            warning in the recipe.
        header: Whether to write the header row.

    Returns:
        The encoded file, UTF-8, ready to hand to
        :func:`tempestweb.native.file.save`.

    Raises:
        ExportError: If ``columns`` is empty or ``delimiter`` is not exactly one
            character.
        ColumnFieldError: If a column names a field a row does not have.
    """
    if not columns:
        raise ExportError("to_csv needs at least one column")
    if len(delimiter) != 1:
        raise ExportError(f"delimiter must be exactly one character, got {delimiter!r}")

    buffer = io.StringIO(newline="")
    writer = csv.writer(
        buffer,
        delimiter=delimiter,
        quoting=csv.QUOTE_MINIMAL,
        lineterminator=LINE_TERMINATOR,
    )
    if header:
        writer.writerow([column.header for column in columns])
    for row in rows:
        writer.writerow([_as_text(column.value_of(row)) for column in columns])

    return ((BOM if bom else "") + buffer.getvalue()).encode("utf-8")

to_xlsx

to_xlsx(rows: Iterable[object], columns: Sequence[Column], *, sheet: str = 'Planilha1', header: bool = True) -> bytes

Render rows as a single-worksheet XLSX workbook.

Parameters:

Name Type Description Default
rows Iterable[object]

The rows to write, in order. Consumed once, so a generator works.

required
columns Sequence[Column]

The columns to write, in order.

required
sheet str

The worksheet name shown on the tab.

'Planilha1'
header bool

Whether to write the header row, in bold.

True

Returns:

Type Description
bytes

The workbook bytes, ready to hand to

bytes

func:tempestweb.native.file.save with the

bytes

application/vnd.openxmlformats-officedocument.spreadsheetml.sheet

bytes

MIME type.

Raises:

Type Description
ExportError

If columns is empty, or a date falls before :data:EXCEL_MIN_DATE, which the serial cannot represent.

SheetNameError

If sheet is one Excel refuses to open.

ColumnFieldError

If a column names a field a row does not have.

Source code in tempestweb/export/xlsx_writer.py
def to_xlsx(
    rows: Iterable[object],
    columns: Sequence[Column],
    *,
    sheet: str = "Planilha1",
    header: bool = True,
) -> bytes:
    """Render rows as a single-worksheet XLSX workbook.

    Args:
        rows: The rows to write, in order. Consumed once, so a generator works.
        columns: The columns to write, in order.
        sheet: The worksheet name shown on the tab.
        header: Whether to write the header row, in bold.

    Returns:
        The workbook bytes, ready to hand to
        :func:`tempestweb.native.file.save` with the
        ``application/vnd.openxmlformats-officedocument.spreadsheetml.sheet``
        MIME type.

    Raises:
        ExportError: If ``columns`` is empty, or a date falls before
            :data:`EXCEL_MIN_DATE`, which the serial cannot represent.
        SheetNameError: If ``sheet`` is one Excel refuses to open.
        ColumnFieldError: If a column names a field a row does not have.
    """
    if not columns:
        raise ExportError("to_xlsx needs at least one column")
    _check_sheet_name(sheet)

    parts = (
        ("[Content_Types].xml", _CONTENT_TYPES),
        ("_rels/.rels", _ROOT_RELS),
        ("xl/workbook.xml", _workbook_xml(sheet)),
        ("xl/_rels/workbook.xml.rels", _WORKBOOK_RELS),
        ("xl/styles.xml", _STYLES),
        ("xl/worksheets/sheet1.xml", _sheet_xml(rows, columns, header=header)),
    )

    buffer = io.BytesIO()
    with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as archive:
        for name, payload in parts:
            info = zipfile.ZipInfo(name, date_time=ZIP_TIMESTAMP)
            info.compress_type = zipfile.ZIP_DEFLATED
            archive.writestr(info, payload)
    return buffer.getvalue()