nbsync 0.0.1__py3-none-any.whl → 0.1.0__py3-none-any.whl
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.
- nbsync/cell.py +113 -0
- nbsync/logger.py +5 -0
- nbsync/markdown.py +112 -0
- nbsync/notebook.py +39 -0
- nbsync/plugin.py +96 -0
- nbsync/sync.py +121 -0
- nbsync-0.1.0.dist-info/METADATA +177 -0
- nbsync-0.1.0.dist-info/RECORD +12 -0
- nbsync-0.1.0.dist-info/entry_points.txt +2 -0
- nbsync-0.1.0.dist-info/licenses/LICENSE +21 -0
- nbsync-0.0.1.dist-info/METADATA +0 -29
- nbsync-0.0.1.dist-info/RECORD +0 -5
- nbsync-0.0.1.dist-info/licenses/LICENSE +0 -9
- {nbsync-0.0.1.dist-info → nbsync-0.1.0.dist-info}/WHEEL +0 -0
nbsync/cell.py
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import textwrap
|
4
|
+
import uuid
|
5
|
+
from dataclasses import dataclass
|
6
|
+
from typing import TYPE_CHECKING
|
7
|
+
|
8
|
+
from nbsync.markdown import is_truelike
|
9
|
+
|
10
|
+
from .logger import logger
|
11
|
+
|
12
|
+
if TYPE_CHECKING:
|
13
|
+
from nbstore.markdown import Image
|
14
|
+
|
15
|
+
|
16
|
+
@dataclass
|
17
|
+
class Cell:
|
18
|
+
image: Image
|
19
|
+
"""The image instance from the Markdown file."""
|
20
|
+
|
21
|
+
language: str
|
22
|
+
"""The language of the source to be used to generate the image."""
|
23
|
+
|
24
|
+
mime: str
|
25
|
+
"""The MIME type of the image."""
|
26
|
+
|
27
|
+
content: bytes | str
|
28
|
+
"""The content of the image."""
|
29
|
+
|
30
|
+
def convert(self) -> str:
|
31
|
+
kind = self.image.attributes.pop("source", "")
|
32
|
+
tabs = self.image.attributes.pop("tabs", "")
|
33
|
+
|
34
|
+
if "/" not in self.mime or not self.content or kind == "source-only":
|
35
|
+
if self.image.source:
|
36
|
+
source = get_source(self, include_attrs=True)
|
37
|
+
kind = "only"
|
38
|
+
else:
|
39
|
+
source = ""
|
40
|
+
result, self.image.url = "", ""
|
41
|
+
|
42
|
+
elif self.mime.startswith("text/") and isinstance(self.content, str):
|
43
|
+
source = get_source(self, include_attrs=True)
|
44
|
+
result, self.image.url = self.content, ""
|
45
|
+
result = result.rstrip()
|
46
|
+
|
47
|
+
else:
|
48
|
+
source = get_source(self, include_attrs=False)
|
49
|
+
result = get_result(self)
|
50
|
+
|
51
|
+
if markdown := get_markdown(kind, source, result, tabs):
|
52
|
+
return textwrap.indent(markdown, self.image.indent)
|
53
|
+
|
54
|
+
return "" # no cov
|
55
|
+
|
56
|
+
|
57
|
+
def get_source(cell: Cell, *, include_attrs: bool = False) -> str:
|
58
|
+
attrs = [cell.language]
|
59
|
+
if include_attrs:
|
60
|
+
attrs.extend(cell.image.iter_parts())
|
61
|
+
attr = " ".join(attrs)
|
62
|
+
return f"```{attr}\n{cell.image.source}\n```"
|
63
|
+
|
64
|
+
|
65
|
+
def get_result(cell: Cell) -> str:
|
66
|
+
msg = f"{cell.image.url}#{cell.image.identifier} [{cell.mime}]"
|
67
|
+
logger.debug(f"Converting image: {msg}")
|
68
|
+
|
69
|
+
ext = cell.mime.split("/")[1].split("+")[0]
|
70
|
+
cell.image.url = f"{uuid.uuid4()}.{ext}"
|
71
|
+
|
72
|
+
attr = " ".join(cell.image.iter_parts(include_identifier=True))
|
73
|
+
return f"{{{attr}}}"
|
74
|
+
|
75
|
+
|
76
|
+
def get_markdown(kind: str, source: str, result: str, tabs: str) -> str:
|
77
|
+
if all(not x for x in (kind, source, result)):
|
78
|
+
return ""
|
79
|
+
|
80
|
+
if not kind or not source:
|
81
|
+
return result
|
82
|
+
|
83
|
+
if kind == "only":
|
84
|
+
return source
|
85
|
+
|
86
|
+
if is_truelike(kind) or kind == "above":
|
87
|
+
return f"{source}\n\n{result}"
|
88
|
+
|
89
|
+
if kind == "below":
|
90
|
+
return f"{result}\n\n{source}"
|
91
|
+
|
92
|
+
if kind == "material-block":
|
93
|
+
result = f'<div class="result" markdown="1">{result}</div>'
|
94
|
+
return f"{source}\n\n{result}"
|
95
|
+
|
96
|
+
if kind == "tabbed-left":
|
97
|
+
tabs = tabs if "|" in tabs else "Source|Result"
|
98
|
+
return get_tabbed(source, result, tabs)
|
99
|
+
|
100
|
+
if kind == "tabbed-right":
|
101
|
+
tabs = tabs if "|" in tabs else "Result|Source"
|
102
|
+
return get_tabbed(result, source, tabs)
|
103
|
+
|
104
|
+
return result
|
105
|
+
|
106
|
+
|
107
|
+
def get_tabbed(left: str, right: str, tabs: str) -> str:
|
108
|
+
left_title, right_title = tabs.split("|", 1)
|
109
|
+
left = textwrap.indent(left, " ")
|
110
|
+
left = f'===! "{left_title}"\n\n{left}'
|
111
|
+
right = textwrap.indent(right, " ")
|
112
|
+
right = f'=== "{right_title}"\n\n{right}'
|
113
|
+
return f"{left}\n\n{right}\n"
|
nbsync/logger.py
ADDED
nbsync/markdown.py
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import textwrap
|
4
|
+
from typing import TYPE_CHECKING, TypeAlias
|
5
|
+
|
6
|
+
import nbstore.markdown
|
7
|
+
from nbstore.markdown import CodeBlock, Image
|
8
|
+
|
9
|
+
if TYPE_CHECKING:
|
10
|
+
from collections.abc import Iterable, Iterator
|
11
|
+
|
12
|
+
Element: TypeAlias = str | CodeBlock | Image
|
13
|
+
|
14
|
+
|
15
|
+
def convert_image(image: Image, index: int | None = None) -> Iterator[Element]:
|
16
|
+
if image.source:
|
17
|
+
if not image.identifier and index is None:
|
18
|
+
msg = "index is required when source is present and identifier is not set"
|
19
|
+
raise ValueError(msg)
|
20
|
+
|
21
|
+
image.identifier = image.identifier or f"image-nbsync-{index}"
|
22
|
+
yield CodeBlock("", image.identifier, [], {}, image.source, image.url)
|
23
|
+
yield image
|
24
|
+
|
25
|
+
elif image.identifier:
|
26
|
+
yield image
|
27
|
+
|
28
|
+
else:
|
29
|
+
yield image.text
|
30
|
+
|
31
|
+
|
32
|
+
def convert_code_block(code_block: CodeBlock) -> Iterator[Element]:
|
33
|
+
source = code_block.attributes.get("source", None)
|
34
|
+
if source == "tabbed-nbsync":
|
35
|
+
yield from _convert_code_block_tabbed(code_block)
|
36
|
+
else:
|
37
|
+
yield code_block
|
38
|
+
|
39
|
+
|
40
|
+
def _convert_code_block_tabbed(code_block: CodeBlock) -> Iterator[Element]:
|
41
|
+
markdown = code_block.text.replace('source="tabbed-nbsync"', "")
|
42
|
+
markdown = textwrap.indent(markdown, " ")
|
43
|
+
yield f'===! "Markdown"\n\n{markdown}\n\n'
|
44
|
+
|
45
|
+
text = textwrap.indent(code_block.source, " ")
|
46
|
+
text = f'=== "Rendered"\n\n{text}'
|
47
|
+
yield from nbstore.markdown.parse(text)
|
48
|
+
|
49
|
+
|
50
|
+
SUPPORTED_EXTENSIONS = (".ipynb", ".md", ".py")
|
51
|
+
|
52
|
+
|
53
|
+
def set_url(elem: Image | CodeBlock, url: str) -> tuple[Element, str]:
|
54
|
+
"""Set the URL of the image or code block.
|
55
|
+
|
56
|
+
If the URL is empty or ".", set the URL to the current URL.
|
57
|
+
"""
|
58
|
+
if elem.url in ["", "."] and url:
|
59
|
+
elem.url = url
|
60
|
+
return elem, url
|
61
|
+
|
62
|
+
if elem.url.endswith(SUPPORTED_EXTENSIONS):
|
63
|
+
return elem, elem.url
|
64
|
+
|
65
|
+
return elem.text, url
|
66
|
+
|
67
|
+
|
68
|
+
def resolve_urls(elems: Iterable[Element]) -> Iterator[Element]:
|
69
|
+
"""Parse the URL of the image or code block.
|
70
|
+
|
71
|
+
If a code block has no URL, yield the text of the code block,
|
72
|
+
which means that the code block is not processed further.
|
73
|
+
"""
|
74
|
+
url = ""
|
75
|
+
|
76
|
+
for elem in elems:
|
77
|
+
if isinstance(elem, CodeBlock) and not elem.url:
|
78
|
+
yield elem.text
|
79
|
+
|
80
|
+
elif isinstance(elem, Image | CodeBlock):
|
81
|
+
elem_, url = set_url(elem, url)
|
82
|
+
yield elem_
|
83
|
+
|
84
|
+
else:
|
85
|
+
yield elem
|
86
|
+
|
87
|
+
|
88
|
+
def convert_code_blocks(elems: Iterable[Element]) -> Iterator[Element]:
|
89
|
+
for elem in elems:
|
90
|
+
if isinstance(elem, CodeBlock):
|
91
|
+
yield from convert_code_block(elem)
|
92
|
+
else:
|
93
|
+
yield elem
|
94
|
+
|
95
|
+
|
96
|
+
def convert_images(elems: Iterable[Element]) -> Iterator[Element]:
|
97
|
+
for index, elem in enumerate(elems):
|
98
|
+
if isinstance(elem, Image):
|
99
|
+
yield from convert_image(elem, index)
|
100
|
+
else:
|
101
|
+
yield elem
|
102
|
+
|
103
|
+
|
104
|
+
def parse(text: str) -> Iterator[Element]:
|
105
|
+
elems = nbstore.markdown.parse(text)
|
106
|
+
elems = convert_code_blocks(elems)
|
107
|
+
elems = resolve_urls(elems)
|
108
|
+
yield from convert_images(elems)
|
109
|
+
|
110
|
+
|
111
|
+
def is_truelike(value: str | None) -> bool:
|
112
|
+
return value is not None and value.lower() in ("yes", "true", "1", "on")
|
nbsync/notebook.py
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import copy
|
4
|
+
from typing import TYPE_CHECKING
|
5
|
+
|
6
|
+
import nbstore.notebook
|
7
|
+
|
8
|
+
if TYPE_CHECKING:
|
9
|
+
from nbformat import NotebookNode
|
10
|
+
|
11
|
+
|
12
|
+
class Notebook:
|
13
|
+
nb: NotebookNode
|
14
|
+
is_modified: bool
|
15
|
+
execution_needed: bool
|
16
|
+
|
17
|
+
def __init__(self, nb: NotebookNode) -> None:
|
18
|
+
self.nb = nb
|
19
|
+
self.is_modified = False
|
20
|
+
self.execution_needed = False
|
21
|
+
|
22
|
+
def set_execution_needed(self) -> None:
|
23
|
+
self.execution_needed = True
|
24
|
+
|
25
|
+
def add_cell(self, identifier: str, source: str) -> None:
|
26
|
+
if not self.is_modified:
|
27
|
+
self.nb = copy.deepcopy(self.nb)
|
28
|
+
self.is_modified = True
|
29
|
+
|
30
|
+
cell = nbstore.notebook.new_code_cell(identifier, source)
|
31
|
+
self.nb.cells.append(cell)
|
32
|
+
self.set_execution_needed()
|
33
|
+
|
34
|
+
def equals(self, other: Notebook) -> bool:
|
35
|
+
return nbstore.notebook.equals(self.nb, other.nb)
|
36
|
+
|
37
|
+
def execute(self) -> None:
|
38
|
+
nbstore.notebook.execute(self.nb)
|
39
|
+
self.execution_needed = False
|
nbsync/plugin.py
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from pathlib import Path
|
4
|
+
from typing import TYPE_CHECKING, ClassVar
|
5
|
+
|
6
|
+
from mkdocs.config import Config as BaseConfig
|
7
|
+
from mkdocs.config import config_options
|
8
|
+
from mkdocs.plugins import BasePlugin
|
9
|
+
from mkdocs.structure.files import File
|
10
|
+
from nbstore.store import Store
|
11
|
+
|
12
|
+
from .logger import logger
|
13
|
+
from .sync import Synchronizer
|
14
|
+
|
15
|
+
if TYPE_CHECKING:
|
16
|
+
from typing import Any
|
17
|
+
|
18
|
+
from mkdocs.config.defaults import MkDocsConfig
|
19
|
+
from mkdocs.structure.files import Files
|
20
|
+
from mkdocs.structure.pages import Page
|
21
|
+
|
22
|
+
from .cell import Cell
|
23
|
+
|
24
|
+
|
25
|
+
class Config(BaseConfig):
|
26
|
+
"""Configuration for Nbstore plugin."""
|
27
|
+
|
28
|
+
src_dir = config_options.Type((str, list), default=".")
|
29
|
+
|
30
|
+
|
31
|
+
class Plugin(BasePlugin[Config]):
|
32
|
+
store: ClassVar[Store | None] = None
|
33
|
+
syncs: ClassVar[dict[str, Synchronizer]] = {}
|
34
|
+
files: Files
|
35
|
+
|
36
|
+
def on_config(self, config: MkDocsConfig, **kwargs: Any) -> MkDocsConfig:
|
37
|
+
if isinstance(self.config.src_dir, str):
|
38
|
+
src_dirs = [self.config.src_dir]
|
39
|
+
else:
|
40
|
+
src_dirs = self.config.src_dir
|
41
|
+
|
42
|
+
src_dirs = [(Path(config.docs_dir) / s).resolve() for s in src_dirs]
|
43
|
+
|
44
|
+
store = self.__class__.store
|
45
|
+
|
46
|
+
if store is None or store.src_dirs != src_dirs:
|
47
|
+
self.__class__.store = Store(src_dirs)
|
48
|
+
config.watch.extend(x.as_posix() for x in src_dirs)
|
49
|
+
|
50
|
+
for name in ["attr_list", "md_in_html"]:
|
51
|
+
if name not in config.markdown_extensions:
|
52
|
+
config.markdown_extensions.append(name)
|
53
|
+
|
54
|
+
return config
|
55
|
+
|
56
|
+
def on_files(self, files: Files, config: MkDocsConfig, **kwargs: Any) -> Files:
|
57
|
+
self.files = files
|
58
|
+
return files
|
59
|
+
|
60
|
+
def on_page_markdown(
|
61
|
+
self,
|
62
|
+
markdown: str,
|
63
|
+
page: Page,
|
64
|
+
config: MkDocsConfig,
|
65
|
+
**kwargs: Any,
|
66
|
+
) -> str:
|
67
|
+
if self.__class__.store is None:
|
68
|
+
msg = "Store must be initialized before processing markdown"
|
69
|
+
logger.error(msg)
|
70
|
+
return markdown
|
71
|
+
|
72
|
+
src_uri = page.file.src_uri
|
73
|
+
syncs = self.__class__.syncs
|
74
|
+
|
75
|
+
if src_uri not in syncs:
|
76
|
+
syncs[src_uri] = Synchronizer(self.__class__.store)
|
77
|
+
|
78
|
+
markdowns = []
|
79
|
+
|
80
|
+
for elem in syncs[src_uri].convert(markdown):
|
81
|
+
if isinstance(elem, str):
|
82
|
+
markdowns.append(elem)
|
83
|
+
|
84
|
+
elif markdown := elem.convert():
|
85
|
+
markdowns.append(markdown)
|
86
|
+
|
87
|
+
if elem.image.url and elem.content:
|
88
|
+
file = generate_file(elem, src_uri, config)
|
89
|
+
self.files.append(file)
|
90
|
+
|
91
|
+
return "".join(markdowns)
|
92
|
+
|
93
|
+
|
94
|
+
def generate_file(cell: Cell, page_uri: str, config: MkDocsConfig) -> File:
|
95
|
+
src_uri = (Path(page_uri).parent / cell.image.url).as_posix()
|
96
|
+
return File.generated(config, src_uri, content=cell.content)
|
nbsync/sync.py
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import re
|
4
|
+
import textwrap
|
5
|
+
from dataclasses import dataclass, field
|
6
|
+
from typing import TYPE_CHECKING
|
7
|
+
|
8
|
+
import nbformat
|
9
|
+
from nbstore.markdown import CodeBlock, Image
|
10
|
+
from nbstore.notebook import get_language, get_mime_content, get_source
|
11
|
+
|
12
|
+
import nbsync.markdown
|
13
|
+
from nbsync.cell import Cell
|
14
|
+
from nbsync.markdown import is_truelike
|
15
|
+
from nbsync.notebook import Notebook
|
16
|
+
|
17
|
+
from .logger import logger
|
18
|
+
|
19
|
+
if TYPE_CHECKING:
|
20
|
+
from collections.abc import Iterator
|
21
|
+
|
22
|
+
from nbformat import NotebookNode
|
23
|
+
from nbstore import Store
|
24
|
+
|
25
|
+
|
26
|
+
@dataclass
|
27
|
+
class Synchronizer:
|
28
|
+
store: Store
|
29
|
+
notebooks: dict[str, Notebook] = field(default_factory=dict, init=False)
|
30
|
+
|
31
|
+
def parse(self, text: str) -> Iterator[str | Image | CodeBlock]:
|
32
|
+
notebooks: dict[str, Notebook] = {}
|
33
|
+
|
34
|
+
for elem in nbsync.markdown.parse(text):
|
35
|
+
yield elem
|
36
|
+
|
37
|
+
if isinstance(elem, Image | CodeBlock):
|
38
|
+
update_notebooks(notebooks, elem, self.store)
|
39
|
+
|
40
|
+
for url, notebook in notebooks.items():
|
41
|
+
if url not in self.notebooks or not self.notebooks[url].equals(notebook):
|
42
|
+
self.notebooks[url] = notebook
|
43
|
+
|
44
|
+
def execute(self) -> None:
|
45
|
+
for url, notebook in self.notebooks.items():
|
46
|
+
if not notebook.execution_needed:
|
47
|
+
continue
|
48
|
+
|
49
|
+
path = ".md" if url == ".md" else self.store.find_path(url)
|
50
|
+
logger.info(f"Executing notebook: {path}")
|
51
|
+
notebook.execute()
|
52
|
+
|
53
|
+
def convert(self, text: str) -> Iterator[str | Cell]:
|
54
|
+
elems = list(self.parse(text))
|
55
|
+
self.execute()
|
56
|
+
|
57
|
+
for elem in elems:
|
58
|
+
if isinstance(elem, str):
|
59
|
+
yield elem
|
60
|
+
|
61
|
+
elif elem.identifier not in [".", "_"]:
|
62
|
+
if isinstance(elem, Image):
|
63
|
+
nb = self.notebooks[elem.url].nb
|
64
|
+
yield convert_image(elem, nb)
|
65
|
+
else:
|
66
|
+
yield convert_code_block(elem)
|
67
|
+
|
68
|
+
|
69
|
+
def update_notebooks(
|
70
|
+
notebooks: dict[str, Notebook],
|
71
|
+
elem: Image | CodeBlock,
|
72
|
+
store: Store,
|
73
|
+
) -> None:
|
74
|
+
url = elem.url
|
75
|
+
|
76
|
+
if url not in notebooks:
|
77
|
+
if url == ".md":
|
78
|
+
notebooks[url] = Notebook(nbformat.v4.new_notebook())
|
79
|
+
else:
|
80
|
+
try:
|
81
|
+
notebooks[url] = Notebook(store.read(url))
|
82
|
+
except Exception: # noqa: BLE001
|
83
|
+
logger.warning(f"Error reading notebook: {url}")
|
84
|
+
return
|
85
|
+
|
86
|
+
notebook = notebooks[url]
|
87
|
+
|
88
|
+
if is_truelike(elem.attributes.pop("exec", None)):
|
89
|
+
notebook.set_execution_needed()
|
90
|
+
|
91
|
+
if isinstance(elem, CodeBlock):
|
92
|
+
source = textwrap.dedent(elem.source)
|
93
|
+
notebook.add_cell(elem.identifier, source)
|
94
|
+
|
95
|
+
|
96
|
+
def convert_image(image: Image, nb: NotebookNode) -> Cell:
|
97
|
+
try:
|
98
|
+
image.source = get_source(nb, image.identifier)
|
99
|
+
mime_content = get_mime_content(nb, image.identifier)
|
100
|
+
except ValueError:
|
101
|
+
cell = f"{image.url}#{image.identifier}"
|
102
|
+
logger.warning(f"Error reading cell: {cell!r}")
|
103
|
+
image.source = ""
|
104
|
+
mime_content = ("", "")
|
105
|
+
|
106
|
+
return Cell(image, get_language(nb), *mime_content)
|
107
|
+
|
108
|
+
|
109
|
+
def convert_code_block(code_block: CodeBlock) -> str:
|
110
|
+
source = code_block.attributes.pop("source", None)
|
111
|
+
if not is_truelike(source):
|
112
|
+
return ""
|
113
|
+
|
114
|
+
lines = code_block.text.splitlines()
|
115
|
+
if lines:
|
116
|
+
pattern = f"\\S+#{code_block.identifier}"
|
117
|
+
lines[0] = re.sub(pattern, "", lines[0])
|
118
|
+
pattern = r"source=[^\s}]+"
|
119
|
+
lines[0] = re.sub(pattern, "", lines[0])
|
120
|
+
|
121
|
+
return "\n".join(lines)
|
@@ -0,0 +1,177 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: nbsync
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: MkDocs plugin treating Jupyter notebooks, Python scripts and Markdown files as first-class citizens for documentation with dynamic execution and real-time synchronization
|
5
|
+
Project-URL: Documentation, https://daizutabi.github.io/nbsync/
|
6
|
+
Project-URL: Source, https://github.com/daizutabi/nbsync
|
7
|
+
Project-URL: Issues, https://github.com/daizutabi/nbsync/issues
|
8
|
+
Author-email: daizutabi <daizutabi@gmail.com>
|
9
|
+
License: MIT License
|
10
|
+
|
11
|
+
Copyright (c) 2025 Daizu
|
12
|
+
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
15
|
+
in the Software without restriction, including without limitation the rights
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
18
|
+
furnished to do so, subject to the following conditions:
|
19
|
+
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
21
|
+
copies or substantial portions of the Software.
|
22
|
+
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
29
|
+
SOFTWARE.
|
30
|
+
License-File: LICENSE
|
31
|
+
Keywords: documentation,dynamic-execution,jupyter,markdown,mkdocs,notebook,python,real-time-sync,visualization
|
32
|
+
Classifier: Development Status :: 4 - Beta
|
33
|
+
Classifier: Programming Language :: Python
|
34
|
+
Classifier: Programming Language :: Python :: 3.10
|
35
|
+
Classifier: Programming Language :: Python :: 3.11
|
36
|
+
Classifier: Programming Language :: Python :: 3.12
|
37
|
+
Classifier: Programming Language :: Python :: 3.13
|
38
|
+
Classifier: Topic :: Documentation
|
39
|
+
Classifier: Topic :: Software Development :: Documentation
|
40
|
+
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
41
|
+
Requires-Python: >=3.10
|
42
|
+
Requires-Dist: mkdocs>=1.6
|
43
|
+
Requires-Dist: nbstore>=0.4.7
|
44
|
+
Description-Content-Type: text/markdown
|
45
|
+
|
46
|
+
# nbsync
|
47
|
+
|
48
|
+
[![PyPI Version][pypi-v-image]][pypi-v-link]
|
49
|
+
[![Python Version][python-v-image]][python-v-link]
|
50
|
+
[![Build Status][GHAction-image]][GHAction-link]
|
51
|
+
[![Coverage Status][codecov-image]][codecov-link]
|
52
|
+
|
53
|
+
<strong>Connect Jupyter notebooks to your MkDocs documentation</strong>
|
54
|
+
|
55
|
+
nbsync is a MkDocs plugin that seamlessly embeds Jupyter notebook
|
56
|
+
visualizations in your documentation, solving the disconnect between
|
57
|
+
code development and documentation.
|
58
|
+
|
59
|
+
## Why Use nbsync?
|
60
|
+
|
61
|
+
### The Documentation Challenge
|
62
|
+
|
63
|
+
Data scientists, researchers, and technical writers face a common dilemma:
|
64
|
+
|
65
|
+
- **Development happens in notebooks** - ideal for experimentation and visualization
|
66
|
+
- **Documentation lives in markdown** - perfect for narrative and explanation
|
67
|
+
- **Connecting the two is painful** - screenshots break, exports get outdated
|
68
|
+
|
69
|
+
### Our Solution
|
70
|
+
|
71
|
+
This plugin creates a live bridge between your notebooks and documentation by:
|
72
|
+
|
73
|
+
- **Keeping environments separate** - work in the tool best suited for each task
|
74
|
+
- **Maintaining connections** - reference specific figures from notebooks
|
75
|
+
- **Automating updates** - changes to notebooks reflect in documentation
|
76
|
+
|
77
|
+
## Key Benefits
|
78
|
+
|
79
|
+
- **True Separation of Concerns**:
|
80
|
+
Develop visualizations in Jupyter notebooks and write documentation
|
81
|
+
in markdown files, with each tool optimized for its purpose.
|
82
|
+
|
83
|
+
- **Intuitive Markdown Syntax**:
|
84
|
+
Use standard image syntax with a simple extension to reference
|
85
|
+
notebook figures: `{#figure-id}`
|
86
|
+
|
87
|
+
- **Automatic Updates**:
|
88
|
+
When you modify your notebooks, your documentation updates
|
89
|
+
automatically in MkDocs serve mode.
|
90
|
+
|
91
|
+
- **Clean Source Documents**:
|
92
|
+
Your markdown remains readable and focused on content, without
|
93
|
+
code distractions or complex embedding techniques.
|
94
|
+
|
95
|
+
- **Enhanced Development Experience**:
|
96
|
+
Take advantage of IDE features like code completion and syntax
|
97
|
+
highlighting in the appropriate environment.
|
98
|
+
|
99
|
+
## Quick Start
|
100
|
+
|
101
|
+
### 1. Installation
|
102
|
+
|
103
|
+
```bash
|
104
|
+
pip install nbsync
|
105
|
+
```
|
106
|
+
|
107
|
+
### 2. Configuration
|
108
|
+
|
109
|
+
Add to your `mkdocs.yml`:
|
110
|
+
|
111
|
+
```yaml
|
112
|
+
plugins:
|
113
|
+
- nbsync:
|
114
|
+
src_dir: ../notebooks
|
115
|
+
```
|
116
|
+
|
117
|
+
### 3. Mark Figures in Your Notebook
|
118
|
+
|
119
|
+
In your Jupyter notebook, identify figures with a comment:
|
120
|
+
|
121
|
+
```python
|
122
|
+
# #my-figure
|
123
|
+
import matplotlib.pyplot as plt
|
124
|
+
|
125
|
+
fig, ax = plt.subplots(figsize=(8, 4))
|
126
|
+
ax.plot([1, 2, 3, 4], [10, 20, 25, 30])
|
127
|
+
```
|
128
|
+
|
129
|
+
### 4. Reference in Markdown
|
130
|
+
|
131
|
+
Use standard Markdown image syntax with the figure identifier:
|
132
|
+
|
133
|
+
```markdown
|
134
|
+
{#my-figure}
|
135
|
+
```
|
136
|
+
|
137
|
+
## Advanced Usage
|
138
|
+
|
139
|
+
For more detailed information on how to use nbsync, see:
|
140
|
+
|
141
|
+
- [Notebook Configuration](usage/notebook.md) - Setting up notebook directories
|
142
|
+
- [Class Options](usage/class.md) - Control how notebook content is displayed
|
143
|
+
<!-- - [Workflow Tips](usage/workflow.md) - Best practices for documentation -->
|
144
|
+
|
145
|
+
## The Power of Separation
|
146
|
+
|
147
|
+
Creating documentation and developing visualizations involve different
|
148
|
+
workflows and timeframes. When building visualizations in Jupyter notebooks,
|
149
|
+
you need rapid cycles of execution, verification, and modification.
|
150
|
+
|
151
|
+
This plugin is designed specifically to address these separation of
|
152
|
+
concerns, allowing you to:
|
153
|
+
|
154
|
+
- **Focus on code** in notebooks without documentation distractions
|
155
|
+
- **Focus on narrative** in markdown without code interruptions
|
156
|
+
- **Maintain powerful connections** between both environments
|
157
|
+
|
158
|
+
Each environment is optimized for its purpose, while the plugin
|
159
|
+
handles the integration automatically.
|
160
|
+
|
161
|
+
## Contributing
|
162
|
+
|
163
|
+
Contributions are welcome! Please open an issue or submit a pull request.
|
164
|
+
|
165
|
+
## License
|
166
|
+
|
167
|
+
This project is licensed under the MIT License.
|
168
|
+
|
169
|
+
<!-- Badges -->
|
170
|
+
[pypi-v-image]: https://img.shields.io/pypi/v/nbsync.svg
|
171
|
+
[pypi-v-link]: https://pypi.org/project/nbsync/
|
172
|
+
[python-v-image]: https://img.shields.io/pypi/pyversions/nbsync.svg
|
173
|
+
[python-v-link]: https://pypi.org/project/nbsync
|
174
|
+
[GHAction-image]: https://github.com/daizutabi/nbsync/actions/workflows/ci.yaml/badge.svg?branch=main&event=push
|
175
|
+
[GHAction-link]: https://github.com/daizutabi/nbsync/actions?query=event%3Apush+branch%3Amain
|
176
|
+
[codecov-image]: https://codecov.io/github/daizutabi/nbsync/coverage.svg?branch=main
|
177
|
+
[codecov-link]: https://codecov.io/github/daizutabi/nbsync?branch=main
|
@@ -0,0 +1,12 @@
|
|
1
|
+
nbsync/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
nbsync/cell.py,sha256=HjGV7WHYwoW-X8AK9EePcjHz2wHl0rVaEAS_hdI8QNw,3248
|
3
|
+
nbsync/logger.py,sha256=tQK8Z8HdWQNhVohQYLwZtJhdaj2w0UTyhHQak3mPqNc,119
|
4
|
+
nbsync/markdown.py,sha256=sGNPuyLTfQFv-I1m6jK6uDKFRHAPYY6hpy4P1bJjmkE,3185
|
5
|
+
nbsync/notebook.py,sha256=Voh-e8RQsoYmWKnaHzAn4bm6Ud5v-aHwmng2Uc0rUts,1009
|
6
|
+
nbsync/plugin.py,sha256=YywBAD7fcrPg3-_9T_kSuvD8RBzsc_ovlIviGlKR9po,2835
|
7
|
+
nbsync/sync.py,sha256=RTF6h0izF1emc1CpG6atEgAQHC7mZCit9NkXK26sOA8,3575
|
8
|
+
nbsync-0.1.0.dist-info/METADATA,sha256=t97_Xq3uoqCGveX9XblM-zTwyHA7npBBK32PdhN_e9Y,6604
|
9
|
+
nbsync-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
10
|
+
nbsync-0.1.0.dist-info/entry_points.txt,sha256=bSqmJTDmrUoSm4yDjk8YDWxI71SShx16judadGdiKbA,47
|
11
|
+
nbsync-0.1.0.dist-info/licenses/LICENSE,sha256=wy1pqn52upuo_qYwY-epWmspwE-3UWJso0xodciGXYc,1062
|
12
|
+
nbsync-0.1.0.dist-info/RECORD,,
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Daizu
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
nbsync-0.0.1.dist-info/METADATA
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.4
|
2
|
-
Name: nbsync
|
3
|
-
Version: 0.0.1
|
4
|
-
Summary: Tools for notebook synchronization and management
|
5
|
-
Author-email: daizutabi <daizutabi@gmail.com>
|
6
|
-
License: MIT License
|
7
|
-
|
8
|
-
Copyright (c) 2020-present daizutabi <daizutabi@gmail.com>
|
9
|
-
|
10
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
11
|
-
|
12
|
-
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
13
|
-
|
14
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
15
|
-
License-File: LICENSE
|
16
|
-
Keywords: content-management,documentation,extraction,jupyter,markdown,notebook,visualization
|
17
|
-
Classifier: Development Status :: 4 - Beta
|
18
|
-
Classifier: Programming Language :: Python
|
19
|
-
Classifier: Programming Language :: Python :: 3.10
|
20
|
-
Classifier: Programming Language :: Python :: 3.11
|
21
|
-
Classifier: Programming Language :: Python :: 3.12
|
22
|
-
Classifier: Programming Language :: Python :: 3.13
|
23
|
-
Classifier: Topic :: Documentation
|
24
|
-
Classifier: Topic :: Software Development :: Documentation
|
25
|
-
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
26
|
-
Requires-Python: >=3.10
|
27
|
-
Description-Content-Type: text/markdown
|
28
|
-
|
29
|
-
# nbsync-monorepo
|
nbsync-0.0.1.dist-info/RECORD
DELETED
@@ -1,5 +0,0 @@
|
|
1
|
-
nbsync/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
nbsync-0.0.1.dist-info/METADATA,sha256=uSwdGxT3JAkRIc7dbbithBN1254yd6fu8wyQuRQszig,1965
|
3
|
-
nbsync-0.0.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
4
|
-
nbsync-0.0.1.dist-info/licenses/LICENSE,sha256=dbO9NY-nQXzIS5St3j3rPUd7gm5r6pczFYaNy5GVNdI,1105
|
5
|
-
nbsync-0.0.1.dist-info/RECORD,,
|
@@ -1,9 +0,0 @@
|
|
1
|
-
MIT License
|
2
|
-
|
3
|
-
Copyright (c) 2020-present daizutabi <daizutabi@gmail.com>
|
4
|
-
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
6
|
-
|
7
|
-
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
8
|
-
|
9
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
File without changes
|