API Reference
juplit
juplit.tasks
Core notebook workflow tasks for juplit.
These functions back both the poe task targets and the juplit CLI commands.
They can also be imported and called directly from Python.
sync_notebooks()
Sync .py and .ipynb files for all paired percent-format notebooks.
Walks the configured notebook_src_dirs (from [tool.juplit] in
pyproject.toml) and calls jupytext --sync on every .py file that has
a jupytext percent-format header pairing it with an .ipynb.
Prints a summary that splits synced files by direction (py → ipynb vs
ipynb → py), alongside unchanged and skipped files. Also flags any
conflict — a file whose .py and .ipynb both changed since the last
sync, where jupytext keeps the newer by mtime and overwrites the other.
Protects a .py that has edits since the last sync from being silently
overwritten by a newer (mtime) .ipynb: such files are run with jupytext's
--check-source-is-newer guard, so instead of clobbering the .py they are
left untouched and reported as overwrite blocked. Files whose .py is
unchanged since the last sync still sync both ways as usual.
Raises SystemExit(1) if jupytext reports any errors, or if any file was
overwrite-blocked.
Source code in juplit/tasks.py
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 | |
generate_notebooks()
Generate .ipynb files from .py percent-format files.
Calls jupytext --to notebook on every paired .py file found in the
configured notebook_src_dirs. Use this after cloning a repo where only
the .py sources are committed.
Prints a summary of created/updated, unchanged, and skipped files.
Raises SystemExit(1) if jupytext reports any errors.
Source code in juplit/tasks.py
clean_notebooks(force=False)
Sync then delete .ipynb files, keeping artifact notebooks unless force.
First calls sync_notebooks() to flush any unsaved changes from the
.ipynb files back into their paired .py sources, then removes every
.ipynb found under notebook_src_dirs. Keeps the working directory
clean for AI agents and CI environments that only need the .py sources.
An artifact notebook is the deliverable, not a build product, so it is kept and
counted; force=True deletes it too.
Prints a summary of removed and kept files.
Source code in juplit/tasks.py
juplit.testing
test() helper — separates exportable logic from inline tests.
test()
True when the calling module is run directly (__main__), or under a pytest run
for a module inside that run's own [tool.juplit] notebook_src_dirs.
Use this to gate test code in percent-format notebook files so that tests run interactively (in Jupyter) and under pytest, but never on import — and never for an installed package that happens to be imported by someone else's pytest run.
Example::
# %%
from juplit import test
# %%
def add(a, b):
return a + b
# %%
if test():
assert add(1, 2) == 3
Source code in juplit/testing.py
Artifact notebooks
juplit.artifacts
Artifact notebooks — pairs whose .ipynb is committed with its outputs.
juplit's default model is "the .py is the source of truth, the .ipynb is a
disposable local artifact". That is wrong for one case: a notebook whose executed
outputs are the deliverable. A project opts those pairs in by path pattern::
[tool.juplit]
artifact_notebooks = ["experiments/**/*.py"]
For a declared pair, no juplit command destroys the outputs. Committed does not mean untouched, though: every write strips the running state of the run that produced the outputs — execution counts, per-run timestamps, widget state, carriage-return progress bars — so re-running and getting the same results produces no diff.
is_artifact(py_file)
True if this paired .py is declared an artifact notebook.
artifact_py_files()
Every declared artifact .py, including ones outside notebook_src_dirs.
The globs are additive to the scanned set, so experiments/**/*.py need not also be
listed in notebook_src_dirs. Discovery of ordinary pairs is unchanged.
Source code in juplit/artifacts.py
cell_state(cell)
Provenance verdict for one cell.
empty — no outputs; nothing to vouch for. clean — stamped, and the stamp matches the current source. stale — stamped, and the stamp disagrees: the outputs describe older source. unverified — has outputs but no stamp: executed outside juplit (a human in Jupyter).
Source code in juplit/artifacts.py
scan(ipynb)
Group an artifact notebook's cell indices by state. Pure read, never writes.
Source code in juplit/artifacts.py
stamp(py_file, cells=None, force=False)
Bless outputs juplit did not produce: stamp unverified cells as current.
This is the way out of the unverified warning for a human who ran the notebook in
Jupyter. Refuses a stale cell unless force — stamping stale outputs asserts
something already known to be false. Returns the indices stamped.
Source code in juplit/artifacts.py
normalize_notebook(py_file)
Scrub a committed artifact on disk and report what it costs.
The file-level counterpart of normalize, which works on an in-memory notebook.
Oversized outputs are reported only — never downscaled, never spilled to separate
files: a spilled image renders as nothing in GitHub's notebook viewer, which is the
one surface where reviewers actually read these notebooks.
Source code in juplit/artifacts.py
check_artifacts(strict=False)
The pre-commit / CI guard. Reads committed files only — no kernel, no sidecar.
Fails on: a stale cell, a missing metadata filter, a declared artifact with no
committed .ipynb, a gitignored artifact, and a notebook or single output over
budget. Warns on unverified cells; strict promotes that warning to a failure.
Raises SystemExit(1) when anything failed.
Source code in juplit/artifacts.py
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 | |
add_cell(py_file, source, outputs, index=None, cell_type='code')
Insert one cell into the .py and its captured outputs into the paired .ipynb.
Nothing is re-executed: the outputs written are the ones the caller already looked
at. Both halves are built from the same in-memory insert, so they cannot disagree
about ordering. Returns the index the cell landed at; index=None appends.
Source code in juplit/artifacts.py
run_cells(py_file, cells=None, stale_only=False, all_cells=False, name='default', timeout=300.0)
Execute selected cells and SAVE their outputs, freshly stamped, into the .ipynb.
Exactly one selector, and one is required: there is no default because the plausible
default — re-running everything — is the expensive one. all_cells is the clean
build: the kernel is restarted first so the run starts from nothing.
The restart does not relocate the kernel. A kernel started with --cwd comes back
in that directory, so a notebook's relative paths mean the same thing under --all as
under --cells. With no session on disk to read, the new kernel lands at the repo
root — as it always did — and "fell_back_to" names that directory so a relocated run
is visible rather than silent.
Returns {"executed": [...], "failed": [...], "fell_back_to": str | None}; a cell whose output is an error counts as failed but is still written, because that error is what the notebook now shows.
Source code in juplit/artifacts.py
juplit.kernel
A Jupyter kernel that outlives the CLI invocation that started it.
Agents call a CLI once per tool use, so "try a cell, look at the result, try again"
needs a kernel that survives between calls. There is no Jupyter server here: juplit
launches the kernelspec itself and records the connection in .juplit/kernels/, so this
works on a login node, in CI and in an ephemeral sandbox.
Two things about the launch are load-bearing, both verified:
- The kernelspec's
argv[0]is a bare"python", which inside a virtualenv resolves to the system interpreter — no ipykernel, kernel dead at launch. It has to be rewritten tosys.executable, the same substitutionjupyter_clientmakes internally. jupyter_clientsetsJPY_PARENT_PIDto the launching process, and ipykernel then kills the kernel when that process exits. Overriding it to1is what makes the kernel detached; without it the kernel does not survive to the first re-attach.
start(name=DEFAULT_NAME, kernel='python3', cwd=None)
Launch a kernel that outlives this process. Reuses a live one of the same name.
Source code in juplit/kernel.py
stop(name=DEFAULT_NAME)
Shut a kernel down and forget it. True if there was a session to stop.
We did not spawn it in this process, so there is no KernelManager to ask: the pid in the session file and the control channel are all we have.
Source code in juplit/kernel.py
status()
One row per recorded session: name, pid, cwd, kernel, alive.
Source code in juplit/kernel.py
execute(code, name=DEFAULT_NAME, timeout=DEFAULT_TIMEOUT)
Run code on the session kernel and return its outputs. Touches no file.
A failed attempt therefore exists only in the kernel's history and in this return value — never in the notebook.
Source code in juplit/kernel.py
juplit.view
Reading a notebook without paying for it.
A plot-heavy .ipynb is megabytes of base64. An agent that reads one into context has
spent its budget on data it cannot interpret. These renderers never emit a payload: an
image output becomes image/png 640x480 82KB, a 10 000-line stream becomes its first
and last lines, and only text is ever shown in full.
cells_table(ipynb)
The index: one line per cell, headings and digests, never any source or base64.
[00] md 6L # Ablation study
[05] md 2L ## Results
[06] code 12L clean out: stream 1.2KB 14L 'Fitting fold 3…'
[07] code 4L STALE out: text/plain 3L '[1200 rows x 8 columns]'
Every column left of the digest is fixed-width, and the digest — the one field with
no common structure — comes last, so clean / STALE land in the same place on
every line instead of after a string whose length depends on the output.
Source code in juplit/view.py
view_cells(py_file, cells=None, full=False)
Source of the named cells, each followed by its outputs. No range means all.
Both halves are introduced by the same banner rule::
── [03] code ───────────────────────────────────────────────────
for arm, scores in ARMS.items():
print(f"{arm:<10} mean {sum(scores) / len(scores):.2f}")
── [03] output ─────────────────────────────────────────────────
baseline mean 0.41
Source comes from the .py — the source of truth — and outputs from the paired
.ipynb, so this works on an ordinary pair with no notebook on disk: it prints the
source and says the notebook has not been generated.
Source code in juplit/view.py
output_digest(output)
A one-line, base64-free summary of a single output.