From f811344972669731bee75756f136af13e1c99228 Mon Sep 17 00:00:00 2001 From: Ben Dichter Date: Fri, 21 Aug 2026 13:07:42 -0400 Subject: [PATCH] Publish a machine-readable index at notebooks.json Alongside the HTML index, the generator now writes notebooks.json: per dandiset, each notebook's path with its GitHub, Colab, and docker links and the ready-to-run docker command (null where a path does not apply), plus site-level URLs and a schema version. It is a stable contract for other sites, in particular dandiset landing pages on dandiarchive.org, to show the notebooks that exist for a dataset without any per-dandiset metadata work. Co-Authored-By: Claude Fable 5 --- .github/scripts/collect_and_render.py | 40 +++++++++++++++++++++++++++ README.md | 4 ++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/.github/scripts/collect_and_render.py b/.github/scripts/collect_and_render.py index c447c5b..a98e2f8 100644 --- a/.github/scripts/collect_and_render.py +++ b/.github/scripts/collect_and_render.py @@ -1,3 +1,4 @@ +import datetime import fnmatch import json import os @@ -211,6 +212,42 @@ def collect_metadata() -> List[Dict[str, Any]]: return dandisets +SITE_URL = "https://notebooks.dandiarchive.org" + + +def machine_readable_index(dandisets: List[Dict[str, Any]]) -> Dict[str, Any]: + """A stable JSON view of the index for other sites (e.g. dandiset landing + pages) to consume. Keys are additive; existing ones should not change.""" + out: Dict[str, Any] = {} + for ds in dandisets: + notebooks = [] + for nb in ds["notebooks"]: + repo_rel = f"{ds['id']}/{nb['path']}" + entry = { + "path": repo_rel, + "github_url": f"https://github.com/dandi/example-notebooks/blob/master/{repo_rel}", + "colab_url": nb["colab_url"] or None, + "docker_image": nb["docker_image"] or None, + "docker_command": ( + f"docker run --rm -p 127.0.0.1:8888:8888 {nb['docker_image']}:latest" + if nb["docker_image"] else None + ), + } + notebooks.append(entry) + out[ds["id"]] = { + "index_url": f"{SITE_URL}/#dandiset-{ds['id']}", + "notebooks": notebooks, + } + return { + "schema_version": 1, + "generated": datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds"), + "source": "https://github.com/dandi/example-notebooks", + "index_url": f"{SITE_URL}/", + "docker_help_url": f"{SITE_URL}/docker-help.html", + "dandisets": out, + } + + def render_webpage(dandisets: List[Dict[str, Any]]) -> None: """ Render the webpage using the collected dandiset information. @@ -248,6 +285,9 @@ def render_webpage(dandisets: List[Dict[str, Any]]) -> None: with open(os.path.join(output_dir, 'docker-help.html'), 'w') as f: f.write(help_template.render(example_image="001550-paganlab")) + with open(os.path.join(output_dir, 'notebooks.json'), 'w') as f: + json.dump(machine_readable_index(dandisets), f, indent=2) + assets_dir = os.path.join(template_dir, 'assets') if os.path.isdir(assets_dir): shutil.copytree(assets_dir, output_dir, dirs_exist_ok=True) diff --git a/README.md b/README.md index 06f14ee..4d49259 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,9 @@ that as a bound in `requirements.in` (e.g. `matplotlib<3.11`). > **Note:** notebooks are automatically tested in CI, made runnable in Google > Colab, and published as self-contained [container -> images](.github/docker/README.md). Before opening a PR, see **[Adding a +> images](.github/docker/README.md). The site also publishes a machine-readable +> index at (per dandiset: +> notebook paths with GitHub, Colab, and docker links) for other sites to embed. Before opening a PR, see **[Adding a > notebook: CI, Colab, and the exclusion lists](docs/adding-notebooks.md)** > for how the CI test works, headless-execution gotchas, and the `.github` > exclusion lists. (Some older submissions carry an `environment.yml` instead