Rendering and delivery¶
A Caxton document describes a workbook; rendering turns that description into an artifact. The document does not own a filename, a byte buffer or a spreadsheet engine. Those choices belong to the render call, so one immutable document can serve different destinations. Reusing it across operations that read data also requires reiterable sources; immutability does not reset a generator.
The examples below use one small catalog:
from caxton import sheet, spreadsheet, table, text
catalog = spreadsheet(
sheet(
"Catalog",
table(
source=(
{"title": "Kindred", "author": "Octavia E. Butler"},
{"title": "Piranesi", "author": "Susanna Clarke"},
),
columns=(
text(source="title", title="Title"),
text(source="author", title="Author"),
),
name="books",
),
),
)
Choose where the artifact goes¶
Use render() when the caller needs XLSX bytes in memory:
from caxton import render
result = render(catalog)
assert result.data is not None
xlsx_bytes = result.data
This is a natural fit for an HTTP response, object storage client or test that immediately inspects the finished
workbook. The complete artifact remains in result.data, so a large export also remains in process memory.
Use write() with a path when the file is the result you want to keep:
from pathlib import Path
from caxton import write
result = write(catalog, Path("catalog.xlsx"))
assert result.data is None
assert result.target == "catalog.xlsx"
The path suffix supplies the format hint. A path without a suffix falls back to XLSX, and an explicit format= takes
precedence over the suffix. Keeping the two consistent makes the artifact unambiguous to other programs.
A writable binary object is useful when another library owns the destination:
from io import BytesIO
from caxton import write
buffer = BytesIO()
result = write(catalog, buffer, format="xlsx")
assert result.data == buffer.getvalue()
Buffers have no filename from which to infer a format. XLSX is the default, but passing format="xlsx" makes the
boundary explicit. Buffer writes retain the completed bytes in result.data; path writes do not.
| Destination | Operation | Artifact bytes in result.data |
Typical use |
|---|---|---|---|
| Caxton-owned memory | render() |
Yes | HTTP responses, tests, object APIs |
| Filesystem path | write() |
No | Reports and scheduled exports |
Writable binary object or BytesIO |
write() |
Yes | Framework and library integrations |
All three return a RenderResult. Its format, mime_type, renderer, bytes_written, execution_mode and
execution_plan describe what actually happened. The older content property is a deprecated alias for data.
Let the workbook operation guide backend selection¶
For a new XLSX workbook, Caxton uses XlsxWriter by default. Select the create-new OpenPyXL adapter only when an integration requires it:
Templates follow a different route. Adding a template to the document changes the workbook operation from “create a
workbook” to “fill this workbook,” and Caxton selects the dedicated openpyxl-template renderer. It does not fall back
to a create-new backend if the template route is incompatible. See Templates for that workflow.
Renderer selection happens before compilation and before row data is read. Caxton checks the requested format, document kind, IR version, workbook operation, required features and execution mode against the renderer descriptor. An unsupported backend or capability therefore fails before a destination is changed.
Pass backend= when choosing between bundled implementations of the same format. Pass renderer= when supplying a
renderer object of your own; the explicit renderer still has to satisfy the same compatibility checks.
Choose an execution mode¶
Execution mode controls how the selected renderer builds the artifact. It does not change the document model or the shape of the resulting workbook.
| Mode | Behavior |
|---|---|
AUTO |
Let the renderer choose a compatible plan. This is the default. |
STANDARD |
Use ordinary workbook construction, including features that need non-append-only access. |
STREAM |
Request a constant-memory or write-only plan from a renderer that supports one. |
The XlsxWriter renderer can use either standard or constant-memory execution. In AUTO, it chooses constant-memory for
append-only worksheets and standard execution when the document needs a named XLSX table or shape-dependent buffering.
OpenPyXL create-new and template rendering currently support standard execution only.
Request streaming when bounded workbook memory is a requirement rather than a preference:
from caxton import ExecutionMode, sheet, spreadsheet, table, text, write
rows = ({"title": title} for title in ("Kindred", "Piranesi"))
reading_list = spreadsheet(
sheet(
"Reading list",
table(
source=rows,
columns=(text(source="title", title="Title"),),
),
),
)
result = write(
reading_list,
"reading-list.xlsx",
mode=ExecutionMode.STREAM,
)
assert result.execution_plan == "constant_memory"
The table is deliberately unnamed: creating a native XLSX table needs standard execution. Grouping, matrices and other
features that prepare the complete output shape can also rule out streaming. Caxton does not commit the output target
when STREAM is rejected. A named table is rejected before its ordinary row stream is entered, but grouped tables and
matrices may consume their single preparation pass before XlsxWriter reports shape_dependent_buffering. Use a fresh
source if you plan to retry those documents with STANDARD.
Streaming does not make a one-shot source reusable. A generator is still consumed once by the successful render; create a fresh document or materialize the rows before rendering it again.
Know when the destination changes¶
Path delivery is transactional. Caxton renders to a sibling staging file, then atomically replaces the destination only after the renderer succeeds. If validation, row evaluation or the backend fails, an existing file remains unchanged and an absent target remains absent.
Binary targets also receive data only after rendering succeeds. A seekable buffer is rewound to offset zero, overwritten and truncated, so old trailing bytes cannot survive a shorter artifact. Forward-only writers receive the finished artifact in chunks, and Caxton retries valid short writes until every byte is delivered.
The distinction matters during delivery failure. A filesystem replacement is atomic. A general buffer or stream cannot
promise rollback once its own write() method has accepted bytes; Caxton reports such failures as OutputError and
preserves the original exception as the cause.
Add a custom renderer¶
A custom renderer implements the public Renderer protocol and declares its contract in a RendererDescriptor. The
pipeline supplies a compiled, read-only SpreadsheetIR, an OutputSink and a resolved RenderContext.
This small renderer writes worksheet names as plain text. Its descriptor advertises an empty feature set, so the example document contains empty worksheets; a production renderer must declare every semantic feature it can handle.
from caxton import render, sheet, spreadsheet
from caxton.core.ir import SPREADSHEET_IR_VERSION, SpreadsheetIR
from caxton.core.models import DocumentKind
from caxton.core.protocols import OutputSink
from caxton.core.rendering import (
RenderContext,
RendererCapabilities,
RendererDescriptor,
RenderResult,
)
class WorksheetListRenderer:
descriptor = RendererDescriptor(
name="worksheet-list",
version="1.0",
formats=frozenset(("txt",)),
mime_types=frozenset(("text/plain",)),
extensions=frozenset((".txt",)),
capabilities=RendererCapabilities(
ir_versions={
DocumentKind.SPREADSHEET: frozenset((SPREADSHEET_IR_VERSION,)),
},
),
)
def render(
self,
document: SpreadsheetIR,
sink: OutputSink,
context: RenderContext,
) -> RenderResult:
payload = (
"\n".join(sheet.name for sheet in document.worksheets) + "\n"
).encode()
written = sink.write(payload)
return RenderResult(
format=context.format,
mime_type="text/plain",
renderer=self.descriptor.name,
bytes_written=written,
)
document = spreadsheet(sheet("Catalog"), sheet("Archive"))
result = render(document, format="txt", renderer=WorksheetListRenderer())
assert result.data == b"Catalog\nArchive\n"
Pass a renderer directly to render() or write(); there is no global registry or entry-point discovery. Keep engine
objects inside the adapter and consume each SpreadsheetTableIR.rows stream once. If an algorithm truly needs repeated
access, call table.rows.materialized() deliberately and account for the memory cost.
The public contracts are listed in caxton.core.protocols, and the versioned spreadsheet
IR is documented in caxton.core.ir.