Spreadsheets (.xlsx)¶
A PDF is what you send when the numbers are final. A spreadsheet is what you
send when the recipient has to work with them: sort, filter, re-total,
check line by line. A budget, a price table, a reconciliation, a report
export — all of it arrives as .xlsx, and almost always gets assembled with
openpyxl by hand.
Assembling by hand costs three things, always the same three:
- Row arithmetic. Every write is a
(row, column)pair you track. Insert a line near the top and every constant below it shifts. - Styling that drifts. Four assignments (
font,fill,alignment,border) repeated on every cell, and the thousandth row stops matching the first. - The wrong number format.
"#,##0.00"looks right and is a trap: Excel resolves that mask with the locale of whoever opens the file. The workbook you built in São Paulo shows1.234,56at home and1,234.56on a colleague's en-US laptop. The value is the same; the document is wrong, and nobody notices.
tempest_fastapi_sdk.spreadsheet fixes all three: a row cursor, columns
declared once, and masks pinned to pt-BR.
Extra required
Bringsopenpyxl. The engine is imported at first use, so importing the
module — and defining your project's columns and theme — works without
it.
Your first spreadsheet¶
# scripts/budget.py
from decimal import Decimal
from tempest_fastapi_sdk.spreadsheet import (
BR_CURRENCY_FORMAT,
Column,
SheetWriter,
new_workbook,
workbook_to_bytes,
)
def main() -> None:
"""Write a two-item price table to disk."""
workbook = new_workbook("Orçamento")
writer = SheetWriter(
workbook["Orçamento"],
columns=[
Column("Item", width=48, wrap=True),
Column("Qty.", width=12, horizontal="center"),
Column("Unit price", width=20, number_format=BR_CURRENCY_FORMAT),
],
)
writer.title_block(["PREFEITURA MUNICIPAL DE EXEMPLO", "Pregão 1/2026"])
writer.header_row()
writer.write_row(["Installation service", 2, Decimal("2930.00")])
writer.write_row(["Monthly maintenance", 12, Decimal("450.50")])
writer.total_row(["Total", None, Decimal("11266.00")])
writer.apply_widths()
with open("budget.xlsx", "wb") as handle:
handle.write(workbook_to_bytes(workbook))
if __name__ == "__main__":
main()
Open the file: the title centred across all three columns, a navy header
with white text, amounts right-aligned as R$ 2.930,00, and the total row
picked out in amber.
What you did not write
No (row, column) pairs. No Font, PatternFill or Border. No mask
repeated per cell. The cursor belongs to SheetWriter, the styling
comes from the theme, the format comes from the column.
new_workbook and the ghost sheet¶
openpyxl always creates a workbook with one sheet called Sheet.
Forgetting to remove it ships a document with a stray empty tab — the kind
of detail that gives away that a file was generated by a script.
from tempest_fastapi_sdk.spreadsheet import new_workbook
workbook = new_workbook("Analysis", "Budget", "Feasibility")
print(workbook.sheetnames) # ['Analysis', 'Budget', 'Feasibility']
With no arguments the default sheet is kept — handy when you name it later.
Columns: declare them once¶
Column specifies the column, not a cell. It applies to every body row,
which is why the format cannot drift between the first row and the
thousandth.
from tempest_fastapi_sdk.spreadsheet import (
BR_CURRENCY_FORMAT,
BR_PERCENT_FORMAT,
Column,
TEXT_FORMAT,
)
columns = [
Column("Process", width=18, number_format=TEXT_FORMAT),
Column("Description", width=52, wrap=True),
Column("Discount", width=12, number_format=BR_PERCENT_FORMAT),
Column("Amount", width=18, number_format=BR_CURRENCY_FORMAT),
]
| Field | What it does |
|---|---|
title |
Header text, used by header_row() |
width |
Width in characters; None keeps the default (which truncates) |
number_format |
Mask applied to every body cell |
horizontal |
"left", "center", "right"; None lets Excel decide |
wrap |
Text wrapping — on for the description, off elsewhere |
wrap=True on a short column makes rows tall for nothing
Row height follows the tallest cell. A two-word column with wrapping on stretches the whole row and buys nothing.
Numbers, not strings¶
The temptation is to format in Python and write the finished text. The cell
then holds "R$ 2.930,00", which is text: the recipient cannot sum,
sort or filter on it, and Excel's SUM returns zero for the whole column.
from decimal import Decimal
from tempest_fastapi_sdk.spreadsheet import (
BR_CURRENCY_FORMAT,
Column,
SheetWriter,
new_workbook,
)
from tempest_fastapi_sdk.utils import format_currency_br
workbook = new_workbook("Budget")
writer = SheetWriter(
workbook["Budget"],
columns=[
Column("Item", width=48),
Column("Amount", width=18, number_format=BR_CURRENCY_FORMAT),
],
)
# ❌ becomes text: no sum, no sorting, no filtering
writer.write_row(["Service", format_currency_br(Decimal("2930.00"))])
# ✅ write the number and let the mask present it
writer.write_row(["Service", Decimal("2930.00")])
On screen both rows read the same R$ 2.930,00; in the file only the second
one is a number.
Use format_currency_br for prose
tempest_fastapi_sdk.utils.format_currency_br
exists for text headed to a PDF, an e-mail or a page. A spreadsheet cell
takes a number.
Brazilian formats¶
| Constant | Renders | For |
|---|---|---|
BR_CURRENCY_FORMAT |
R$ 1.234,56 |
Money with the symbol |
BR_CURRENCY_FORMAT_NO_SYMBOL |
1.234,56 |
Column whose header already says (R$) |
BR_QUANTITY_FORMAT |
1.234,56 |
Non-monetary quantity |
BR_INTEGER_FORMAT |
1.234 |
Counts, whole numbers |
BR_PERCENT_FORMAT |
30,00% |
Percentages |
BR_DATE_FORMAT |
14/08/2026 |
Dates |
BR_DATETIME_FORMAT |
14/08/2026 19:30 |
Date and time |
TEXT_FORMAT |
exactly what you wrote | An identifier that looks numeric |
What makes these masks work is the embedded language code — [$R$-416] for
currency, [$-416] for the rest. It pins the dot as thousands separator and
the comma as decimal inside the file, so the document reads the same on
any machine.
A percent cell holds the ratio, not the percentage
Excel multiplies by 100 itself. A cell with BR_PERCENT_FORMAT must
receive Decimal("0.30"), not 30 — writing 30 displays 3000,00%.
It reads like a typo but it is a unit error.
TEXT_FORMAT saves leading zeros
A CPF, a process number (0001/2026), a bank branch. Without it Excel
normalizes them to numbers and the zeros are gone for good.
The rows a document has¶
from decimal import Decimal
from tempest_fastapi_sdk.spreadsheet import (
BR_CURRENCY_FORMAT,
Column,
SheetWriter,
new_workbook,
)
workbook = new_workbook("Budget")
writer = SheetWriter(
workbook["Budget"],
columns=[
Column("Item", width=48),
Column("Qty.", width=10, horizontal="center"),
Column("Amount", width=18, number_format=BR_CURRENCY_FORMAT),
],
)
writer.title_block(["AGENCY", "Pregão 1/2026", "Annex I"]) # merged, centred
first_item_row = writer.header_row() # table header
writer.group_row(["GROUP 1 — HANDICRAFT"]) # in-table subheading
writer.write_row(["Item", 2, Decimal("10.00")]) # body
writer.total_row(["Total", None, Decimal("20.00")]) # emphasis
writer.blank_rows(2) # breathing room
All of them return the next free row — that is how first_item_row got
the position of the first item without anyone counting lines.
A None cell in the middle of a row is still styled: that is how a total
row skips the middle columns without losing its fill.
Live formulas¶
A string starting with = becomes a real formula:
from tempest_fastapi_sdk.spreadsheet import Column, SheetWriter, new_workbook
workbook = new_workbook("Budget")
writer = SheetWriter(workbook["Budget"], [Column("Item"), Column("Amount")])
writer.write_row(["Checked sum", "=SUM(B5:B24)"])
Worth it for closing checks. An auditor who edits a value sees the number react, instead of reading a constant that was true only at generation time.
Theme¶
SheetStyle is plain data — hex colours, integer sizes, no openpyxl
objects. That is what makes your project's theme definable, testable and
comparable without the extra installed.
from tempest_fastapi_sdk.spreadsheet import SheetStyle, SheetWriter
CORPORATE = SheetStyle(
header_background="0B3D2E",
header_foreground="FFFFFF",
group_background="D6E9DF",
total_background="F3E5AB",
border_color="C0C0C0",
font_name="Calibri",
)
Pass it to the constructor: SheetWriter(sheet, columns, style=CORPORATE).
Colours follow the openpyxl convention
RRGGBB or AARRGGBB, without a leading #.
Serving it as a download¶
None of this touches disk: workbook_to_bytes returns the bytes and the
handler ships them.
from fastapi import APIRouter
from fastapi.responses import Response
from tempest_fastapi_sdk.spreadsheet import new_workbook, workbook_to_bytes
from tempest_fastapi_sdk.utils import build_content_disposition
XLSX_MEDIA_TYPE = (
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
)
router = APIRouter()
@router.get("/budgets/{budget_id}/spreadsheet")
async def download_budget(budget_id: int) -> Response:
"""Stream the budget as an .xlsx download."""
workbook = new_workbook("Orçamento")
return Response(
content=workbook_to_bytes(workbook),
media_type=XLSX_MEDIA_TYPE,
headers={
"Content-Disposition": build_content_disposition(
f"budget-{budget_id}.xlsx",
),
},
)
No temp file, no race
Two concurrent requests would write the same temporary path. In memory the problem does not exist — and there is nothing to clean up.
Recap¶
new_workbook("Sheet")creates the workbook without openpyxl's ghost tab.Columndeclares title, width, mask and alignment once.SheetWriterowns the cursor:title_block,header_row,group_row,write_row,total_row,blank_rows— all return the next free row.- Write numbers; the mask presents them. Finished text kills sum and filter.
- The
BR_*masks embed language code416, so the file reads the same under any locale. SheetStyleis plain data, so a theme exists without the extra.workbook_to_byteshands you bytes — HTTP response, storage, e-mail.
To ship the same content as a closed document, see PDF generation. For the currency helpers that format the document's prose, see Brazilian helpers.