From 1edf9c57020be416e67697b91799f55231e83be6 Mon Sep 17 00:00:00 2001 From: patnr Date: Tue, 15 Sep 2026 09:52:58 +0200 Subject: [PATCH 1/4] Colab-first deps; vendor mpl-tools helpers; no build backend Dependencies: - pyproject.toml documents why Colab-preinstalled packages are unconstrained and where reproducibility comes from (uv.lock locally). - colab_bootstrap.sh installs requirements-colab.txt with --no-deps: only what Colab lacks, never touching preinstalled (already imported) packages, so no runtime restart. Also no more PEP 517 build just to read a dependency list. - Runtime deps now list what the notebooks import; jupyter moved to dev. - Dropped jupyterlab~=3.6, ipympl, jupyter_nbextensions_configurator and the jupyter-server<2 pin they forced. Dev frontend is now notebook 7. - No [build-system]; [tool.uv] package = false. Vendoring: - freshfig, nRowCol and is_notebook (was mpl_tools.is_notebook_or_qt) were the only things used from mpl-tools; rewritten in tools/plotting.py using only stable, public matplotlib/IPython API. mpl-tools kept breaking on Colab's matplotlib upgrades via its use of internals. Co-Authored-By: Claude Fable 5.1 --- .pre-commit-config.yaml | 2 +- colab_bootstrap.sh | 60 ++++-- notebooks/tools/geostat.py | 3 +- notebooks/tools/plotting.py | 176 +++++++++-------- pyproject.toml | 94 ++++++--- requirements-colab.txt | 23 +++ uv.lock | 385 +++++++++++------------------------- 7 files changed, 343 insertions(+), 400 deletions(-) mode change 100644 => 100755 colab_bootstrap.sh create mode 100644 requirements-colab.txt diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 077520d..a2b39f9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,7 @@ repos: - tags - repo: https://github.com/mwouts/jupytext - rev: v1.15.1 # ensure equal to main venv + rev: v1.15.2 # keep equal to the `jupytext` pin in pyproject.toml hooks: - id: jupytext args: [--sync] diff --git a/colab_bootstrap.sh b/colab_bootstrap.sh old mode 100644 new mode 100755 index a49422d..4a77ea0 --- a/colab_bootstrap.sh +++ b/colab_bootstrap.sh @@ -1,42 +1,60 @@ #!/usr/bin/env bash -# Colab doesn't provide -# - Auto-installing dependencies (neither pyproject.toml nor requirements.txt) -# - Pre-loading data/modules (aside from the notebook itself) -# This script takes care of the above by cloning the full (shallow) repo. +# Bootstrap for Google Colab, invoked from the first code cell of each notebook: +# +# !wget -qO- https://raw.githubusercontent.com/patnr/HistoryMatching/master/colab_bootstrap.sh | bash -s +# +# Colab does not auto-install a project's dependencies (no pyproject/requirements +# support), nor does it fetch anything but the notebook itself. So this script +# 1. shallow-clones the repo (or copies it from `--repo=PATH`, for tests), +# 2. installs ONLY the packages Colab lacks, with `--no-deps`, so that nothing +# preinstalled (and already imported) gets touched, which would otherwise +# require a runtime restart. See requirements-colab.txt and pyproject.toml. +# 3. copies the notebooks dir (incl. `tools/`) into the working directory. +# +# Options: `--debug`/`-v` for verbose output; `--branch=NAME` (default: master). main () { set -e - # Clear any existing REPO for a fresh git clone + # Clear any existing REPO for a fresh copy rm -rf REPO - # Download repo - URL=https://github.com/patnr/HistoryMatching.git - if [[ ! -d REPO ]]; then git clone --depth=1 $URL REPO; fi - - # https://pythonspeed.com/articles/upgrade-pip/ - pip install --upgrade pip + if [[ -n "$SRC" ]]; then + # Local checkout (used by tests/colab/smoke.sh) + mkdir REPO + tar -C "$SRC" --exclude=.git -cf - . | tar -C REPO -xf - + else + git clone --depth=1 --branch "$BRANCH" https://github.com/patnr/HistoryMatching.git REPO + fi - # Install dependencies (declared in REPO/pyproject.toml) - pip install ./REPO + # Install what Colab lacks. NB: no pip upgrade, no dependency resolution. + pip install --no-deps -r REPO/requirements-colab.txt - # Put repo contents (including hidden files) in PWD + # Put notebooks (including hidden files) in PWD shopt -s dotglob cp -r REPO/notebooks/* ./ } -# Only run if we're on colab -if python -c "import google.colab" 2>/dev/null; then - - # Use `bash -s -- --debug` to get verbose output - if echo $@ | grep -E -- '(--debug|-v)' > /dev/null ; then +# Parse args +SRC="" +BRANCH="master" +DEBUG="" +for arg in "$@"; do + case "$arg" in + --repo=*) SRC="${arg#--repo=}" ;; + --branch=*) BRANCH="${arg#--branch=}" ;; + --debug|-v) DEBUG=1 ;; + esac +done + +# Only run if we're on Colab (the env var is also set in Colab's runtime image) +if [[ -n "$COLAB_RELEASE_TAG" ]] || python -c "import google.colab" 2>/dev/null; then + if [[ -n "$DEBUG" ]]; then main else - # Quiet main > /dev/null 2>&1 fi - echo "Initialization for Colab done." else echo "Not running on Colab => Didn't do anything." diff --git a/notebooks/tools/geostat.py b/notebooks/tools/geostat.py index 0d2d576..8b5094e 100644 --- a/notebooks/tools/geostat.py +++ b/notebooks/tools/geostat.py @@ -3,9 +3,10 @@ import numpy as np import scipy.linalg as sla from matplotlib import pyplot as plt -from mpl_tools.misc import nRowCol from numpy.random import randn +from tools.plotting import nRowCol + def variogram_gauss(xx, r, n=0, a=1 / 3): """Compute the Gaussian variogram for the 1D points xx. diff --git a/notebooks/tools/plotting.py b/notebooks/tools/plotting.py index ad50cfb..ba9f875 100644 --- a/notebooks/tools/plotting.py +++ b/notebooks/tools/plotting.py @@ -2,18 +2,17 @@ import copy import warnings +from math import sqrt import ipywidgets as wg import matplotlib as mpl -import mpl_tools -import mpl_tools.place import numpy as np import struct_tools +from IPython import get_ipython from IPython.display import display from matplotlib import pyplot as plt from matplotlib.gridspec import GridSpec from matplotlib.ticker import MaxNLocator, LogLocator -from mpl_tools.misc import nRowCol from struct_tools import DotDict as Dict from minires.plotting import styles @@ -58,10 +57,82 @@ from matplotlib.colors import LogNorm, AsinhNorm # noqa # type: ignore -def freshfig(*args, **kwargs): - if not kwargs.get("figsize", None): - kwargs["figsize"] = (8, 4) - return mpl_tools.place.freshfig(*args, **kwargs) +# The following 3 helpers used to come from `mpl-tools` (also by patnr). +# They were vendored (and reduced to what this repo uses) so that the tutorials +# depend only on stable, public matplotlib/IPython API. This matters because +# on Colab the base environment is outside our control (see pyproject.toml). + + +def freshfig(num=None, figsize=None, sup=True, **kwargs): + """Like `plt.subplots(num=num, figsize=figsize, **kwargs)`, but re-using figures. + + If a figure labelled `num` (a descriptive string, please) already exists, + it is *cleared*, not closed and re-opened. Thus its window keeps its + position and size on screen, which is what you want when re-running + the `.py` mirror of a notebook with `%run` in IPython (GUI backend). + + The inline (Jupyter/Colab) backend does not show the figure label, + so there (if `sup`) the label is set as `fig.suptitle` instead. + """ + if figsize is None: + figsize = (8, 4) + fig, ax = plt.subplots(num=num, figsize=figsize, clear=True, **kwargs) + if sup and isinstance(num, str) and "inline" in mpl.get_backend(): + fig.suptitle(num) + return fig, ax + + +def nRowCol(nTotal, figsize=None, axsize=None): + """Compute `(nrows, ncols)` such that `nTotal ≈ nrows*ncols`. + + Takes into account the `figsize` and `axsize`, either of which + may be given as a tuple, or a number (the ratio width/height). + Defaults are taken from `mpl.rcParams`. + + Examples + -------- + >>> nRowCol(4) + {'nrows': 2, 'ncols': 2} + >>> nRowCol(4, (4, 1), (1, 1)) + {'nrows': 1, 'ncols': 4} + >>> nRowCol(4, (4, 1), (4, 1)) + {'nrows': 2, 'ncols': 2} + >>> nRowCol(12, (4, 3), (1, 1)) + {'nrows': 3, 'ncols': 4} + """ + if figsize is None: + figsize = mpl.rcParams["figure.figsize"] + if axsize is None: + rc = {k: mpl.rcParams["figure.subplot." + k] for k in ["bottom", "left", "right", "top"]} + axsize = (rc["right"] - rc["left"], rc["top"] - rc["bottom"]) + + def ratio(size): + try: + w, h = size + return w / h + except TypeError: + return size + + ratio = ratio(figsize) / ratio(axsize) + nrows = round(sqrt(nTotal / ratio)) or 1 + ncols = nTotal // nrows + if nrows * ncols < nTotal: # since `//` rounds down + ncols += 1 + return {"nrows": nrows, "ncols": ncols} + + +def is_notebook(): + """Detect a Jupyter-type frontend (local Jupyter, Colab, VS Code, nbclient, ...). + + Returns `False` in plain Python and in terminal IPython. + Relies only on the shell class, which has been stable for a decade: + `ZMQInteractiveShell` for Jupyter kernels, `google.colab._shell.Shell` on Colab. + """ + ip = get_ipython() + if ip is None: + return False + cls = type(ip) + return cls.__name__ == "ZMQInteractiveShell" or "colab" in cls.__module__ def fields( @@ -151,51 +222,28 @@ def fields( def init(): - """Configure mpl. - - ## On the choice of backend: - - - In scripts, `Qt5Agg` is nice coz it (is interactive and) allows - programmatic (automatic) placement of figures on screen. - - In notebooks `%matplotib notebook` (nbAgg) is interactive, - so cooler than `%matplotlib inline`. - - On my local machine, `%matplotlib inline` always makes figures display - as string:
, despite `plt.show()`, `plt.pause(0.1)`, etc. - - However, `%matplotlib widget/ipympl` ('module://ipympl.backend_nbagg') - is also interactive, and compatible with BOTH jupyter-notebook and -lab. - It may also be selected via `import ipympl`. - - Colab only supports `%matplotlib inline` (but could look into "plotly") - https://stackoverflow.com/a/64297121 - - ## On IPython magics vs `mpl.use()`: - - The magics (like `%matplotlib notebook/ipympl`) set `plt.ion()` and some rcParams. - They could probably be used in a module via `get_ipython().run_line_magic()` - however, here I choose to instead use `mpl.use(...)` or `import ipympl`. - - Obsolete? If you do `import ipympl` BEFORE `import matplotlib.pyplot` then - the figures only display as the text string
. - - If you do mpl.use("nbAgg") BEFORE `import matplotlib.pyplot` then - you must remember to do `plt.ion()` too. - PS: this "interactive" is not to be confused with "interactive backends". - PS: check status with `plt.isinteractive`. - - ## About "run all (cells)": - - On my Mac, the figures sometimes don't display, or are "inline" instead of "nbAgg". - - ## About animation displaying twice: - - The solution seems to be to split the creation and display cells, and using %%capture. - This worked both locally and on Colab. - - [Ref](https://stackoverflow.com/q/47138023) - - [Ref](https://stackoverflow.com/a/36685236) - On my Mac, `%matplotlib inline` did not have this issue, but plenty others (see above). - However, on Colab (i.e. `%matplotlib inline`), the animation still displayed double. + """Configure mpl (backend, rcParams). + + ## Backend + In notebooks, we always use the "inline" backend, which is the default + of Jupyter kernels, and the only one supported by Colab. The interactive + alternatives (`%matplotlib widget`, i.e. `ipympl`, and `nbAgg`) were + dropped: they don't work on Colab, are slower and flicker when embedded + in `ipywidgets` controls, and testing a single backend is simpler. + NB: this means `init()` must not `mpl.use()` anything in notebooks. + + In scripts (e.g. `%run HistoryMatch.py` in IPython), `Qt5Agg` is preferred + since it allows programmatic placement of figure windows. Otherwise + (e.g. on macOS without Qt) the default GUI backend is used, and figure + windows at least keep their placement across re-runs thanks to `freshfig`. + + ## Animations displaying twice + Split the creation and display into separate cells, using `%%capture`. + Refs: , . """ - if mpl_tools.is_notebook_or_qt: + if is_notebook(): mpl.rc("animation", html="jshtml") - # mpl.rcParams["figure.figsize"] = [5, 3.5] # NB: Non-default figsize/fontsize may cause axis labels/titles # that do not fit within the figure, or trespass into the axes # (unless fixed by tight_layout, but that is sometimes not possible) @@ -205,32 +253,6 @@ def init(): mpl.rcParams.update({"legend.fontsize": "large"}) mpl.rcParams["font.size"] = 11 - try: - # Colab - import google.colab # type: ignore # noqa: F401 - # [colab-specific adjustments] - - except ImportError: - # Local Jupyter - - # ipympl is nice but when embedded in ipywidget controls - # - it is a bit slower than inline - # - it flickers (in classic notebook, not jupyter-lab) - # Also it's nice to only have to test for one backend, - # i.e. the one that works on Colab - pass # use "inline" - - # try: - # # Similar to `%matplotlib widget/ipympl` - # # or `mpl.use('module://ipympl.backend_nbagg')` - # # import ipympl # type: ignore - - # except ImportError: - # pass # use "inline" - - # # Similar to `%matplotlib notebook`: - # # mpl.use("nbAgg") # NB: must be installed! - else: # Script run mpl.rcParams.update({"font.size": 10}) @@ -464,8 +486,8 @@ def ens_style(label, N=100): return style -# NOTE: This uses IPython/jupyter widgets. Another solution, using interactive -# mpl backends (=> not available on Colab), can be found in mpl_tools. +# NOTE: This uses IPython/jupyter widgets (works on Colab), rather than +# an interactive mpl backend (not available on Colab). def toggle_items(wrapped): """Include checkboxes/checkmarks to toggle plotted data series on/off.""" diff --git a/pyproject.toml b/pyproject.toml index bdb29a9..ef0df33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,51 +5,85 @@ description = "Tutorial on history matching and production optimisation." readme = "README.md" license = "MIT" authors = [{ name = "Patrick N. Raanes", email = "patrick.n.raanes@gmail.com" }] + +# ── Dependency policy ──────────────────────────────────────────────────────── +# +# The tutorials must run with a single click on Google Colab, whose base +# environment we do not control and which gets upgraded every few weeks. +# That single fact shapes everything below. There are two install targets: +# +# (A) Local (`uv sync`): exact reproducibility comes from `uv.lock`, +# not from pins in this file. Refresh with `uv lock --upgrade`. +# +# (B) Colab (`colab_bootstrap.sh`): installs `requirements-colab.txt` +# with `pip install --no-deps`, i.e. it adds ONLY the packages Colab lacks, +# and never touches a preinstalled one. Reason: numpy/scipy/matplotlib/ +# ipywidgets are already *imported* when a Colab kernel starts, so +# re-installing them (up- or downgrade) requires a runtime restart. +# Hence the constraints below on Colab-preinstalled packages must accept +# whatever Colab ships. In practice this means: no upper bounds here +# (our own `minires` carries `matplotlib<4`), and lower bounds only +# where the code demands them. If Colab's upgrades break us, we fix the +# code, not the pins -- and `tests/colab/` exists to catch that early. +# +# Consequently `pyproject.toml` and `requirements-colab.txt` must be kept in +# sync by hand (they are short). The Colab smoke test fails loudly on drift +# (ImportError for a missing package, or a mutated preinstalled package). +# +# Python: 3.12 (`.python-version`) because that is what Colab runs +# (runtime 2026.08). Newer versions work too (`requires-python` is open), +# but 3.12 is what gets tested. + requires-python = ">=3.12" dependencies = [ - # Pinned to a commit (rather than a release) -- bump as needed. + # Our own toy reservoir simulator. Pinned to a commit until the PyPI release, + # then this becomes `minires>=X.Y` (and `requirements-colab.txt` likewise). + # For local hacking: `uv pip install -e ${HOME}/path/MiniRes` "minires @ git+https://github.com/patnr/MiniRes.git@1b9f8d56010a", - # "Editable" mode: install with `uv pip install -e ${HOME}/path/MiniRes` - "jupyter", - "pathos", + # Preinstalled on Colab => deliberately unconstrained (see policy above). + "numpy", + "scipy", + "matplotlib", + "ipywidgets", + "ipython", "threadpoolctl", - "adjustText==0.8", - # NB: 0.4.0 uses `mpl.rcsetup.interactive_bk`, removed in matplotlib 3.11 - # (and 0.4.1 has a `freshfig` bug) -- so this floor is load-bearing. - "mpl-tools>=0.4.2,<0.5", - # Used by the notebooks; used to arrive transitively, via TPFA-ResSim. + # NOT preinstalled on Colab => also listed in `requirements-colab.txt`. + # `pathos` (multiprocessing with dill pickling) drags in + # dill/multiprocess/pox/ppft, all pure-Python, so also cheap on Colab. + "pathos", + # Zero dependencies, by design (it is our own). "struct-tools", + # Label de-overlapping in `tools/utils.py`. Exact-pinned since it was added + # (2023, when 0.8 was current); the 1.x releases rewrote the algorithm and + # the pin has simply not been re-evaluated against them. Not preinstalled on + # Colab (only depends on numpy+matplotlib), so the exact pin is harmless there. + "adjustText==0.8", ] [dependency-groups] +# Local-only tooling. Never installed on Colab, which has its own frontend. dev = [ - # Extensions for classic notebook - "jupyter_nbextensions_configurator", - "jupyter-server<2.0.0", - # For jupyterlab - # NB: jupyterlab>=4 breaks complex widgets and throws errors in console. - # NB: `.python-version` pins 3.12, since `jupyterlab~=3.6` has no wheels for later versions. - "jupyterlab~=3.6", + # Notebook frontend (v7, i.e. built on JupyterLab 4). Classic notebook (v6) + # and `jupyterlab~=3.6` were dropped: they forced `jupyter-server<2` and + # Python 3.12, and doubled the surface to test. `ipympl` was also dropped: + # all notebooks use the inline backend (the only one that works on Colab). + "notebook", + # Executes notebooks headlessly (`tests/colab/run_nb.py`). + "nbclient", + # Keep in sync with the `jupytext` rev in `.pre-commit-config.yaml`. "jupytext~=1.15.2", - "ipympl~=0.9.3", - "ipython", "ipdb", "pre-commit", "ruff~=0.5.0", ] -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -# Needed for the `minires @ git+...` pin above. -[tool.hatch.metadata] -allow-direct-references = true - -# This "project" is just a dependency set (the notebooks are run in-place, -# not imported), so the wheel ships no files. -[tool.hatch.build.targets.wheel] -bypass-selection = true +# Not a package: nothing is imported from this project, the notebooks are run +# in place. Hence no `[build-system]` (a PEP 517 build would only serve to +# read the dependency list). Install with `uv sync`, or, for pip users, +# `pip install -r requirements-colab.txt` is NOT the way (it is Colab-specific); +# use `uv pip install -r pyproject.toml` or just `pip install uv && uv sync`. +[tool.uv] +package = false [tool.ruff] line-length = 100 diff --git a/requirements-colab.txt b/requirements-colab.txt new file mode 100644 index 0000000..ba1a9c0 --- /dev/null +++ b/requirements-colab.txt @@ -0,0 +1,23 @@ +# Packages that Google Colab does NOT preinstall, and that the notebooks need. +# +# Installed by `colab_bootstrap.sh` with `pip install --no-deps -r `. +# `--no-deps` guarantees that pip never up/downgrades a package Colab already +# ships (numpy, scipy, matplotlib, ipywidgets, tqdm, ...): those are already +# imported when the kernel starts, so changing them would require a runtime +# restart. The price is that transitive dependencies must be listed here by +# hand. Drift shows up as an ImportError, which `tests/colab/smoke.sh` catches. +# +# Keep in sync with `pyproject.toml` (the policy is documented there). +# Colab's current package list: https://github.com/googlecolab/backend-info + +# Pin must equal the one in pyproject.toml (until the PyPI release of minires). +minires @ git+https://github.com/patnr/MiniRes.git@1b9f8d56010a +struct-tools +adjustText==0.8 +# pathos and its (pure-Python) dependencies. Colab ships dill and multiprocess +# already; they are listed anyway so that this file does not depend on that. +pathos +dill +multiprocess +pox +ppft diff --git a/uv.lock b/uv.lock index e3b474c..36d3704 100644 --- a/uv.lock +++ b/uv.lock @@ -19,35 +19,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/36/ac4a2dfefe115be67403bb8c622edf5b6adacbb3afd2ac921af37138ec1c/adjustText-0.8-py3-none-any.whl", hash = "sha256:6da99f9d739c496f79346753a3fcadb6354d8d09699cde112fed46cfb837d271", size = 9132, upload-time = "2023-02-17T10:54:26.116Z" }, ] -[[package]] -name = "aiofiles" -version = "22.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/86/26/6e5060a159a6131c430e8a01ec8327405a19a449a506224b394e36f2ebc9/aiofiles-22.1.0.tar.gz", hash = "sha256:9107f1ca0b2a5553987a94a3c9959fe5b491fdf731389aa5b7b1bd0733e32de6", size = 14669, upload-time = "2022-09-04T17:09:21.97Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/48/d5d1ab7cfe46e573c3694fa1365442a7d7cadc3abb03d8507e58a3755bb2/aiofiles-22.1.0-py3-none-any.whl", hash = "sha256:1142fa8e80dbae46bb6339573ad4c8c0841358f79c6eb50a493dceca14621bad", size = 14171, upload-time = "2022-09-04T17:09:19.625Z" }, -] - -[[package]] -name = "aiosqlite" -version = "0.22.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/8a/64761f4005f17809769d23e518d915db74e6310474e733e3593cfc854ef1/aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650", size = 14821, upload-time = "2025-12-23T19:25:43.997Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/00/b7/e3bf5133d697a08128598c8d0abc5e16377b51465a33756de24fa7dee953/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb", size = 17405, upload-time = "2025-12-23T19:25:42.139Z" }, -] - [[package]] name = "anyio" -version = "3.7.1" +version = "4.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, - { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/99/2dfd53fd55ce9838e6ff2d4dac20ce58263798bd1a0dbe18b3a9af3fcfce/anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780", size = 142927, upload-time = "2023-07-05T16:45:02.294Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/24/44299477fe7dcc9cb58d0a57d5a7588d6af2ff403fdd2d47a246c91a3246/anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5", size = 80896, upload-time = "2023-07-05T16:44:59.805Z" }, + { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, ] [[package]] @@ -136,6 +118,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d4/2b/04b8a15f3a1c77bc79ddf5c73875327f34b4fa75982df2b76e45e402d364/asttokens-3.0.2-py3-none-any.whl", hash = "sha256:9da13157f5b28becde0bd374fc677dcd3c290614264eff096f167c469cd9f933", size = 28702, upload-time = "2026-07-12T03:31:47.542Z" }, ] +[[package]] +name = "async-lru" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/1f/989ecfef8e64109a489fff357450cb73fa73a865a92bd8c272170a6922c2/async_lru-2.3.0.tar.gz", hash = "sha256:89bdb258a0140d7313cf8f4031d816a042202faa61d0ab310a0a538baa1c24b6", size = 16332, upload-time = "2026-03-19T01:04:32.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/e2/c2e3abf398f80732e58b03be77bde9022550d221dd8781bf586bd4d97cc1/async_lru-2.3.0-py3-none-any.whl", hash = "sha256:eea27b01841909316f2cc739807acea1c623df2be8c5cfad7583286397bb8315", size = 8403, upload-time = "2026-03-19T01:04:30.883Z" }, +] + [[package]] name = "attrs" version = "26.1.0" @@ -675,16 +666,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" }, ] +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + [[package]] name = "history-matching-tutorial" version = "0.1.0" -source = { editable = "." } +source = { virtual = "." } dependencies = [ { name = "adjusttext" }, - { name = "jupyter" }, + { name = "ipython" }, + { name = "ipywidgets" }, + { name = "matplotlib" }, { name = "minires" }, - { name = "mpl-tools" }, + { name = "numpy" }, { name = "pathos" }, + { name = "scipy" }, { name = "struct-tools" }, { name = "threadpoolctl" }, ] @@ -692,12 +695,9 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "ipdb" }, - { name = "ipympl" }, - { name = "ipython" }, - { name = "jupyter-nbextensions-configurator" }, - { name = "jupyter-server" }, - { name = "jupyterlab" }, { name = "jupytext" }, + { name = "nbclient" }, + { name = "notebook" }, { name = "pre-commit" }, { name = "ruff" }, ] @@ -705,10 +705,13 @@ dev = [ [package.metadata] requires-dist = [ { name = "adjusttext", specifier = "==0.8" }, - { name = "jupyter" }, + { name = "ipython" }, + { name = "ipywidgets" }, + { name = "matplotlib" }, { name = "minires", git = "https://github.com/patnr/MiniRes.git?rev=1b9f8d56010a" }, - { name = "mpl-tools", specifier = ">=0.4.2,<0.5" }, + { name = "numpy" }, { name = "pathos" }, + { name = "scipy" }, { name = "struct-tools" }, { name = "threadpoolctl" }, ] @@ -716,16 +719,41 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "ipdb" }, - { name = "ipympl", specifier = "~=0.9.3" }, - { name = "ipython" }, - { name = "jupyter-nbextensions-configurator" }, - { name = "jupyter-server", specifier = "<2.0.0" }, - { name = "jupyterlab", specifier = "~=3.6" }, { name = "jupytext", specifier = "~=1.15.2" }, + { name = "nbclient" }, + { name = "notebook" }, { name = "pre-commit" }, { name = "ruff", specifier = "~=0.5.0" }, ] +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + [[package]] name = "identify" version = "2.6.19" @@ -781,23 +809,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl", hash = "sha256:897eb64da762549ef610698fca5e9675195ec6ac8ec7f19d81ce1ca20c876057", size = 120583, upload-time = "2026-06-10T08:41:23.648Z" }, ] -[[package]] -name = "ipympl" -version = "0.9.8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ipython" }, - { name = "ipywidgets" }, - { name = "matplotlib" }, - { name = "numpy" }, - { name = "pillow" }, - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c4/8c/f9e60abf409cef8234e66e69ce3fe263f1236b285f9105ea125e4660b77a/ipympl-0.9.8.tar.gz", hash = "sha256:6d7230d518384521093f3854f7db89d069dcd9c28a935b371e9c9f126354dee1", size = 58483988, upload-time = "2025-10-09T14:20:07.741Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/6e/9148bfed8ca535e4c61ce7843327c76ec7c63c40e33848ec03aa844a26af/ipympl-0.9.8-py3-none-any.whl", hash = "sha256:4a03612f77d92c9e2160c9e0d2a80b277e30387126399088f780dba9622247be", size = 515832, upload-time = "2025-10-09T14:20:05.39Z" }, -] - [[package]] name = "ipython" version = "9.17.1" @@ -819,15 +830,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2d/1e/65b59cf518c106aa755e7f7da3099027738687a862ec785060702a481320/ipython-9.17.1-py3-none-any.whl", hash = "sha256:6d1645743cfd1a07eb695d85aa2b5fa66721f8cbae9431d4049f7084bbf06509", size = 639038, upload-time = "2026-09-01T08:29:30.673Z" }, ] -[[package]] -name = "ipython-genutils" -version = "0.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/69/fbeffffc05236398ebfcfb512b6d2511c622871dca1746361006da310399/ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8", size = 22208, upload-time = "2017-03-13T22:12:26.393Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/bc/9bd3b5c2b4774d5f33b2d544f1460be9df7df2fe42f352135381c347c69a/ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", size = 26343, upload-time = "2017-03-13T22:12:25.412Z" }, -] - [[package]] name = "ipython-pygments-lexers" version = "1.1.1" @@ -951,20 +953,16 @@ wheels = [ ] [[package]] -name = "jupyter" -version = "1.1.1" +name = "jupyter-builder" +version = "1.2.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ipykernel" }, - { name = "ipywidgets" }, - { name = "jupyter-console" }, - { name = "jupyterlab" }, - { name = "nbconvert" }, - { name = "notebook" }, + { name = "jupyter-core" }, + { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/f3/af28ea964ab8bc1e472dba2e82627d36d470c51f5cd38c37502eeffaa25e/jupyter-1.1.1.tar.gz", hash = "sha256:d55467bceabdea49d7e3624af7e33d59c37fff53ed3a350e1ac957bed731de7a", size = 5714959, upload-time = "2024-08-30T07:15:48.299Z" } +sdist = { url = "https://files.pythonhosted.org/packages/75/3e/56f593e6a664cd14d441724654ce8243f3cac7d3e45867cf084749c8bc75/jupyter_builder-1.2.3.tar.gz", hash = "sha256:01aba6794eb9b19e0e29ae21137ca60ba4135c70347d4b9f664a586822b8c809", size = 1024218, upload-time = "2026-09-04T18:54:09.411Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/64/285f20a31679bf547b75602702f7800e74dbabae36ef324f716c02804753/jupyter-1.1.1-py2.py3-none-any.whl", hash = "sha256:7a59533c22af65439b24bbe60373a4e95af8f16ac65a6c00820ad378e3f7cc83", size = 2657, upload-time = "2024-08-30T07:15:47.045Z" }, + { url = "https://files.pythonhosted.org/packages/84/aa/be79e87c50698673f196d0633fc1664607da2289f9688d1de7a1c9db9e71/jupyter_builder-1.2.3-py3-none-any.whl", hash = "sha256:c5ea5a7190c2a7b082494abade98eece1b2b5bd5dbd7d610606cbcd10a1d08b3", size = 947541, upload-time = "2026-09-04T18:54:07.644Z" }, ] [[package]] @@ -984,38 +982,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0f/88/7c548de1f6c2ade7c931a3282da73f9274fa6a1531091be682f89c85efb9/jupyter_client-8.10.0-py3-none-any.whl", hash = "sha256:5f73f24f22fa25192cfff6b23c051932a2473a797b05734aff495b392103e14e", size = 110184, upload-time = "2026-08-28T12:17:09.028Z" }, ] -[[package]] -name = "jupyter-console" -version = "6.6.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ipykernel" }, - { name = "ipython" }, - { name = "jupyter-client" }, - { name = "jupyter-core" }, - { name = "prompt-toolkit" }, - { name = "pygments" }, - { name = "pyzmq" }, - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bd/2d/e2fd31e2fc41c14e2bcb6c976ab732597e907523f6b2420305f9fc7fdbdb/jupyter_console-6.6.3.tar.gz", hash = "sha256:566a4bf31c87adbfadf22cdf846e3069b59a71ed5da71d6ba4d8aaad14a53539", size = 34363, upload-time = "2023-03-06T14:13:31.02Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/77/71d78d58f15c22db16328a476426f7ac4a60d3a5a7ba3b9627ee2f7903d4/jupyter_console-6.6.3-py3-none-any.whl", hash = "sha256:309d33409fcc92ffdad25f0bcdf9a4a9daa61b6f341177570fdac03de5352485", size = 24510, upload-time = "2023-03-06T14:13:28.229Z" }, -] - -[[package]] -name = "jupyter-contrib-core" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jupyter-core" }, - { name = "notebook" }, - { name = "setuptools" }, - { name = "tornado" }, - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/94/0d37e5b49ea1c8bf204c46f9b0257c1f3319a4ab88acbd401da2cab25e55/jupyter_contrib_core-0.4.2.tar.gz", hash = "sha256:1887212f3ca9d4487d624c0705c20dfdf03d5a0b9ea2557d3aaeeb4c38bdcabb", size = 17490, upload-time = "2022-11-15T16:21:53.357Z" } - [[package]] name = "jupyter-core" version = "5.9.1" @@ -1049,25 +1015,20 @@ wheels = [ ] [[package]] -name = "jupyter-nbextensions-configurator" -version = "0.6.4" +name = "jupyter-lsp" +version = "2.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jupyter-contrib-core" }, - { name = "jupyter-core" }, { name = "jupyter-server" }, - { name = "notebook" }, - { name = "pyyaml" }, - { name = "tornado" }, - { name = "traitlets" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/36/ff/1e4a61f5170a9a1d978f3ac3872449de6c01fc71eaf89657824c878b1549/jupyter_lsp-2.3.1.tar.gz", hash = "sha256:fdf8a4aa7d85813976d6e29e95e6a2c8f752701f926f2715305249a3829805a6", size = 55677, upload-time = "2026-04-02T08:10:06.749Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/fe/cffb14a4fbb43cf276aa3047e42c3f9ecfda851ba3c466295401f6b1e085/jupyter_nbextensions_configurator-0.6.4-py2.py3-none-any.whl", hash = "sha256:fe7a7b0805b5926449692fb077e0e659bab8b27563bc68cba26854532fdf99c7", size = 466890, upload-time = "2024-06-05T16:08:37.236Z" }, + { url = "https://files.pythonhosted.org/packages/23/e8/9d61dcbd1dce8ef418f06befd4ac084b4720429c26b0b1222bc218685eff/jupyter_lsp-2.3.1-py3-none-any.whl", hash = "sha256:71b954d834e85ff3096400554f2eefaf7fe37053036f9a782b0f7c5e42dadb81", size = 77513, upload-time = "2026-04-02T08:10:01.753Z" }, ] [[package]] name = "jupyter-server" -version = "1.24.0" +version = "2.21.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1075,6 +1036,8 @@ dependencies = [ { name = "jinja2" }, { name = "jupyter-client" }, { name = "jupyter-core" }, + { name = "jupyter-events" }, + { name = "jupyter-server-terminals" }, { name = "nbconvert" }, { name = "nbformat" }, { name = "packaging" }, @@ -1087,70 +1050,46 @@ dependencies = [ { name = "traitlets" }, { name = "websocket-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/79/0c/ce06c97c52707bc0fed9461ed5624f1a5ee76dc772d7da2c0699395653af/jupyter_server-1.24.0.tar.gz", hash = "sha256:23368e8e214baf82b313d4c5a0d828ca73015e1a192ce3829bd74e62fab8d046", size = 456590, upload-time = "2023-04-13T19:53:20.991Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/ce/142bcb35ffe215d8880e968689ab733bd7976a6c20dae24b6782cce2219a/jupyter_server-1.24.0-py3-none-any.whl", hash = "sha256:c88ddbe862966ea1aea8c3ccb89a5903abd8fbcfe5cd14090ef549d403332c37", size = 347516, upload-time = "2023-04-13T19:53:18.153Z" }, -] - -[[package]] -name = "jupyter-server-fileid" -version = "0.9.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jupyter-events" }, - { name = "jupyter-server" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0d/eb/7c2c09454bbf66b3727ba8c431d16159d642c0eb1aa179412a4f7af721cf/jupyter_server_fileid-0.9.3.tar.gz", hash = "sha256:521608bb87f606a8637fcbdce2f3d24a8b3cc89d2eef61751cb40e468d4e54be", size = 54959, upload-time = "2024-09-06T07:18:40.412Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/d6/5e5bca083664b1dd368e261107cbe2d350e3bdc62bdba8720fdbb9b9db39/jupyter_server_fileid-0.9.3-py3-none-any.whl", hash = "sha256:f73c01c19f90005d3fff93607b91b4955ba4e1dccdde9bfe8026646f94053791", size = 16922, upload-time = "2024-09-06T07:18:38.445Z" }, -] - -[[package]] -name = "jupyter-server-ydoc" -version = "0.8.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jupyter-server-fileid" }, - { name = "jupyter-ydoc" }, - { name = "ypy-websocket" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2b/a5/a2f366d772d7da8bc36f67eadd08707610512685e266305f5e59fe317c26/jupyter_server_ydoc-0.8.0.tar.gz", hash = "sha256:a6fe125091792d16c962cc3720c950c2b87fcc8c3ecf0c54c84e9a20b814526c", size = 25769, upload-time = "2023-03-05T11:22:23.828Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/e0/63b481d5b21f81fe23173dfc9eb25629fa1bc174fdc4a2c19d09c1ff8124/jupyter_server-2.21.0.tar.gz", hash = "sha256:70d9a1883f57d3576ea17f4ce061ec1a7aad7ef388d00428cfb7f5e4f0022271", size = 760357, upload-time = "2026-08-27T16:06:34.049Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/e4/b9e9b31e5b0d91b4ef749195a19a02f565f6edfb3a845d8dd457031578e3/jupyter_server_ydoc-0.8.0-py3-none-any.whl", hash = "sha256:969a3a1a77ed4e99487d60a74048dc9fa7d3b0dcd32e60885d835bbf7ba7be11", size = 11515, upload-time = "2023-03-05T11:22:21.199Z" }, + { url = "https://files.pythonhosted.org/packages/cb/8a/cb97c2b353cb46cced413be8f742fa8fcd3cb1659524f27314494aa94968/jupyter_server-2.21.0-py3-none-any.whl", hash = "sha256:2ae2e5ce5e97268553e25aebe040197455673d925b5fc7995477153318160cf7", size = 393961, upload-time = "2026-08-27T16:06:31.814Z" }, ] [[package]] -name = "jupyter-ydoc" -version = "0.2.5" +name = "jupyter-server-terminals" +version = "0.5.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "y-py" }, + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "terminado" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/01/11/4bc1c63aad398095171848d66940f7cf057cc7c1d04ecbeb4119a4a2b22d/jupyter_ydoc-0.2.5.tar.gz", hash = "sha256:5a02ca7449f0d875f73e8cb8efdf695dddef15a8e71378b1f4eda6b7c90f5382", size = 64912, upload-time = "2023-07-18T10:25:51.044Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/a7/bcd0a9b0cbba88986fe944aaaf91bfda603e5a50bda8ed15123f381a3b2f/jupyter_server_terminals-0.5.4.tar.gz", hash = "sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5", size = 31770, upload-time = "2026-01-14T16:53:20.213Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/36/66e6cb851a43c95f00f47b36d2cb3e17d37f862449dc8258b2c04f02544b/jupyter_ydoc-0.2.5-py3-none-any.whl", hash = "sha256:5759170f112c70320a84217dd98d287699076ae65a7f88d458d57940a9f2b882", size = 6196, upload-time = "2023-07-18T10:25:49.011Z" }, + { url = "https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl", hash = "sha256:55be353fc74a80bc7f3b20e6be50a55a61cd525626f578dcb66a5708e2007d14", size = 13704, upload-time = "2026-01-14T16:53:18.738Z" }, ] [[package]] name = "jupyterlab" -version = "3.6.8" +version = "4.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ipython" }, + { name = "async-lru" }, + { name = "httpx" }, + { name = "ipykernel" }, { name = "jinja2" }, + { name = "jupyter-builder" }, { name = "jupyter-core" }, + { name = "jupyter-lsp" }, { name = "jupyter-server" }, - { name = "jupyter-server-ydoc" }, - { name = "jupyter-ydoc" }, { name = "jupyterlab-server" }, - { name = "nbclassic" }, - { name = "notebook" }, + { name = "notebook-shim" }, { name = "packaging" }, { name = "tornado" }, + { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e2/b8/b4aeb5f2a6a6e1c05867b16ac436945e711bde15083cc2c8cb968b9e0c2b/jupyterlab-3.6.8.tar.gz", hash = "sha256:a2477383e23f20009188bd9dac7e6e38dbc54307bc36d716bea6ced450647c97", size = 16854256, upload-time = "2024-08-26T20:20:40.593Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/c0/45934229995d4c6e38192ca118d93185f60808f64157e3df3c5c28f8c5fd/jupyterlab-4.6.3.tar.gz", hash = "sha256:2e3db6e3a12495ebd188276e985bf5ac502fbde3d1e8628819920210008de498", size = 28319771, upload-time = "2026-08-10T18:50:57.947Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/09/34b872cee73d02a960523bb8edccdcc5b76d4fc75ffa3f0f04f701cd5137/jupyterlab-3.6.8-py3-none-any.whl", hash = "sha256:891284e75158998e23eb7a23ecc4caaf27b365e41adca374109b1305b9f769db", size = 8864730, upload-time = "2024-08-26T20:20:35.645Z" }, + { url = "https://files.pythonhosted.org/packages/e9/47/242f46de028074651c9bd6d8000fc340ed0d3cdd1a0eae4387826123413a/jupyterlab-4.6.3-py3-none-any.whl", hash = "sha256:0a1ebc6567186f1eabd99536e94df7ed9e96d1e7c5ddf3e4406ae16e88abacb7", size = 17168312, upload-time = "2026-08-10T18:50:53.044Z" }, ] [[package]] @@ -1513,18 +1452,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl", hash = "sha256:ee015381e955e370962968befe1d729ab60fafb6a715ac6751763fbce38c8d4a", size = 66862, upload-time = "2026-07-22T05:22:29.419Z" }, ] -[[package]] -name = "mpl-tools" -version = "0.4.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "matplotlib" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1f/c0/19a7354f50f27628e0cf80cbcf07fae34998002dd3f819f6c6f5b4dab0b5/mpl_tools-0.4.3.tar.gz", hash = "sha256:4ca1a449ec95f3e4e0d873bd85c01eb56790fdaa16df70a63d5f6f3b808feafe", size = 17133, upload-time = "2026-06-26T14:04:11.733Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/57/3d/ef26b234b9816f6fd497149a548ac747d418863291fe33f899dc01b0ab0f/mpl_tools-0.4.3-py3-none-any.whl", hash = "sha256:35c4d630ecad2070c332d2c9db72a3084ab9e639a82cd13902a5de3034388687", size = 19213, upload-time = "2026-06-26T14:04:10.57Z" }, -] - [[package]] name = "multiprocess" version = "0.70.19" @@ -1542,21 +1469,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/82/69e539c4c2027f1e1697e09aaa2449243085a0edf81ae2c6341e84d769b6/multiprocess-0.70.19-py39-none-any.whl", hash = "sha256:0d4b4397ed669d371c81dcd1ef33fd384a44d6c3de1bd0ca7ac06d837720d3c5", size = 133477, upload-time = "2026-01-19T06:47:38.619Z" }, ] -[[package]] -name = "nbclassic" -version = "1.3.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ipykernel" }, - { name = "ipython-genutils" }, - { name = "nest-asyncio" }, - { name = "notebook-shim" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ec/cc/a495b5eb9a964b70c6ae8c861168b78386d2520fd89c68390932f96400b2/nbclassic-1.3.3.tar.gz", hash = "sha256:434228763f8cee754318cd6dfa42370db191af630dabab8e30bafc8c1aa3eee6", size = 64116062, upload-time = "2025-09-16T20:33:15.967Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/fd/dfb6db427bb4e0a50c9802b11df0b69d9364192f3db999849cde9209c8d0/nbclassic-1.3.3-py3-none-any.whl", hash = "sha256:dcee5149aa6aa01846c7458d6394b29b325213b5e118ee14c80d689122e0e4f2", size = 11527229, upload-time = "2025-09-16T20:33:08.625Z" }, -] - [[package]] name = "nbclient" version = "0.11.0" @@ -1612,15 +1524,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/69/ee613f74085ca7103f79cd08d579c4f3177d1d26e5d3d9528d2d6536a707/nbformat-5.11.1-py3-none-any.whl", hash = "sha256:cc6698fa75f4fab8755ead786317815f13a6fee3b53311c0abb1a8b51d52f7ec", size = 79849, upload-time = "2026-08-17T08:10:50.18Z" }, ] -[[package]] -name = "nest-asyncio" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, -] - [[package]] name = "nest-asyncio2" version = "1.7.2" @@ -1641,29 +1544,19 @@ wheels = [ [[package]] name = "notebook" -version = "6.5.4" +version = "7.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "argon2-cffi" }, - { name = "ipykernel" }, - { name = "ipython-genutils" }, - { name = "jinja2" }, - { name = "jupyter-client" }, - { name = "jupyter-core" }, - { name = "nbclassic" }, - { name = "nbconvert" }, - { name = "nbformat" }, - { name = "nest-asyncio" }, - { name = "prometheus-client" }, - { name = "pyzmq" }, - { name = "send2trash" }, - { name = "terminado" }, + { name = "jupyter-builder" }, + { name = "jupyter-server" }, + { name = "jupyterlab" }, + { name = "jupyterlab-server" }, + { name = "notebook-shim" }, { name = "tornado" }, - { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/1e/b555b6e33c962a605e2e85b6014f609d3e1c6a5ff48f7c2480376b430d96/notebook-6.5.4.tar.gz", hash = "sha256:517209568bd47261e2def27a140e97d49070602eea0d226a696f42a7f16c9a4e", size = 5785832, upload-time = "2023-04-06T15:08:15.471Z" } +sdist = { url = "https://files.pythonhosted.org/packages/31/d9/5c76de84e96e1cf8aae3fce930875e0615e14f3557bbcdb634aab52da9d3/notebook-7.6.2.tar.gz", hash = "sha256:cc02b5f0bb972160cccfe44ad8a1a202036206ba3439469c514f03aefa9ae807", size = 5500147, upload-time = "2026-08-11T13:21:16.632Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/21/0e7683e7c4d51b8f6cc5df9bbd33fb2d1e114b9e5dcddeef96ebd8e86348/notebook-6.5.4-py3-none-any.whl", hash = "sha256:dd17e78aefe64c768737b32bf171c1c766666a21cc79a44d37a1700771cab56f", size = 529822, upload-time = "2023-04-06T15:08:11.457Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fd/3e552ff5b24dd305c6e9a10e8645e29c03091c317429f326ae10dbae1ac6/notebook-7.6.2-py3-none-any.whl", hash = "sha256:5fe9e09c335cb4b7de21627b860f77210e70e54b1fb1276ad942a4a7e1d858d3", size = 5547102, upload-time = "2026-08-11T13:21:13.302Z" }, ] [[package]] @@ -2421,15 +2314,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl", hash = "sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c", size = 17610, upload-time = "2026-01-14T06:27:35.218Z" }, ] -[[package]] -name = "setuptools" -version = "84.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" }, -] - [[package]] name = "six" version = "1.17.0" @@ -2439,15 +2323,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] -[[package]] -name = "sniffio" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, -] - [[package]] name = "soupsieve" version = "2.9.2" @@ -2526,19 +2401,19 @@ wheels = [ [[package]] name = "tornado" -version = "6.5.8" +version = "6.5.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/10/d3/343e5bb989d6515b1646cf3d40135d73f3d5e45339bded401b56cdac24dd/tornado-6.5.8.tar.gz", hash = "sha256:9452e1b208a8bd771e2cb1f2ff564985b9b214bdebbe622793e1799e0a6bd23f", size = 520493, upload-time = "2026-08-07T02:12:42.971Z" } +sdist = { url = "https://files.pythonhosted.org/packages/22/15/ca7aaebb77f850493cba71ace508aeffb896d1ad8976417b48b79823dbd2/tornado-6.5.9.tar.gz", hash = "sha256:4d868544ebdf2fc155239a74397f65ebfea9b4d3635296c96b5dfb0701c354f5", size = 536145, upload-time = "2026-09-14T18:08:37.434Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/d5/007086fd8df5489338e204f65adce33fd4f21a4999dbb2b9cff2f897b5f4/tornado-6.5.8-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:cc6aa787d7cfab7c3d35189dc7a56fbd2399a569624c730c6b55b3d6531d0403", size = 449487, upload-time = "2026-08-07T02:12:28.682Z" }, - { url = "https://files.pythonhosted.org/packages/70/c8/5a24a99495903f594f6a199dd7beead1cbc0a13e2cb9102727bcaaf2a997/tornado-6.5.8-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9715b5eb79735b2bcd454ce216a9275b7c0470e64ea1bf5742f78b2f72b26eeb", size = 447649, upload-time = "2026-08-07T02:12:30.306Z" }, - { url = "https://files.pythonhosted.org/packages/6e/de/f2e733f386b85962d1b1dc82cd63d169b5b4580062b35397eac9244a41fe/tornado-6.5.8-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:547d63f450d570c14fe0e8db2cfb14c9bbd1c2503b4a6612586267955aa47b58", size = 450707, upload-time = "2026-08-07T02:12:31.95Z" }, - { url = "https://files.pythonhosted.org/packages/0b/94/20efeee9a01c141e9ac47c397f81679dfda24b32768fc4fff24e76d36c2c/tornado-6.5.8-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e2360a0ffbe145eca8af0b19cb7203d79b1a98dd4cccdd6b368f6f49c2e3808", size = 451677, upload-time = "2026-08-07T02:12:33.512Z" }, - { url = "https://files.pythonhosted.org/packages/42/ec/a96ccb8ccf0de2b7bc2c5fa1608a4803735018242e90c4882365a9fd418f/tornado-6.5.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5d242290bdf7ab3151bc1065fdd75c0dcc21cbc7b49f22a4c56329c2d6566d22", size = 451510, upload-time = "2026-08-07T02:12:35.346Z" }, - { url = "https://files.pythonhosted.org/packages/29/b5/93185859245ad3f00e62175f29607346788b696369347f0146e0421286bb/tornado-6.5.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7b94ff0e128fe0542f3bd331fb44d06260fc4ac16881545159f34ef08aad4195", size = 450917, upload-time = "2026-08-07T02:12:36.963Z" }, - { url = "https://files.pythonhosted.org/packages/97/cf/fe33cf062834487d34d1559746a4a12521033c22645b6d74d4bca702e018/tornado-6.5.8-cp39-abi3-win32.whl", hash = "sha256:67832909c4779c64942380cb5f044a5c6163d00831472d80e25e115de9917836", size = 451952, upload-time = "2026-08-07T02:12:38.512Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e1/468ad54333e92ccb62627e62cb88e5fc14a2171daa67ed47b1b8542d5b86/tornado-6.5.8-cp39-abi3-win_amd64.whl", hash = "sha256:11881db6b7c168494be2c2d12e65931451bdf7ee718535418ae1d8855dd5a0ee", size = 452391, upload-time = "2026-08-07T02:12:39.971Z" }, - { url = "https://files.pythonhosted.org/packages/ad/3e/cd5e4f06e34cde33b8ef66cf36aa2b5ad46354cc1af7d2136bbe365fee1d/tornado-6.5.8-cp39-abi3-win_arm64.whl", hash = "sha256:68a7468c7e289f8514d7d664101753903217eff1bb6822c6b5994a0b5f5bcb26", size = 451411, upload-time = "2026-08-07T02:12:41.469Z" }, + { url = "https://files.pythonhosted.org/packages/a0/4a/4d3459fd2f360dd99233f69f22b36012a42856ace526da2c6da3cb0e6df2/tornado-6.5.9-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:dea3bee433b73d6312d993ce89ad5f2b8d3ed9fde56e79ba5342b9edb78d342a", size = 464279, upload-time = "2026-09-14T18:08:20.909Z" }, + { url = "https://files.pythonhosted.org/packages/26/4b/5af2f7dcd674e2f1cacef763456f79823404f50954789d6e1ec7a70cd58a/tornado-6.5.9-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:22bed49cdb55292d1d58f46008ba2cc3020e5b84995ef6ae749676c8b5309514", size = 462440, upload-time = "2026-09-14T18:08:22.711Z" }, + { url = "https://files.pythonhosted.org/packages/9e/0f/38f8d16011de3b1ce2babb4c3bcefb126d2dc47d04878ae3ad5c39a00caa/tornado-6.5.9-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0cc0876a58eee9143476f24901b1b8eb4d52443283ade2b6bca650dd1da04f84", size = 465496, upload-time = "2026-09-14T18:08:24.407Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e9/3888f265d1e287fc63cd61a6696f96a650024e32f9e89c0b1817ac5c40f4/tornado-6.5.9-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc0d703cc7ec92b47ebfe236c97cd765c1f2e45744a2e156e6677e54c948274d", size = 466468, upload-time = "2026-09-14T18:08:25.928Z" }, + { url = "https://files.pythonhosted.org/packages/d6/8b/d2bcf417981fa6f7698ab84a52394a76cf608e6704d266e2e0e29f1fd9d0/tornado-6.5.9-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:93f9ab0226ef625b94cacb7981a5af104f1735ea02f92282abe5c389109fb31d", size = 466303, upload-time = "2026-09-14T18:08:27.566Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a5/c5cfab11420c3f908238a6c861b8f3864fdbd2b2b87e30c9c16f36a11512/tornado-6.5.9-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5801bbf94dfde8d21087b6aa0b17f3a5dab6110dfe44e7e4492df5de393ac15b", size = 465706, upload-time = "2026-09-14T18:08:29.182Z" }, + { url = "https://files.pythonhosted.org/packages/ab/36/5a5da0aa15b7afdd46ece09c004a75af7d00f6a96b692bf696ff94bc2a55/tornado-6.5.9-cp39-abi3-win32.whl", hash = "sha256:dca9f377e80efe5dd4d52a2776b67492a81a39b2d931de169b6b36d0c1da444c", size = 466750, upload-time = "2026-09-14T18:08:30.836Z" }, + { url = "https://files.pythonhosted.org/packages/34/d7/2446cf3125372e2b4be6483b4b93f360dc50ce2493864755484b23bb50f2/tornado-6.5.9-cp39-abi3-win_amd64.whl", hash = "sha256:f810730ac71eadf009e6e5cb6d534d096887310f5649cff19d7e0d19c0bf4ee7", size = 467191, upload-time = "2026-09-14T18:08:32.521Z" }, + { url = "https://files.pythonhosted.org/packages/39/70/f1024364bf78fa921c63ef2883d36a3b3b705750c98ae174593afaab7a65/tornado-6.5.9-cp39-abi3-win_arm64.whl", hash = "sha256:b5f4d92798337260c6d9f9f0e400c7539dd5e55cf93479a3f8508c9aa09c2b3b", size = 466211, upload-time = "2026-09-14T18:08:35.18Z" }, ] [[package]] @@ -2657,33 +2532,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/bf/60/bc7a980fc78837d6e wheels = [ { url = "https://files.pythonhosted.org/packages/34/95/40e17e20046b7bc820d29d09ae84ec157ec8dd6e6f6cd722626292c31b2e/widgetsnbextension-4.0.16-py3-none-any.whl", hash = "sha256:a31a8774885b96fe825462f5d6496166f0c7cae111195b6465c801d230eb5a4e", size = 2225148, upload-time = "2026-08-18T08:52:53.736Z" }, ] - -[[package]] -name = "y-py" -version = "0.6.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7a/4f/af6e0c02d6876fc466f0ae74ac01693f00d822a93830a9c3e84d17b03f8d/y_py-0.6.2.tar.gz", hash = "sha256:4757a82a50406a0b3a333aa0122019a331bd6f16e49fed67dca423f928b3fd4d", size = 53013, upload-time = "2023-10-05T06:00:28.253Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/19/e0/7baf1d43bf922e91e54e9fa04858619d1957ec63292191e524456d146648/y_py-0.6.2-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:7cbefd4f1060f05768227ddf83be126397b1d430b026c64e0eb25d3cf50c5734", size = 752650, upload-time = "2023-10-05T05:58:34.025Z" }, - { url = "https://files.pythonhosted.org/packages/05/41/b242e358c530d88053172617489a702cabaa00a4ea6462e02857f7ee173a/y_py-0.6.2-cp312-cp312-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:032365dfe932bfab8e80937ad6093b4c22e67d63ad880096b5fa8768f8d829ba", size = 1461497, upload-time = "2023-10-05T05:58:35.904Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ff/4cbe7961c0ddb421cee48a2dc754a43dde05f31ee15d76e089eeb4f541b8/y_py-0.6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a70aee572da3994238c974694767365f237fc5949a550bee78a650fe16f83184", size = 1687776, upload-time = "2023-10-05T05:58:37.766Z" }, - { url = "https://files.pythonhosted.org/packages/ff/ff/7190c93d6f0f63d40514a10116512b788b2363a3f8a960851f435ccbab3f/y_py-0.6.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae80d505aee7b3172cdcc2620ca6e2f85586337371138bb2b71aa377d2c31e9a", size = 1693306, upload-time = "2023-10-05T05:58:40.354Z" }, - { url = "https://files.pythonhosted.org/packages/4e/29/df7d9b506deff4158b80433c19294889951afe0cef911ab99dbbcf8704d5/y_py-0.6.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a497ebe617bec6a420fc47378856caae40ab0652e756f3ed40c5f1fe2a12220", size = 1844205, upload-time = "2023-10-05T05:58:42.409Z" }, - { url = "https://files.pythonhosted.org/packages/1d/a1/be12ce76ae783838c4df08f37dc0b65e85fe762f49c8510e2ca388d4eb68/y_py-0.6.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e8638355ae2f996356f7f281e03a3e3ce31f1259510f9d551465356532e0302c", size = 2029472, upload-time = "2023-10-05T05:58:44.186Z" }, - { url = "https://files.pythonhosted.org/packages/cc/60/cbf95e42656fd84af4bb341f253add1030184b39f86962434920e121a2e4/y_py-0.6.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8448da4092265142662bbd3fc46cb8b0796b1e259189c020bc8f738899abd0b5", size = 1701647, upload-time = "2023-10-05T05:58:46.146Z" }, - { url = "https://files.pythonhosted.org/packages/c9/84/31998386aa81982fac3097816e45db238c8259480a228b497c3b3dc5d57a/y_py-0.6.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:69cfbcbe0a05f43e780e6a198080ba28034bf2bb4804d7d28f71a0379bfd1b19", size = 1758480, upload-time = "2023-10-05T05:58:48.771Z" }, -] - -[[package]] -name = "ypy-websocket" -version = "0.8.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiofiles" }, - { name = "aiosqlite" }, - { name = "y-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/0b/8e936a41e15b8a8a034c9708062ee82f18f4ca6a969c443d3ea10a54f1ea/ypy_websocket-0.8.4.tar.gz", hash = "sha256:43a001473f5c8abcf182f603049cf305cbc855ad8deaa9dfa0f3b5a7cea9d0ff", size = 11142, upload-time = "2023-02-21T16:33:52.059Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/f3/f6a8dfcae1716d260e6cf3ecea864a6abfddadd6a059bed80bd5618b67c1/ypy_websocket-0.8.4-py3-none-any.whl", hash = "sha256:b1ba0dfcc9762f0ca168d2378062d3ca1299d39076b0f145d961359121042be5", size = 10465, upload-time = "2023-02-21T16:33:49.668Z" }, -] From 24eb76a4f063790914c035c7deb7e7ba140bfd33 Mon Sep 17 00:00:00 2001 From: patnr Date: Tue, 15 Sep 2026 09:53:58 +0200 Subject: [PATCH 2/4] Add Colab smoke tests (GitHub Actions + local) Run inside Google's published Colab runtime image: bootstrap, check that no preinstalled package changed (the restart hazard), execute both notebooks. Monthly cron catches Colab's upgrades; also on PRs/pushes touching Colab files. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/colab-compat.yml | 44 +++++++++++++++++++++ README.md | 61 +++++++++++++++++++++--------- tests/colab/check_freeze.py | 44 +++++++++++++++++++++ tests/colab/run_nb.py | 58 ++++++++++++++++++++++++++++ tests/colab/smoke.sh | 56 +++++++++++++++++++++++++++ tests/colab/smoke_inner.sh | 39 +++++++++++++++++++ 6 files changed, 284 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/colab-compat.yml create mode 100644 tests/colab/check_freeze.py create mode 100644 tests/colab/run_nb.py create mode 100755 tests/colab/smoke.sh create mode 100755 tests/colab/smoke_inner.sh diff --git a/.github/workflows/colab-compat.yml b/.github/workflows/colab-compat.yml new file mode 100644 index 0000000..4117403 --- /dev/null +++ b/.github/workflows/colab-compat.yml @@ -0,0 +1,44 @@ +name: Colab compatibility + +# Runs the tutorials inside Google's published Colab runtime image +# (which lags the live Colab runtime by a few weeks), checking that +# - colab_bootstrap.sh installs cleanly, +# - it modifies no preinstalled package (=> no "restart runtime" prompt), +# - both notebooks execute end-to-end. +# See tests/colab/ and the dependency policy in pyproject.toml. + +on: + workflow_dispatch: + schedule: + - cron: "0 8 1 * *" # 1st of every month: catches Colab's upgrades + push: + branches: [master] + paths: + - requirements-colab.txt + - colab_bootstrap.sh + - notebooks/** + - tests/colab/** + - .github/workflows/colab-compat.yml + pull_request: + paths: + - requirements-colab.txt + - colab_bootstrap.sh + - notebooks/** + - tests/colab/** + - .github/workflows/colab-compat.yml + +jobs: + colab: + runs-on: ubuntu-latest + timeout-minutes: 90 + container: + image: us-docker.pkg.dev/colab-images/public/runtime:latest + + steps: + - uses: actions/checkout@v4 + + - name: Smoke test (bootstrap, freeze diff, execute notebooks) + env: + REPO_DIR: ${{ github.workspace }} + SMOKE_MODE: local + run: bash tests/colab/smoke_inner.sh diff --git a/README.md b/README.md index e1e73bd..48d3940 100644 --- a/README.md +++ b/README.md @@ -18,24 +18,20 @@ Use this option for development, or if you simply want faster computations *You could instead download & unzip, but then you will have to manually download any later updates.* -**Install** (Python >= 3.12) with either +**Install** (Python >= 3.12) with +[uv](https://docs.astral.sh/uv/getting-started/installation/), +a single, cross-platform tool that also fetches Python for you: -- [uv](https://docs.astral.sh/uv/getting-started/installation/) -- recommended: - a single, cross-platform tool that also fetches Python for you. +```bash +uv sync +uv run jupyter notebook +``` - ```bash - uv sync - uv run jupyter notebook - ``` - -- Or any other installer, e.g. `pip` inside an active environment - created by [venv](https://docs.python.org/3/library/venv.html), - [conda](https://www.anaconda.com/download), ... : - - ```bash - pip install . - jupyter notebook - ``` +*Without uv:* this project is not a package (nothing is imported from it), +so there is nothing to `pip install .`. Instead, in an active environment +(venv, conda, ...) do `pip install uv && uv pip install -r pyproject.toml`. +Or, if you already have the scientific stack, just add what +`requirements-colab.txt` lists. The `jupyter notebook` command opens a file navigator in your web browser. Click on `notebooks/HistoryMatch.ipynb`. @@ -44,8 +40,7 @@ Click on `notebooks/HistoryMatch.ipynb`. The dev tooling (jupytext, ruff, pre-commit, ...) lives in the `dev` [dependency group](https://peps.python.org/pep-0735/), -which `uv sync` installs by default -(with `pip`: `pip install --group dev`). +which `uv sync` installs by default. I prefer to develop mostly in the format of standard python script, which is why each notebook corresponds to a `.py` file synced via [jupytext](https://jupytext.readthedocs.io/en/latest/). @@ -56,6 +51,36 @@ then the notebooks will get synced with the `.py` files before committing. Linting (which is, as of now, just a suggestion) can be run with `ruff check --output-format=grouped`. +### Dependencies and Colab + +The single-click Colab experience is the design constraint for dependencies: +Colab's base environment is not ours to control and gets upgraded regularly, +and re-installing anything it has already imported (numpy, matplotlib, ...) +forces a runtime restart. +Therefore `colab_bootstrap.sh` installs `requirements-colab.txt` with `--no-deps`, +i.e. only what Colab lacks, while `pyproject.toml` leaves Colab-preinstalled +packages unconstrained and `uv.lock` provides reproducibility locally. +The full reasoning is documented in `pyproject.toml`. +The two files must be kept in sync by hand. + +### Smoke tests + +Google publishes the Colab runtime image, so the Colab experience can be tested +without Colab. This runs monthly (and on relevant pushes) on GitHub Actions, +see `.github/workflows/colab-compat.yml`, which is the primary way to catch +breakage from Colab's upgrades. The same test can be run locally +(requires `podman` or `docker`; the image is large, see the script): + +```bash +tests/colab/smoke.sh # local checkout +tests/colab/smoke.sh --remote # exactly what students get (GitHub master) +``` + +It checks that the bootstrap installs cleanly, that it modifies no +preinstalled package, and that both notebooks execute end-to-end. +The notebook runner also works locally, without the image: +`uv run tests/colab/run_nb.py notebooks/HistoryMatch.ipynb --skip-bootstrap`. + ## Contributors This work has been developed by *Patrick N. Raanes*, researcher at *NORCE*. diff --git a/tests/colab/check_freeze.py b/tests/colab/check_freeze.py new file mode 100644 index 0000000..bf4158a --- /dev/null +++ b/tests/colab/check_freeze.py @@ -0,0 +1,44 @@ +"""Compare two `pip freeze` outputs: fail if any pre-existing package changed. + +Usage: check_freeze.py BEFORE.txt AFTER.txt + +Rationale: on Colab, packages present at kernel start are already imported; +re-installing them would require a runtime restart. `colab_bootstrap.sh` must +therefore only *add* packages. This check makes that contract testable. +""" + +import sys + + +def parse(path): + pkgs = {} + for line in open(path): + line = line.strip() + if not line or line.startswith("#"): + continue + for sep in ("==", " @ "): + if sep in line: + name, ver = line.split(sep, 1) + break + else: + name, ver = line, "" + pkgs[name.lower().replace("_", "-")] = ver + return pkgs + + +def main(before_path, after_path): + before, after = parse(before_path), parse(after_path) + changed = {k: (v, after.get(k)) for k, v in before.items() if after.get(k) != v} + added = sorted(set(after) - set(before)) + print(f"Added packages ({len(added)}):", ", ".join(added) or "none") + if changed: + print("FAIL: preinstalled packages were modified (=> Colab would need a restart):") + for k, (old, new) in sorted(changed.items()): + print(f" {k}: {old} -> {new if new is not None else '(removed)'}") + return 1 + print("OK: no preinstalled package was modified.") + return 0 + + +if __name__ == "__main__": + sys.exit(main(*sys.argv[1:3])) diff --git a/tests/colab/run_nb.py b/tests/colab/run_nb.py new file mode 100644 index 0000000..9fc1ec2 --- /dev/null +++ b/tests/colab/run_nb.py @@ -0,0 +1,58 @@ +"""Execute a notebook headlessly (via `nbclient`), as a smoke test. + +Usage: run_nb.py NOTEBOOK.ipynb [--timeout SECONDS] [--skip-bootstrap] [--out PATH] + +- `--skip-bootstrap` removes the cell that runs `colab_bootstrap.sh` (so that + a test of a local checkout does not fetch and install `master` on top). +- Fails (exit 1) on the first cell error, printing that cell's source and traceback. + +Runs both locally (`uv run tests/colab/run_nb.py notebooks/HistoryMatch.ipynb`) +and inside the Colab runtime image (see smoke.sh), which ships `nbclient`. +""" + +import argparse +import sys +import time +from pathlib import Path + +import nbformat +from nbclient import NotebookClient +from nbclient.exceptions import CellExecutionError + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("notebook", type=Path) + ap.add_argument("--timeout", type=int, default=1800, help="per-cell timeout (s)") + ap.add_argument("--skip-bootstrap", action="store_true") + ap.add_argument("--out", type=Path, help="write the executed notebook here") + args = ap.parse_args() + + nb = nbformat.read(args.notebook, as_version=4) + if args.skip_bootstrap: + n = len(nb.cells) + nb.cells = [c for c in nb.cells if "colab_bootstrap.sh" not in c.source] + print(f"Skipped {n - len(nb.cells)} bootstrap cell(s).") + + client = NotebookClient( + nb, + timeout=args.timeout, + kernel_name="python3", + resources={"metadata": {"path": str(args.notebook.parent)}}, + ) + t0 = time.time() + try: + client.execute() + except CellExecutionError as e: + print(f"FAIL: {args.notebook} after {time.time() - t0:.0f}s\n{e}") + return 1 + finally: + if args.out: + nbformat.write(nb, args.out) + ncode = sum(c.cell_type == "code" for c in nb.cells) + print(f"OK: {args.notebook}: {ncode} code cells in {time.time() - t0:.0f}s") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/colab/smoke.sh b/tests/colab/smoke.sh new file mode 100755 index 0000000..2ab3fc6 --- /dev/null +++ b/tests/colab/smoke.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Smoke-test the tutorials inside Google's published Colab runtime image, +# LOCALLY. The same test runs monthly on GitHub Actions +# (.github/workflows/colab-compat.yml), which is the primary way to run it: +# native x86-64 and a fast network. Use this script when you need to iterate +# without pushing. +# +# tests/colab/smoke.sh # local checkout, both notebooks +# tests/colab/smoke.sh --remote # what students get: bootstrap from GitHub master +# SMOKE_NOTEBOOKS=HistoryMatch.ipynb tests/colab/smoke.sh +# COLAB_IMAGE=us-docker.pkg.dev/colab-images/public/runtime: tests/colab/smoke.sh +# +# Checks (see smoke_inner.sh): the bootstrap runs, it modifies no preinstalled +# package (=> no "restart runtime"), and every notebook executes without error. +# +# The image is x86-64 and ~23 GB compressed (it is the GPU image: CUDA, torch, +# TF, ...), so the first run takes a while; later runs use the cached image. +# On Apple silicon it runs emulated (Rosetta via `podman machine`), i.e. slowly. +# Tags: `latest`, or dated ones like `release-colab_20250925-060051_RC00`; +# list with `podman search --list-tags us-docker.pkg.dev/colab-images/public/runtime`. +# Note the public image lags Colab's actual runtime by a few weeks; for what +# students *currently* have, see https://github.com/googlecolab/backend-info . +set -euo pipefail + +IMAGE=${COLAB_IMAGE:-us-docker.pkg.dev/colab-images/public/runtime:latest} +ENGINE=${CONTAINER_ENGINE:-$(command -v podman || command -v docker)} +REPO=$(cd "$(dirname "$0")/../.." && pwd) + +MODE=local +for arg in "$@"; do + case "$arg" in + --remote) MODE=remote ;; + *) echo "Unknown arg: $arg"; exit 2 ;; + esac +done + +# The repo is copied into the container rather than bind-mounted: it is a few +# MB, and bind mounts are unreliable in some `podman machine` setups +# (e.g. "statfs ...: connection refused" with the libkrun provider on macOS). +name=hm-colab-smoke-$$ +staging=$(mktemp -d) +trap '"$ENGINE" rm -f "$name" >/dev/null 2>&1; rm -rf "$staging"' EXIT + +mkdir "$staging/repo" +tar -C "$REPO" --exclude=.git --exclude=.claude --exclude=.ipynb_checkpoints \ + --exclude=.ruff_cache --exclude=.pytest_cache -cf - . | tar -C "$staging/repo" -xf - + +"$ENGINE" create --name "$name" --platform linux/amd64 \ + -e REPO_DIR=/repo \ + -e SMOKE_MODE="$MODE" \ + -e SMOKE_BRANCH="${SMOKE_BRANCH:-master}" \ + -e SMOKE_NOTEBOOKS="${SMOKE_NOTEBOOKS:-HistoryMatch.ipynb Optimise.ipynb}" \ + -e SMOKE_TIMEOUT="${SMOKE_TIMEOUT:-1800}" \ + --entrypoint bash "$IMAGE" /repo/tests/colab/smoke_inner.sh >/dev/null +"$ENGINE" cp "$staging/repo" "$name":/repo +"$ENGINE" start --attach "$name" diff --git a/tests/colab/smoke_inner.sh b/tests/colab/smoke_inner.sh new file mode 100755 index 0000000..5d3ccd7 --- /dev/null +++ b/tests/colab/smoke_inner.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Runs INSIDE the Colab runtime image, either on GitHub Actions +# (.github/workflows/colab-compat.yml) or locally via smoke.sh. +# +# Env: REPO_DIR (checkout of this repo, default /repo), +# SMOKE_MODE=local|remote (default local), SMOKE_BRANCH (remote mode), +# SMOKE_NOTEBOOKS (default: both), SMOKE_TIMEOUT (per cell, default 1800). +set -euo pipefail +REPO_DIR=${REPO_DIR:-/repo} + +MODE=${SMOKE_MODE:-local} +NOTEBOOKS=${SMOKE_NOTEBOOKS:-"HistoryMatch.ipynb Optimise.ipynb"} +TIMEOUT=${SMOKE_TIMEOUT:-1800} + +mkdir -p /content && cd /content +echo "== Image: ${COLAB_RELEASE_TAG:-unknown} | $(python --version) | $(which python) $(which pip)" +echo "== Mode: $MODE" + +pip freeze > /tmp/before.txt + +echo "== 1/3 bootstrap" +if [[ "$MODE" == remote ]]; then + remote=https://raw.githubusercontent.com/patnr/HistoryMatching + wget -qO- "$remote/${SMOKE_BRANCH:-master}/colab_bootstrap.sh" | bash -s -- --debug --branch="${SMOKE_BRANCH:-master}" +else + bash "$REPO_DIR/colab_bootstrap.sh" --debug --repo="$REPO_DIR" +fi + +echo "== 2/3 preinstalled packages untouched?" +pip freeze > /tmp/after.txt +python "$REPO_DIR/tests/colab/check_freeze.py" /tmp/before.txt /tmp/after.txt + +echo "== 3/3 execute notebooks" +skip=""; [[ "$MODE" == local ]] && skip="--skip-bootstrap" +status=0 +for nb in $NOTEBOOKS; do + python "$REPO_DIR/tests/colab/run_nb.py" "$nb" --timeout "$TIMEOUT" $skip || status=1 +done +exit $status From c1d9add51f7eec0ba234fafd9907f984da41284f Mon Sep 17 00:00:00 2001 From: patnr Date: Tue, 15 Sep 2026 11:21:46 +0200 Subject: [PATCH 3/4] Depend on minires 0.3.0 from PyPI, pinned to the minor `minires~=0.3.0` replaces the git-commit pin, in `pyproject.toml`, `requirements-colab.txt` and the lockfile. minires is 0.x, so a minor bump may break the API; the pin therefore advances deliberately, with the notebooks checked against the new version. Co-Authored-By: Claude Fable 5.1 --- pyproject.toml | 17 +++++++++++------ requirements-colab.txt | 4 ++-- uv.lock | 16 +++++++++++----- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ef0df33..c69fd2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,10 +36,12 @@ authors = [{ name = "Patrick N. Raanes", email = "patrick.n.raanes@gmail.com" }] requires-python = ">=3.12" dependencies = [ - # Our own toy reservoir simulator. Pinned to a commit until the PyPI release, - # then this becomes `minires>=X.Y` (and `requirements-colab.txt` likewise). - # For local hacking: `uv pip install -e ${HOME}/path/MiniRes` - "minires @ git+https://github.com/patnr/MiniRes.git@1b9f8d56010a", + # Our own toy reservoir simulator, from PyPI since its 0.3.0 (2026-09-15). + # Pinned to the minor: `minires` is 0.x, so a minor bump may break the API + # (keep `requirements-colab.txt` in step). An unreleased snapshot is + # `"minires @ git+https://github.com/patnr/MiniRes.git@"`; for local + # hacking, `uv pip install -e ${HOME}/path/MiniRes`. + "minires~=0.3.0", # Preinstalled on Colab => deliberately unconstrained (see policy above). "numpy", "scipy", @@ -68,8 +70,11 @@ dev = [ # Python 3.12, and doubled the surface to test. `ipympl` was also dropped: # all notebooks use the inline backend (the only one that works on Colab). "notebook", - # Executes notebooks headlessly (`tests/colab/run_nb.py`). - "nbclient", + # `tests/colab/run_nb.py` executes notebooks headlessly with these two + # (deliberately not nbclient, see its docstring). Both come with `notebook` + # anyway, and Colab ships them. + "jupyter_client", + "nbformat", # Keep in sync with the `jupytext` rev in `.pre-commit-config.yaml`. "jupytext~=1.15.2", "ipdb", diff --git a/requirements-colab.txt b/requirements-colab.txt index ba1a9c0..7366e0f 100644 --- a/requirements-colab.txt +++ b/requirements-colab.txt @@ -10,8 +10,8 @@ # Keep in sync with `pyproject.toml` (the policy is documented there). # Colab's current package list: https://github.com/googlecolab/backend-info -# Pin must equal the one in pyproject.toml (until the PyPI release of minires). -minires @ git+https://github.com/patnr/MiniRes.git@1b9f8d56010a +# Pin must equal the one in pyproject.toml. +minires~=0.3.0 struct-tools adjustText==0.8 # pathos and its (pure-Python) dependencies. Colab ships dill and multiprocess diff --git a/uv.lock b/uv.lock index 36d3704..2b6d30e 100644 --- a/uv.lock +++ b/uv.lock @@ -695,8 +695,9 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "ipdb" }, + { name = "jupyter-client" }, { name = "jupytext" }, - { name = "nbclient" }, + { name = "nbformat" }, { name = "notebook" }, { name = "pre-commit" }, { name = "ruff" }, @@ -708,7 +709,7 @@ requires-dist = [ { name = "ipython" }, { name = "ipywidgets" }, { name = "matplotlib" }, - { name = "minires", git = "https://github.com/patnr/MiniRes.git?rev=1b9f8d56010a" }, + { name = "minires", specifier = "~=0.3.0" }, { name = "numpy" }, { name = "pathos" }, { name = "scipy" }, @@ -719,8 +720,9 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "ipdb" }, + { name = "jupyter-client" }, { name = "jupytext", specifier = "~=1.15.2" }, - { name = "nbclient" }, + { name = "nbformat" }, { name = "notebook" }, { name = "pre-commit" }, { name = "ruff", specifier = "~=0.5.0" }, @@ -1434,14 +1436,18 @@ wheels = [ [[package]] name = "minires" -version = "0.2.0" -source = { git = "https://github.com/patnr/MiniRes.git?rev=1b9f8d56010a#1b9f8d56010a8c1cd36b65481b2a0d81c094bf0f" } +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "matplotlib" }, { name = "scipy" }, { name = "struct-tools" }, { name = "tqdm" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/35/95/45fe49d0e8fd79f7efe1ed9e730935e4ed3ab9147651f4fd3027051060b7/minires-0.3.0.tar.gz", hash = "sha256:858b91e089ac25fe9df002d30e71e8d1f1d8c3d51134d7f598af33edaba7dd47", size = 63406, upload-time = "2026-09-15T08:37:23.147Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/94/e15c1963ff7026bd0dbbf978e421054ee66cecaa6bf8cd1eba9aebda1116/minires-0.3.0-py3-none-any.whl", hash = "sha256:fdb7f331fc1606d8b4e9efc2b9804c183a3dc5ad99cfe84d9562abb52a4f0fcf", size = 67341, upload-time = "2026-09-15T08:37:21.882Z" }, +] [[package]] name = "mistune" From 33850269bd275c63b528449221309d879b4abf1e Mon Sep 17 00:00:00 2001 From: patnr Date: Tue, 15 Sep 2026 11:23:31 +0200 Subject: [PATCH 4/4] Smoke tests: notebook runner on jupyter_client instead of nbclient nbclient's asyncio loop intermittently misses the execute-reply of cells that emit a burst of ipywidgets comm messages (our interact/toggle_items cells), stalling for the full per-cell timeout ~50% of the time. Seen with nbclient 0.11, jupyter_client 8.10, pyzmq 25-27, ipykernel 6 and 7. The blocking client polls synchronously and is unaffected (8/8 fast). Also: -v prints each cell with its duration; timeouts are failures. Co-Authored-By: Claude Fable 5.1 --- tests/colab/run_nb.py | 97 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 78 insertions(+), 19 deletions(-) diff --git a/tests/colab/run_nb.py b/tests/colab/run_nb.py index 9fc1ec2..bf362b1 100644 --- a/tests/colab/run_nb.py +++ b/tests/colab/run_nb.py @@ -1,13 +1,21 @@ -"""Execute a notebook headlessly (via `nbclient`), as a smoke test. +"""Execute a notebook headlessly, as a smoke test. -Usage: run_nb.py NOTEBOOK.ipynb [--timeout SECONDS] [--skip-bootstrap] [--out PATH] +Usage: run_nb.py NOTEBOOK.ipynb [--timeout SECONDS] [--skip-bootstrap] [--out PATH] [-v] - `--skip-bootstrap` removes the cell that runs `colab_bootstrap.sh` (so that a test of a local checkout does not fetch and install `master` on top). -- Fails (exit 1) on the first cell error, printing that cell's source and traceback. +- Fails (exit 1) on the first cell error or timeout, printing that cell's source. +- `-v` prints each code cell (index, first line, duration) as it runs. Runs both locally (`uv run tests/colab/run_nb.py notebooks/HistoryMatch.ipynb`) -and inside the Colab runtime image (see smoke.sh), which ships `nbclient`. +and inside the Colab runtime image (see smoke.sh). + +Why not `nbclient` (or `jupyter nbconvert --execute`, `papermill`, which wrap it)? +Its asyncio message loop intermittently misses the execute-reply of cells that +emit a burst of ipywidgets comm messages (our `interact`/`toggle_items` cells), +so such cells stall for the full per-cell timeout, ~50% of the time (seen 2026-09 +with nbclient 0.11, jupyter_client 8.10, pyzmq 25-27, ipykernel 6 and 7). +`jupyter_client`'s blocking client polls synchronously and does not have this. """ import argparse @@ -16,8 +24,47 @@ from pathlib import Path import nbformat -from nbclient import NotebookClient -from nbclient.exceptions import CellExecutionError +from jupyter_client.manager import start_new_kernel +from nbformat.v4 import output_from_msg + + +class CellFailed(Exception): + pass + + +def run_cell(kc, cell, timeout): + """Execute one code cell; collect its outputs; raise `CellFailed` on error/timeout.""" + msg_id = kc.execute(cell.source) + cell.outputs = [] + deadline = time.monotonic() + timeout + reply = idle = False + while not (reply and idle): + remaining = deadline - time.monotonic() + if remaining <= 0: + raise CellFailed(f"Timed out after {timeout}s") + # Drain iopub (outputs, status), then check for the shell reply. + try: + msg = kc.get_iopub_msg(timeout=min(remaining, 0.1)) + except Exception: # queue.Empty + msg = None + if msg and msg["parent_header"].get("msg_id") == msg_id: + mt = msg["msg_type"] + if mt == "status": + idle = msg["content"]["execution_state"] == "idle" + elif mt in ("stream", "display_data", "execute_result", "error"): + cell.outputs.append(output_from_msg(msg)) + elif mt == "clear_output": + cell.outputs = [] + if not reply: + try: + r = kc.get_shell_msg(timeout=0) + except Exception: + r = None + if r and r["parent_header"].get("msg_id") == msg_id: + reply = True + if r["content"]["status"] == "error": + c = r["content"] + raise CellFailed(f"{c.get('ename')}: {c.get('evalue')}\n" + "\n".join(c.get("traceback", []))) def main(): @@ -26,6 +73,7 @@ def main(): ap.add_argument("--timeout", type=int, default=1800, help="per-cell timeout (s)") ap.add_argument("--skip-bootstrap", action="store_true") ap.add_argument("--out", type=Path, help="write the executed notebook here") + ap.add_argument("-v", "--verbose", action="store_true", help="print each cell as it runs") args = ap.parse_args() nb = nbformat.read(args.notebook, as_version=4) @@ -34,24 +82,35 @@ def main(): nb.cells = [c for c in nb.cells if "colab_bootstrap.sh" not in c.source] print(f"Skipped {n - len(nb.cells)} bootstrap cell(s).") - client = NotebookClient( - nb, - timeout=args.timeout, - kernel_name="python3", - resources={"metadata": {"path": str(args.notebook.parent)}}, - ) + km, kc = start_new_kernel(kernel_name="python3", cwd=str(args.notebook.parent.resolve())) t0 = time.time() + status = 0 try: - client.execute() - except CellExecutionError as e: - print(f"FAIL: {args.notebook} after {time.time() - t0:.0f}s\n{e}") - return 1 + for i, cell in enumerate(nb.cells): + if cell.cell_type != "code": + continue + if args.verbose: + head = cell.source.strip().splitlines()[0][:70] if cell.source.strip() else "" + print(f"[{i:3}] {head}", end=" ", flush=True) + t1 = time.time() + try: + run_cell(kc, cell, args.timeout) + except CellFailed as e: + print(f"\nFAIL: {args.notebook} at cell {i} after {time.time() - t0:.0f}s: {e}") + print("-" * 19 + f"\n{cell.source}\n" + "-" * 19) + status = 1 + break + if args.verbose: + print(f"{time.time() - t1:6.1f}s", flush=True) finally: + kc.stop_channels() + km.shutdown_kernel(now=True) if args.out: nbformat.write(nb, args.out) - ncode = sum(c.cell_type == "code" for c in nb.cells) - print(f"OK: {args.notebook}: {ncode} code cells in {time.time() - t0:.0f}s") - return 0 + if status == 0: + ncode = sum(c.cell_type == "code" for c in nb.cells) + print(f"OK: {args.notebook}: {ncode} code cells in {time.time() - t0:.0f}s") + return status if __name__ == "__main__":