Export DuckDB UI notebooks to standalone static HTML, re-executing cells against the target database.
Project description
duckdb-nb-export
duckdb-nb-export is a CLI for exporting DuckDB UI (duckdb --ui) notebooks
to a single static HTML file by re-executing notebook cells against a target
database. It fills the current gap where DuckDB UI notebooks do not have a
Jupyter nbconvert equivalent.
Status: beta, Phase 1, CLI only. Published on PyPI as
duckdb-nb-export. It depends
on DuckDB UI's unofficial internal ui.db schema, so a DuckDB UI update can
break notebook discovery or parsing. This is an unofficial third-party tool,
not affiliated with DuckDB, DuckDB Labs, or MotherDuck.
For users
Requirements
- Python >=3.11
duckdb>=1.5.4- Linux and macOS are fully supported. Windows is supported on a
best-effort basis: reading
ui.dbwhile DuckDB UI is running is not possible on Windows because of OS file locking (copying a file another process holds open for read-write fails there). Close DuckDB UI before exporting on Windows; once the UI is closed, both the default snapshot path and--require-ui-closedwork.
Installation
uv tool install duckdb-nb-export
or:
pip install duckdb-nb-export
To install the latest unreleased code from this repository instead, use
uv tool install git+<repository-url> or pip install git+<repository-url>.
Quick start
List available notebooks:
duckdb-nb-export --list
Export a notebook:
duckdb-nb-export "My Notebook" -o report.html
By default, the exporter reads DuckDB UI notebooks from
~/.duckdb/extension_data/ui/ui.db. If your ui.db is stored in a custom
location, pass it explicitly:
duckdb-nb-export --ui-db ~/duckdb-ui-profiles/work/ui.db --list
duckdb-nb-export "My Notebook" --ui-db ~/duckdb-ui-profiles/work/ui.db -o report.html
The default reader path uses a snapshot copy of ui.db and its WAL file, so
the command can read notebooks while DuckDB UI is still running. Use
--require-ui-closed only when you want to open ui.db directly.
Notebook name matching resolves against the display name shown in DuckDB UI
(the current notebook title) first, and falls back to DuckDB UI's internal
notebook name (the notebook_... slug stored in notebooks.name) only if
no title matches. --list always shows the display name. If several
notebooks share the same display name, pass --notebook-id <id> (from
--list) to select one unambiguously.
How it works
The exporter copies ui.db together with its WAL, reads the notebook
definition, re-executes each SQL cell against the target DuckDB database, and
renders one standalone HTML file. The HTML has inline CSS, supports light and
dark color schemes, and does not reference external resources.
The rendered HTML footer records the export timestamp, DuckDB version,
notebook version, tool version, target database, and write mode, so a
downstream reader can tell how the export was produced without access to the
original command line. The target database line shows only a privacy-safe
display form, never the full connect string or path: :memory: is shown
verbatim, URI-style connect strings such as md:... or postgres://...
(which may embed credentials) show only the scheme, e.g. md: (URI), and
plain file paths show only the basename, e.g. sales.duckdb. The write mode
line shows one of rollback (default), writes committed (--allow-writes),
or read-only, matching the safety model described below.
During execution, the exporter logs a cell_started and cell_finished
event (via structlog, to stderr) for every cell, including its 1-based
index, the total cell count, and, for cell_finished, the resulting status
and duration in seconds. This gives visibility into long-running notebooks
without waiting for the whole export to finish; pipe or grep stderr to
follow progress.
If Ctrl-C (SIGINT) is sent while a cell is executing, the exporter attempts
to interrupt the running query and, if that succeeds within
--interrupt-grace, rolls back and closes the target database connection
before exiting. If the query cannot be interrupted in time, the connection
is intentionally left untouched (mirroring the uninterruptible-timeout
behavior) rather than risking a hang or a corrupt state.
Safety model
Exporting a notebook executes its SQL with your privileges. Do not export notebooks from sources you would not trust enough to run yourself.
By default, notebook cells run inside one transaction and the exporter finishes
with ROLLBACK, so changes inside the target database file are not retained.
Use --allow-writes only when you want the exporter to commit those changes.
For a stronger no-writes guarantee, pass --read-only to open the target
database in DuckDB's read-only mode instead: notebook cells that create or
modify tables then fail outright rather than being rolled back after the
fact. --read-only and --allow-writes are mutually exclusive, and
--read-only cannot be combined with a :memory: target (DuckDB cannot open
:memory: read-only). The default remains rollback-based (not read-only)
because some analytics notebooks create intermediate tables that are expected
to be rolled back at the end of the run.
With --allow-writes, if a cell error or an unrecoverable timeout aborts the
transaction, the exporter never partially commits: it skips the remaining
cells, rolls back the whole transaction instead of committing it, and adds a
warning to the rendered HTML explaining that nothing was committed. This
avoids silently persisting only the writes made before (or, for a
timeout-abort, only after) the point of failure.
ROLLBACK cannot undo external side effects such as COPY ... TO file writes,
writes to an attached database, remote writes, INSTALL, or LOAD. The CLI
therefore asks for confirmation before execution; in non-interactive contexts,
use --yes to make that confirmation explicit. The confirmation prompt shows
the notebook name, version, cell count, target database, write mode, and
output path, followed by a short masked preview of each cell (first two
non-empty lines, up to 160 characters); URI-style target databases are shown
as their scheme only because URIs can embed credentials. --no-external-access runs
with DuckDB external access disabled, which also disables external file reads
such as CSV or Parquet scans.
CREATE SECRET parameter values are masked as *** in rendered SQL. Secrets
written in any other SQL form are not detected and will remain in the HTML.
Masking only covers CREATE SECRET statement text; it does not cover every
way a credential can end up in the exported HTML. Credentials embedded in
other SQL forms are exported verbatim: for example, an
ATTACH 'postgres://user:password@host/db' AS pg; cell renders its full
connection string, password included, in the rendered SQL. Query results
are never masked either: a cell such as SELECT * FROM duckdb_secrets();
renders secret values as ordinary table cells, exposing them just like any
other query output. Review the generated HTML before sharing it, and prefer
DuckDB's Secrets Manager (CREATE SECRET, whose parameter values are masked)
over inline credentials in ATTACH strings or other SQL wherever possible.
If no target database is resolved, execution falls back to :memory: and emits
a warning. DuckDB UI notebook JSON stores database names, not reliable file
paths, so pass --db <path> for exports that depend on existing tables. This
fallback warning is only emitted when :memory: was chosen automatically;
passing --db :memory: explicitly does not trigger it.
--db <path> must point to an existing DuckDB database file (or :memory:,
or a URI-style connect string such as md:...); a nonexistent local path is
rejected with exit code 6 instead of silently creating an empty database
file, which usually means the path was mistyped.
Found a security issue, including a CREATE SECRET masking bypass? See
SECURITY.md for how to report it privately.
CLI reference
The command is registered by [project.scripts] as duckdb-nb-export.
| Argument or option | Meaning | Default |
|---|---|---|
notebook_name |
Notebook name to export. Optional when --list is used. |
None |
-h, --help |
Show help and exit. | Off |
--version |
Show the tool version (and the DuckDB version in use) and exit. | Off |
-o, --output |
Output HTML path. | <notebook-name>.html under the allowed base |
--output-dir |
Allowed base directory and default output directory. | Current directory |
--notebook-id |
Export the notebook with this exact ID (from --list); use when names are ambiguous. |
None |
--db |
Target DuckDB database path for notebook re-execution. Must exist (a nonexistent local path is rejected instead of creating a new file). | Resolved from notebook metadata, then :memory: |
--ui-db |
Path to DuckDB UI ui.db. |
~/.duckdb/extension_data/ui/ui.db |
--nb-version |
Notebook version identifier to export. Must be an integer string; an unknown (but well-formed) version is reported as exit code 1 (see --list-versions). |
Latest version |
--list |
List notebooks and exit. | Off |
--list-versions |
List versions for the selected notebook and exit. | Off |
--json |
With --list or --list-versions, print the listing as a JSON array instead of a table. |
Off |
--max-rows |
Maximum rows to render per cell. Must be a positive integer (>= 1). | 1000 |
--cell-timeout |
Per-cell execution timeout in seconds. Must be a positive, finite number. | 300.0 |
--interrupt-grace |
Seconds to wait after a timeout interrupt before abandoning execution. Must be a positive, finite number. | 30.0 |
--stop-on-error |
Stop processing after the first cell error. | Off |
--no-fail-on-cell-error |
Exit 0 even when individual cells fail (previous default). Timeouts and abandoned execution still exit 2. | Off |
--allow-writes |
Commit notebook changes instead of rolling them back. Mutually exclusive with --read-only. |
Off |
--read-only |
Open the target database in DuckDB read-only mode for a stronger no-writes guarantee. Cells that create or modify tables fail. Mutually exclusive with --allow-writes; cannot be combined with a :memory: target. |
Off |
--no-external-access |
Disable DuckDB external access during execution. | Off |
--require-ui-closed |
Open ui.db directly and require DuckDB UI to be closed. |
Off |
--yes |
Skip the execution confirmation prompt. | Off |
--force |
Overwrite the output file if it exists, instead of writing to a numeric-suffixed sibling path. | Off |
-q, --quiet |
Only show ERROR-level log events on stderr. Mutually exclusive with -v/--verbose. |
Off |
-v, --verbose |
Show DEBUG-level log events on stderr in addition to the default. Mutually exclusive with -q/--quiet. |
Off |
By default, existing output files are not overwritten; a numeric suffix is
added. Pass --force to overwrite the requested path in place instead (no
suffix, no dedupe warning). The HTML is written atomically: it is staged in
a temporary file in the destination directory and moved into place with an
atomic rename, so a reader never observes a partially written export, and
the suffixed name is reserved on disk the moment it is chosen so a
concurrent export cannot claim the same path. On success, the CLI prints
the final output path (after any numeric-suffix deduplication) as a single
line to stdout, so scripts can capture it directly; a renamed path also
emits a warning naming the requested and actual paths on stderr.
If -o/--output points outside the allowed base directory (the current
directory by default, or the directory passed to --output-dir), the export
is rejected with exit code 3. To write outside the current directory, pass
that location to --output-dir so it becomes the allowed base itself, for
example:
duckdb-nb-export "My Notebook" --output-dir /tmp -o /tmp/report.html
Exit codes
| Code | Meaning |
|---|---|
| 0 | Success. |
| 1 | Notebook not found, notebook name is ambiguous (use --notebook-id), or --nb-version does not match any stored version of the resolved notebook (use --list-versions). |
| 2 | One or more cells failed, timed out, or were skipped, or --stop-on-error stopped processing after the first cell error, or a timeout interrupt failed and the export ended partially. A non-integer --nb-version, or a non-positive --max-rows, --cell-timeout, or --interrupt-grace value, is also rejected with this code, as a standard argument-parsing error. |
| 3 | Output path rejected because it escapes the allowed base directory. |
| 4 | ui.db access failed, including lock, corruption, storage-version mismatch, an unsupported stored notebook format, or a ui.db file whose schema does not match what this tool expects. |
| 5 | Execution confirmation was declined, including non-interactive execution without --yes and EOF (Ctrl-D) at the confirmation prompt. |
| 6 | Notebook execution or HTML writing failed, including a missing or unusable --db target. |
| 130 | Interrupted by Ctrl-C (128 + SIGINT), at the confirmation prompt or during execution. |
By default, the exit code fails (exit 2) whenever any cell result is not a
plain success (ERROR, SKIPPED_ABORT, REJECTED_TRANSACTION_STATEMENT,
TIMEOUT), or execution was abandoned after an uninterruptible timeout;
--stop-on-error additionally stops processing early after the first cell
error. Whenever any cell fails, the CLI logs a cell_failed event per
failed cell (1-based cell index, status, and a truncated error message) and
a final cells_failed_summary event naming the output path, both on
stderr, before returning the exit code; this happens regardless of
--no-fail-on-cell-error. Pass --no-fail-on-cell-error to restore the
previous (pre-0.0.3) behavior of exiting 0 whenever the run completes
despite per-cell failures, which are still reported in the rendered HTML
and on stderr; timeouts and abandoned execution still exit 2 even with
this flag, since those indicate the export itself did not run to
completion as requested.
Limitations
- Chart rendering is not supported, and this is a permanent limitation
rather than a pending feature: DuckDB UI does not persist chart
configuration anywhere (neither in notebook JSON nor in any
ui.dbtable, verified against the UI frontend bundle), so there is no stored chart definition an exporter could reproduce. Cells are re-executed and displayed as tables. If a future DuckDB UI version starts persisting chart settings, chart export will be reconsidered. Chart cells cannot be detected in stored notebook data at all: stored format v3 does not record whether a cell was displayed as a chart, so exported HTML shows such cells as ordinary SQL cells. - Result display is limited to the first 1,000 rows by default. The total row count is not computed.
- Long scalar values are truncated after 500 characters in HTML output.
- The reader depends on DuckDB UI's unofficial schema and stored notebook
format v3. A stored notebook whose
notebookSerializationFormatis not 3 is rejected with a clear error (exit code 4) instead of being exported with unverified, potentially incorrect results; aui.dbfile whosenotebooks/notebook_versionstables or expected columns are missing is rejected the same way instead of surfacing a raw DuckDB Catalog Error. - Notebook JSON's
currentDatabase/useDatabase(database name) is applied with a best-effortUSEon re-execution: it switches the database only when a catalog with that name is attached (via--dbor an earlier ATTACH cell), and otherwise warns and continues. Database names are not resolved to file paths, so still pass--db <path>for exports that depend on existing tables.
License
MIT. See LICENSE.
For developers
Documentation
- Design document (Japanese)
- Architecture decision records (Japanese)
- Test design document (Japanese)
- Changelog
Setup
uv sync --group dev
uv run pre-commit install
Common tasks
uv run pytest
uv run poe check
Pytest markers: assumptions guards DuckDB behavior and the unofficial UI
schema; integration covers multi-layer real-file/process tests; e2e drives
the real CLI and compares golden HTML.
Test suite
The project is test-driven: tests describe Phase 1 behavior before the
implementation is treated as complete. Failures in assumption tests (AT-*)
are not ordinary implementation bugs; they signal that a documented design
assumption about DuckDB behavior or the unofficial DuckDB UI schema changed.
Golden HTML fixtures are updated with:
UPDATE_GOLDEN_HTML=1 uv run pytest tests/test_e2e.py
Fixtures
tests/fixtures/ui_db is regenerated with:
uv run python scripts/regenerate_ui_db_fixtures.py
The current checked-in fixtures were generated by the fallback SQL generator because the fixture environment did not have an available browser.
Architecture
Reader copies and reads ui.db; Executor re-executes cells in one transaction;
Renderer builds a single Jinja2/Pygments HTML document. The CLI wires those
layers together. models/ separates internal notebook models from stored
DuckDB UI format v3 models (Stored*).
DuckDB UI ui.db -> Reader -> Executor -> Renderer -> standalone HTML
CLI wiring and validation connects the full pipeline.
CI
GitHub Actions runs lint once on Ubuntu across Python 3.11 and latest Python
3, then runs tests across ubuntu-latest and macos-latest, Python 3.11 and
latest Python 3, and DuckDB 1.5.4 plus latest DuckDB. Assumption-test
failures on latest DuckDB are non-blocking signals for design review; failures
on DuckDB 1.5.4 remain blocking.
Roadmap
See design document section 2.1. Phase 1.5 adds a notebook-cell callable
export_notebook_html() path after the target-database lock problem is
resolved. Phase 2 targets a C++ rendering core; client-side chart embedding
is dormant because DuckDB UI does not persist chart settings (see
Limitations), and will only be revisited if a future DuckDB UI version
starts persisting them.
Conventions
Application logging uses structlog; do not use print in package code.
Docstrings are English and follow NumPy-style sections. User-facing messages
are English.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file duckdb_nb_export-0.1.0.tar.gz.
File metadata
- Download URL: duckdb_nb_export-0.1.0.tar.gz
- Upload date:
- Size: 55.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
daf81664f7c4800cec6c9ae3ffea35483303d24d3cc4bcc2c01e99e98df042eb
|
|
| MD5 |
4019d5473dcd2fe79a8467d6037cd02a
|
|
| BLAKE2b-256 |
7347c8a9056446e08374fdf37e9c13a8dfb7e3ecf4ba8275eab15b365dd9e954
|
Provenance
The following attestation bundles were made for duckdb_nb_export-0.1.0.tar.gz:
Publisher:
publish.yml on b-trout/duckdb-nb-export
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
duckdb_nb_export-0.1.0.tar.gz -
Subject digest:
daf81664f7c4800cec6c9ae3ffea35483303d24d3cc4bcc2c01e99e98df042eb - Sigstore transparency entry: 2091346749
- Sigstore integration time:
-
Permalink:
b-trout/duckdb-nb-export@b37ee341d39db5e9e3c993bf68ff90946b618826 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/b-trout
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b37ee341d39db5e9e3c993bf68ff90946b618826 -
Trigger Event:
push
-
Statement type:
File details
Details for the file duckdb_nb_export-0.1.0-py3-none-any.whl.
File metadata
- Download URL: duckdb_nb_export-0.1.0-py3-none-any.whl
- Upload date:
- Size: 51.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ed9e621cadf1e278670456af086ffcef4a05563c41a293814d03d89d282157d3
|
|
| MD5 |
08cf16496940d25207eeff90ae7e89dd
|
|
| BLAKE2b-256 |
482618e87e4803c811806805145b3c70defb796e95503768824792ae7a0d6e79
|
Provenance
The following attestation bundles were made for duckdb_nb_export-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on b-trout/duckdb-nb-export
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
duckdb_nb_export-0.1.0-py3-none-any.whl -
Subject digest:
ed9e621cadf1e278670456af086ffcef4a05563c41a293814d03d89d282157d3 - Sigstore transparency entry: 2091346980
- Sigstore integration time:
-
Permalink:
b-trout/duckdb-nb-export@b37ee341d39db5e9e3c993bf68ff90946b618826 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/b-trout
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b37ee341d39db5e9e3c993bf68ff90946b618826 -
Trigger Event:
push
-
Statement type: