mkdocs-jupyterlite 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ name: docs
2
+ on:
3
+ workflow_dispatch:
4
+ push:
5
+ branches: [main]
6
+ permissions:
7
+ contents: write
8
+ jobs:
9
+ deploy:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ with:
14
+ fetch-depth: 0
15
+
16
+ - name: Configure Git Credentials
17
+ run: |
18
+ git config user.name github-actions[bot]
19
+ git config user.email github-actions[bot]@users.noreply.github.com
20
+
21
+ - uses: actions/setup-python@v5
22
+ with:
23
+ python-version: "3.13"
24
+
25
+ - name: Install UV
26
+ run: curl -LsSf https://astral.sh/uv/0.4.18/install.sh | sh
27
+
28
+ - run: uv run mkdocs gh-deploy
@@ -0,0 +1,30 @@
1
+
2
+ name: publish
3
+ on:
4
+ workflow_dispatch:
5
+ jobs:
6
+ pypi-publish:
7
+ name: Upload release to PyPI
8
+ runs-on: ubuntu-latest
9
+ environment:
10
+ name: pypi
11
+ url: https://pypi.org/p/<your-pypi-project-name>
12
+ permissions:
13
+ id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ with:
17
+ fetch-depth: 0
18
+
19
+ - uses: actions/setup-python@v5
20
+ with:
21
+ python-version: "3.13"
22
+
23
+ - name: Install UV
24
+ run: curl -LsSf https://astral.sh/uv/0.4.18/install.sh | sh
25
+
26
+ - name: Build package distributions
27
+ run: uv build
28
+
29
+ - name: Publish package distributions to PyPI
30
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,12 @@
1
+ site
2
+
3
+ # Python-generated files
4
+ __pycache__/
5
+ *.py[oc]
6
+ build/
7
+ dist/
8
+ wheels/
9
+ *.egg-info
10
+
11
+ # Virtual environments
12
+ .venv
@@ -0,0 +1,55 @@
1
+ Metadata-Version: 2.4
2
+ Name: mkdocs-jupyterlite
3
+ Version: 0.1.0
4
+ Summary: Embed interactive JupyterLite notebooks in your MkDocs site.
5
+ Project-URL: Documentation, https://github.com/NickCrews/mkdocs-jupyterlite#readme
6
+ Project-URL: Homepage, https://github.com/NickCrews/mkdocs-jupyterlite
7
+ Project-URL: Source, https://github.com/NickCrews/mkdocs-jupyterlite
8
+ Project-URL: Tracker, https://github.com/NickCrews/mkdocs-jupyterlite/issues
9
+ Author-email: Nick Crews <nicholas.b.crews@gmail.com>
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: jupyterlab-server>=2.27.3
12
+ Requires-Dist: jupyterlite-core>=0.6.4
13
+ Requires-Dist: jupyterlite-pyodide-kernel>=0.6.1
14
+ Requires-Dist: mkdocs>=1.6.1
15
+ Description-Content-Type: text/markdown
16
+
17
+ # mkdocs-jupyterlite
18
+
19
+ A MkDocs plugin that enables embedding interactive jupyterlite notebooks in your docs.
20
+
21
+ Say you have a notebook `example.ipynb` in your awesome project, and you want
22
+ users to be able to play around with it.
23
+ In the past, you could use a tool like [Binder](https://mybinder.org/) to achieve this.
24
+ But, that requires a full docker environment and a remote server.
25
+ By using [JupyterLite](https://jupyterlite.readthedocs.io/),
26
+ you can run Jupyter notebooks directly in the browser without any server-side dependencies.
27
+
28
+ However, to use jupyterlite, you have to manually install jupyterlite and
29
+ then run a build step to package your notebooks, other files, and python
30
+ dependencies into a single static site.
31
+
32
+ This plugin automates that process for you.
33
+
34
+ ## Installation
35
+
36
+ 1. Install the plugin
37
+
38
+ ```bash
39
+ pip install mkdocs-jupyterlite
40
+ ```
41
+
42
+ 2. Configure in your `mkdocs.yml` file
43
+
44
+ ```yaml
45
+ plugins:
46
+ - search
47
+ - mkdocstrings
48
+ - etc
49
+ - jupyterlite:
50
+ enabled: true
51
+ notebook_patterns:
52
+ - "**/*.ipynb"
53
+ pip_urls:
54
+ - "https://pypi.org/simple"
55
+ ```
@@ -0,0 +1,39 @@
1
+ # mkdocs-jupyterlite
2
+
3
+ A MkDocs plugin that enables embedding interactive jupyterlite notebooks in your docs.
4
+
5
+ Say you have a notebook `example.ipynb` in your awesome project, and you want
6
+ users to be able to play around with it.
7
+ In the past, you could use a tool like [Binder](https://mybinder.org/) to achieve this.
8
+ But, that requires a full docker environment and a remote server.
9
+ By using [JupyterLite](https://jupyterlite.readthedocs.io/),
10
+ you can run Jupyter notebooks directly in the browser without any server-side dependencies.
11
+
12
+ However, to use jupyterlite, you have to manually install jupyterlite and
13
+ then run a build step to package your notebooks, other files, and python
14
+ dependencies into a single static site.
15
+
16
+ This plugin automates that process for you.
17
+
18
+ ## Installation
19
+
20
+ 1. Install the plugin
21
+
22
+ ```bash
23
+ pip install mkdocs-jupyterlite
24
+ ```
25
+
26
+ 2. Configure in your `mkdocs.yml` file
27
+
28
+ ```yaml
29
+ plugins:
30
+ - search
31
+ - mkdocstrings
32
+ - etc
33
+ - jupyterlite:
34
+ enabled: true
35
+ notebook_patterns:
36
+ - "**/*.ipynb"
37
+ pip_urls:
38
+ - "https://pypi.org/simple"
39
+ ```
@@ -0,0 +1,39 @@
1
+ # mkdocs-jupyterlite
2
+
3
+ A MkDocs plugin that enables embedding interactive jupyterlite notebooks in your docs.
4
+
5
+ Say you have a notebook `example.ipynb` in your awesome project, and you want
6
+ users to be able to play around with it.
7
+ In the past, you could use a tool like [Binder](https://mybinder.org/) to achieve this.
8
+ But, that requires a full docker environment and a remote server.
9
+ By using [JupyterLite](https://jupyterlite.readthedocs.io/),
10
+ you can run Jupyter notebooks directly in the browser without any server-side dependencies.
11
+
12
+ However, to use jupyterlite, you have to manually install jupyterlite and
13
+ then run a build step to package your notebooks, other files, and python
14
+ dependencies into a single static site.
15
+
16
+ This plugin automates that process for you.
17
+
18
+ ## Installation
19
+
20
+ ### Step 1: Install the `mkdocs-jupyterlite` package
21
+
22
+ ```bash
23
+ pip install mkdocs-jupyterlite
24
+ ```
25
+
26
+ ### Step 2: Configure your `mkdocs.yml` file
27
+
28
+ ```yaml
29
+ plugins:
30
+ - search
31
+ - mkdocstrings
32
+ - etc
33
+ - jupyterlite:
34
+ enabled: true
35
+ notebook_patterns:
36
+ - "**/*.ipynb"
37
+ pip_urls:
38
+ - "https://pypi.org/simple"
39
+ ```
@@ -0,0 +1,57 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "e311f1e4",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Notebook 1"
9
+ ]
10
+ },
11
+ {
12
+ "cell_type": "code",
13
+ "execution_count": null,
14
+ "id": "2de0b9fe",
15
+ "metadata": {
16
+ "vscode": {
17
+ "languageId": "plaintext"
18
+ }
19
+ },
20
+ "outputs": [],
21
+ "source": [
22
+ "x = 1"
23
+ ]
24
+ },
25
+ {
26
+ "cell_type": "markdown",
27
+ "id": "a6b80ff4",
28
+ "metadata": {},
29
+ "source": [
30
+ "## Heading 2"
31
+ ]
32
+ },
33
+ {
34
+ "cell_type": "code",
35
+ "execution_count": null,
36
+ "id": "28f91d4a",
37
+ "metadata": {
38
+ "vscode": {
39
+ "languageId": "plaintext"
40
+ }
41
+ },
42
+ "outputs": [],
43
+ "source": [
44
+ "import ipywidgets\n",
45
+ "\n",
46
+ "ipywidgets.IntSlider()"
47
+ ]
48
+ }
49
+ ],
50
+ "metadata": {
51
+ "language_info": {
52
+ "name": "python"
53
+ }
54
+ },
55
+ "nbformat": 4,
56
+ "nbformat_minor": 5
57
+ }
@@ -0,0 +1,14 @@
1
+ site_name: mkdocs-jupyterlite
2
+ site_url: https://nickcrews.github.io/mkdocs-jupyterlite/
3
+ repo_url: https://github.com/nickcrews/mkdocs-jupyterlite/
4
+
5
+ nav:
6
+ - Home: index.md
7
+ - Notebook 1: notebook.ipynb
8
+
9
+ plugins:
10
+ - jupyterlite:
11
+ enabled: true
12
+ notebook_patterns:
13
+ - '*.ipynb'
14
+ pip_urls: []
@@ -0,0 +1,36 @@
1
+ [project]
2
+ name = "mkdocs-jupyterlite"
3
+ version = "0.1.0"
4
+ description = "Embed interactive JupyterLite notebooks in your MkDocs site."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Nick Crews", email = "nicholas.b.crews@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "jupyterlab-server>=2.27.3",
12
+ "jupyterlite-core>=0.6.4",
13
+ "jupyterlite-pyodide-kernel>=0.6.1",
14
+ "mkdocs>=1.6.1",
15
+ ]
16
+
17
+ [project.urls]
18
+ Documentation = "https://github.com/NickCrews/mkdocs-jupyterlite#readme"
19
+ Homepage = "https://github.com/NickCrews/mkdocs-jupyterlite"
20
+ Source = "https://github.com/NickCrews/mkdocs-jupyterlite"
21
+ Tracker = "https://github.com/NickCrews/mkdocs-jupyterlite/issues"
22
+
23
+ [project.entry-points."mkdocs.plugins"]
24
+ jupyterlite = "mkdocs_jupyterlite:JupyterlitePlugin"
25
+
26
+ [build-system]
27
+ requires = ["hatchling"]
28
+ build-backend = "hatchling.build"
29
+
30
+ [dependency-groups]
31
+ dev = [
32
+ "pytest>=8.4.1",
33
+ ]
34
+ lint = [
35
+ "ruff>=0.12.8",
36
+ ]
@@ -0,0 +1,13 @@
1
+ import importlib.metadata
2
+ import warnings
3
+
4
+ from mkdocs_jupyterlite._plugin import JupyterlitePlugin as JupyterlitePlugin
5
+ from mkdocs_jupyterlite._plugin import (
6
+ JupyterlitePluginConfig as JupyterlitePluginConfig,
7
+ )
8
+
9
+ try:
10
+ __version__ = importlib.metadata.version(__name__)
11
+ except importlib.metadata.PackageNotFoundError as e:
12
+ warnings.warn(f"Could not determine version of {__name__}\n{e!s}", stacklevel=2)
13
+ __version__ = "unknown"
@@ -0,0 +1,74 @@
1
+ import json
2
+ import logging
3
+ import shutil
4
+ import subprocess
5
+ import tempfile
6
+ from collections.abc import Iterable
7
+ from pathlib import Path
8
+
9
+ log = logging.getLogger("mkdocs.plugins.jupyterlite")
10
+
11
+
12
+ def build_site(
13
+ *,
14
+ notebooks: Iterable[Path],
15
+ pip_urls: Iterable[str],
16
+ output_dir: Path,
17
+ ) -> None:
18
+ shutil.rmtree(output_dir, ignore_errors=True)
19
+ output_dir.mkdir(parents=True, exist_ok=True)
20
+ with tempfile.TemporaryDirectory() as working_dir_str:
21
+ working_dir = Path(working_dir_str)
22
+ write_jupyter_lite_config(
23
+ out_path=working_dir / "jupyter_lite_config.json",
24
+ pip_urls=pip_urls,
25
+ )
26
+ contents_args = []
27
+ for notebook in notebooks:
28
+ contents_args.extend(["--contents", str(notebook)])
29
+ cmd = [
30
+ "jupyter",
31
+ "lite",
32
+ "build",
33
+ # *(["--debug"] if debug else []),
34
+ "--debug",
35
+ *contents_args,
36
+ "--no-libarchive",
37
+ "--apps",
38
+ "notebooks",
39
+ "--no-unused-shared-packages",
40
+ "--output-dir",
41
+ str(output_dir),
42
+ ]
43
+ log.info("[jupyterlite] running build command: " + " ".join(cmd))
44
+ try:
45
+ result = subprocess.run(
46
+ cmd,
47
+ # capture_output=True,
48
+ text=True,
49
+ cwd=working_dir,
50
+ check=True,
51
+ )
52
+ if result.stdout:
53
+ log.debug("[jupyterlite] build output:\n" + result.stdout)
54
+ except subprocess.CalledProcessError as e:
55
+ log.error("[jupyterlite] build failed")
56
+ if e.stdout:
57
+ log.debug("[jupyterlite] build stdout:\n" + e.stdout)
58
+ if e.stderr:
59
+ log.error("[jupyterlite] build stderr:\n" + e.stderr)
60
+ raise
61
+ assert output_dir.exists(), "Output directory was not created"
62
+
63
+
64
+ def write_jupyter_lite_config(
65
+ *,
66
+ out_path: Path,
67
+ pip_urls: Iterable[str],
68
+ ) -> None:
69
+ config = {
70
+ "JupyterLiteAddon": {
71
+ "piplite_urls": list(pip_urls),
72
+ }
73
+ }
74
+ out_path.write_text(json.dumps(config, indent=2))
@@ -0,0 +1,162 @@
1
+ import fnmatch
2
+ import logging
3
+ import shutil
4
+ import tempfile
5
+ from collections.abc import Iterable
6
+ from pathlib import Path
7
+ from typing import Any, Literal
8
+
9
+ import markdown
10
+ import nbformat
11
+ from mkdocs.config.base import Config as BaseConfig
12
+ from mkdocs.config.config_options import Type as OptionType
13
+ from mkdocs.config.defaults import MkDocsConfig
14
+ from mkdocs.plugins import BasePlugin
15
+ from mkdocs.structure.files import File, Files
16
+ from mkdocs.structure.pages import Page
17
+ from mkdocs.structure.toc import TableOfContents, get_toc
18
+ from nbconvert import MarkdownExporter
19
+
20
+ from mkdocs_jupyterlite import _build
21
+
22
+ log = logging.getLogger("mkdocs.plugins.jupyterlite")
23
+
24
+
25
+ class NotebookFile(File):
26
+ """
27
+ Wraps a regular File object to make .ipynb files appear as valid documentation files.
28
+ """
29
+
30
+ def __init__(self, file: File) -> None:
31
+ self._file = file
32
+
33
+ def __getattr__(self, name: str) -> Any:
34
+ return self._file.__getattribute__(name)
35
+
36
+ def is_documentation_page(self) -> Literal[True]:
37
+ return True
38
+
39
+
40
+ class JupyterlitePluginConfig(BaseConfig):
41
+ enabled = OptionType(bool, default=True)
42
+ notebook_patterns = OptionType(list, default=[])
43
+ pip_urls = OptionType(list, default=[])
44
+
45
+
46
+ class JupyterlitePlugin(BasePlugin[JupyterlitePluginConfig]):
47
+ def __init__(self):
48
+ super().__init__()
49
+ if isinstance(self.config, dict):
50
+ plugin_config = JupyterlitePluginConfig()
51
+ plugin_config.load_dict(self.config)
52
+ self.config = plugin_config
53
+ self._jupyterlite_build_dir = tempfile.TemporaryDirectory()
54
+
55
+ def _cleanup(self) -> None:
56
+ log.info(
57
+ "[jupyterlite] cleaning up temporary build directory: "
58
+ + str(self._jupyterlite_build_dir.name)
59
+ )
60
+ self._jupyterlite_build_dir.cleanup()
61
+
62
+ def on_files(self, files: Files, config: MkDocsConfig) -> Files:
63
+ outfiles = []
64
+ notebook_paths = []
65
+ for file in files:
66
+ if is_notebook(
67
+ relative_path=file.src_uri,
68
+ notebook_patterns=self.config.notebook_patterns,
69
+ ):
70
+ log.info("[jupyterlite] including notebook: " + str(file.abs_src_path))
71
+ outfiles.append(NotebookFile(file))
72
+ notebook_paths.append(file.abs_src_path)
73
+ else:
74
+ log.debug("[jupyterlite] ignoring file: " + str(file.abs_src_path))
75
+ outfiles.append(file)
76
+ notebooks = [Path(config.docs_dir) / p for p in notebook_paths]
77
+ _build.build_site(
78
+ notebooks=notebooks,
79
+ pip_urls=self.config.pip_urls,
80
+ output_dir=Path(self._jupyterlite_build_dir.name),
81
+ )
82
+ return Files(outfiles)
83
+
84
+ def on_pre_page(
85
+ self, page: Page, /, *, config: MkDocsConfig, files: Files
86
+ ) -> Page | None:
87
+ if not isinstance(page.file, NotebookFile):
88
+ return page
89
+ log.info("[jupyterlite] on_pre_page " + str(page.file.src_uri))
90
+
91
+ def new_render(self: Page, config: MkDocsConfig, files: Files) -> None:
92
+ body = f"""
93
+ <iframe src="{config.site_url}jupyterlite/notebooks/index.html?path={page.file.src_uri}"
94
+ width="100%"
95
+ height="800px"
96
+ frameborder="1">
97
+ </iframe>
98
+ """
99
+ self.content = body
100
+ toc, title_in_notebook = get_nb_toc_and_title(page.file.abs_src_path)
101
+ self.toc = toc
102
+ if title_in_notebook:
103
+ self.title = title_in_notebook
104
+
105
+ # replace render with new_render for this object only
106
+ page.render = new_render.__get__(page, Page)
107
+ return page
108
+
109
+ def on_post_build(self, config: MkDocsConfig) -> None:
110
+ shutil.copytree(
111
+ self._jupyterlite_build_dir.name,
112
+ Path(config.site_dir) / "jupyterlite",
113
+ dirs_exist_ok=True,
114
+ )
115
+ self._cleanup()
116
+
117
+ def on_build_error(self, *, error: Exception) -> None:
118
+ self._cleanup()
119
+
120
+
121
+ def is_notebook(*, relative_path: str | Path, notebook_patterns: Iterable[str]) -> bool:
122
+ for pattern in notebook_patterns:
123
+ if fnmatch.fnmatch(relative_path, pattern):
124
+ return True
125
+ return False
126
+
127
+
128
+ # Hooks for development
129
+ def on_startup(command: str, dirty: bool) -> None:
130
+ log.info("[jupyterlite][development] plugin started.")
131
+
132
+
133
+ def on_page_markdown(markdown: str, page: Any, config: MkDocsConfig, files: Any) -> str:
134
+ log.info("[jupyterlite][development] plugin started.")
135
+ plugin = JupyterlitePlugin()
136
+ return plugin.on_page_markdown(markdown, page=page, config=config, files=files)
137
+
138
+
139
+ def on_post_page(output: str, page: Page, config: MkDocsConfig) -> str:
140
+ log.info("[jupyterlite][development] plugin started.")
141
+ plugin = JupyterlitePlugin()
142
+ return plugin.on_post_page(output, page=page, config=config)
143
+
144
+
145
+ def on_files(files: Files, config: MkDocsConfig) -> Files:
146
+ log.info("[jupyterlite][development] plugin started.")
147
+ plugin = JupyterlitePlugin()
148
+ return plugin.on_files(files, config)
149
+
150
+
151
+ def get_nb_toc_and_title(path: str | Path) -> tuple[TableOfContents, str | None]:
152
+ """Returns a TOC and title (the first heading, if present) for the Notebook."""
153
+ notebook = nbformat.reads(Path(path).read_text(), as_version=4)
154
+ (markdown_source, _resources) = MarkdownExporter().from_notebook_node(notebook)
155
+ md = markdown.Markdown(extensions=["toc"])
156
+ md.convert(markdown_source)
157
+ toc = get_toc(md.toc_tokens)
158
+ title = None
159
+ for token in md.toc_tokens:
160
+ if token["level"] == 1 and title is None:
161
+ title = token["name"]
162
+ return toc, title