Metadata-Version: 2.3
Name: juv
Version: 0.2.1
Summary: A little wrapper around `uv` to launch ephemeral Jupyter notebooks.
Author-email: Trevor Manz <trevor.j.manz@gmail.com>
License-File: LICENSE
Requires-Python: >=3.10
Requires-Dist: click>=8.1.7
Requires-Dist: jupytext>=1.16.4
Requires-Dist: rich>=13.9.2
Description-Content-Type: text/markdown

# juv

A toolkit for reproducible Jupyter notebooks, powered by [uv](https://docs.astral.sh/uv/).

## Features

- 🗂️ Create, manage, and run reproducible notebooks
- 📌 Pin dependencies with [PEP 723 - inline script metadata](https://peps.python.org/pep-0723)
- 🚀 Launch ephemeral sessions for multiple front ends (e.g., JupyterLab, Notebook, NbClassic)
- ⚡ Powered by [uv](https://docs.astral.sh/uv/) for fast dependency management

## Installation

**juv** requires that you have uv v0.4 or later installed. 

You can install the `juv` cli globally:

```sh
uv tool install juv
```

or use the [`uvx`](https://docs.astral.sh/uv/guides/tools/) command to invoke
it without installing:

```sh
uvx juv
```

## Usage

**juv** should feel familar for `uv` users. The goal is to extend its
dependencies management to Jupyter notebooks.

```sh
# create a notebook
juv init notebook.ipynb
juv init --python=3.9 notebook.ipynb # specify a minimum Python version

# add dependencies to the notebook
juv add notebook.ipynb pandas numpy
juv add notebook.ipynb --requirements=requirements.txt

# launch the notebook
juv run notebook.ipynb
juv run --with=polars notebook.ipynb # additional dependencies for this session (not saved)
juv run --jupyter=notebook@6.4.0 notebook.ipynb # pick a specific Jupyter frontend

# JUV_JUPYTER env var to set preferred Jupyter frontend (default: lab)
export JUV_JUPYTER=nbclassic
juv run notebook.ipynb
```

If a script is provided to `run`, it will be converted to a notebook before
launching the Jupyter session.

```sh
uvx juv run script.py
# Converted script to notebook `script.ipynb`
# Launching Jupyter session...
```

## Motivation

_Rethinking the "getting started" guide for notebooks_

Jupyter notebooks are the de facto standard for data science, yet they suffer
from a [reproducibility
crisis](https://leomurta.github.io/papers/pimentel2019a.pdf).

I believe this reproducibility challenge is a clear example of how our tools
can shape our practices, not some fundamental lack of care for reproducibility.
In this case, our tools fail to support best practices, in part because 
dependency management is **hard**. Notebooks are much like one-off Python
scripts and most often are not a part of a package.

Being a "good steward" of notebooks in this context requires discipline (due to
the manual nature of virtual environments) and knowledge of Python packaging -
an unreasonable expectation for domain experts who just need to get their work
done.

So, generally you might find a "getting started" guide like:

```sh
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt # or just pip install pandas numpy, etc
jupyter lab
```

Four lines of code, where a few things can go wrong. _What version of Python?_
_What package version(s)?_ _What if we forget to activate the virtual
environment?_

In my opinion, the gold standard for a "getting started" guide should be a
**single command** (i.e, no guide).

```sh
<magic tool> run notebook.ipynb
```

However, this gold standard has long been out of reach for Jupyter notebooks. Why?

First, **virtual environments are a leaky abstraction**, and one deeply
ingrained in the Python psyche: _create_, _activate_, _install_, _run_. Their
historical "cost" has forced us to treat them as entities that must be managed
explicitly. In fact, an entire ecosystem of tooling and best practices are
oriented around supporting long-lived environments, rather than assumming
something more ephemeral. End users separately _create_ and then _mutate_
virtual environments with low-level tools like `pip`. The manual nature and
overhead of these steps encourages sharing environments across projects - a
poor practice for reproducibility.

Second, **only Python packages could historically specify their dependencies**.
Lots of data science code lives in notebooks, not packages, and there has not
been a way to specify dependencies for standalone scripts without external
files (e.g., `requirements.txt`).

*Aligning of the stars*

Two key ideas have changed my perspective on this problem and inspired **juv**:

- **Virtual environments are now "cheap"**. If you'd asked me a year ago, I
would have said virtual environments were a necessary evil.
[uv](https://peps.python.org/pep-0723/) is such a departure from the status quo
that it forces us to rethink best practices. Environments are now created
faster than JupyterLab starts - why keep them around at all?

- **PEP 723**. [Inline script metadata](https://peps.python.org/pep-0723/)
introduces a standard way to specify dependencies in standalone Python scripts.
A single file can now contain everything needed to run it, without relying on
external files like `requirements.txt` or `pyproject.toml`. 

So, what if:

- _Environments were disposable by default?_
- _Notebooks could specify their own dependencies?_

This is the vision of **juv**

> [!NOTE]
> Dependency management is just one reproducibility challenge in notebooks
> (non-linear execution being another). **juv** aims to solve this specific
> pain point for the existing ecosystem. I'm personally excited for initiatives
> that [rethink notebooks](https://marimo.io/blog/lessons-learned) from the
> ground up and make this kind of tool obsolete.

## How

[PEP 723 (inline script metadata)](https://peps.python.org/pep-0723) allows
specifying dependencies as comments within Python scripts, enabling
self-contained, reproducible execution. This feature could significantly
improve reproducibility in the data science ecosystem, since many analyses are
shared as standalone code (not packages). However, _a lot_ of data science code
lives in notebooks (`.ipynb` files), not Python scripts (`.py` files).

**juv** bridges this gap by:

- Extending PEP 723-style metadata support from `uv` to Jupyter notebooks
- Launching Jupyter sessions for various notebook front ends (e.g., JupyterLab, Notebook, NbClassic) with the specified dependencies

It's a simple Python script that parses the notebook and starts a Jupyter
session with the specified dependencies (piggybacking on `uv`'s existing
functionality).

## Alternatives

`juv` is opinionated and might not suit your preferences. That's ok! `uv` is
super extensible, and I recommend reading the wonderful
[documentation](https://docs.astral.sh/uv) to learn about its primitives.

For example, you can achieve a similar workflow using the `--with-requirements`
flag:

```sh
uvx --with-requirements=requirements.txt --from=jupyter-core --with=jupyterlab jupyter lab notebook.ipynb
```

While slightly more verbose and breaking self-containment, this approach
totally works and saves you from installing another dependency.
