├── .gitignore
├── README.rst
├── Sample_test.ipynb.exploded
├── 1d163ccf-0d53-43c8-8abb-b2cd81ff2e02
│ ├── metadata.json
│ ├── output1-metadata.json
│ ├── output1.html
│ ├── output1.txt
│ ├── outputs_sequence
│ └── source.py
├── 2c09983a-f7be-483c-bc29-cca89e79b9fd
│ ├── metadata.json
│ └── source.py
├── 99175a45-e90d-4cec-bf4e-5e47e21afe22
│ ├── metadata.json
│ ├── output1-metadata.json
│ ├── output1.html
│ ├── output1.tex
│ ├── output1.txt
│ ├── outputs_sequence
│ └── source.py
├── a375ba32-f258-4ce4-b97e-d6da4c11e23a
│ ├── metadata.json
│ ├── output1.txt
│ ├── output2-metadata.json
│ ├── output2.txt
│ ├── outputs_sequence
│ └── source.py
├── ba77c498-ff03-4b85-820d-02a1683ffe88
│ └── source.md
├── cells_sequence
├── e74cbffc-8e83-4899-abe6-697e5f09eee0
│ ├── metadata.json
│ ├── output1.txt
│ ├── output2-metadata.json
│ ├── output2.txt
│ ├── output3-metadata.json
│ ├── output3.png
│ ├── output3.txt
│ ├── outputs_sequence
│ └── source.py
└── metadata.json
└── nbexplode.py
/.gitignore:
--------------------------------------------------------------------------------
1 | .ipynb_checkpoints/
2 | *.ipynb
3 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Explode a notebook into a directory of files, and recombine it to a .ipynb
2 |
3 | Exploded notebooks have some advantages in version control: it should be easier
4 | to merge changes in this form. They are more unwieldy in many ways, though.
5 |
6 | Usage::
7 |
8 | python3 nbexplode.py Foo.ipynb
9 | python3 nbexplode.py -r Foo.ipynb.exploded
10 |
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/1d163ccf-0d53-43c8-8abb-b2cd81ff2e02/metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "collapsed": false
3 | }
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/1d163ccf-0d53-43c8-8abb-b2cd81ff2e02/output1-metadata.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/1d163ccf-0d53-43c8-8abb-b2cd81ff2e02/output1.html:
--------------------------------------------------------------------------------
1 | Hello
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/1d163ccf-0d53-43c8-8abb-b2cd81ff2e02/output1.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/1d163ccf-0d53-43c8-8abb-b2cd81ff2e02/outputs_sequence:
--------------------------------------------------------------------------------
1 | text/html, text/plain
2 |
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/1d163ccf-0d53-43c8-8abb-b2cd81ff2e02/source.py:
--------------------------------------------------------------------------------
1 | from IPython.display import HTML, display
2 | display(HTML('Hello'))
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/2c09983a-f7be-483c-bc29-cca89e79b9fd/metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "collapsed": false
3 | }
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/2c09983a-f7be-483c-bc29-cca89e79b9fd/source.py:
--------------------------------------------------------------------------------
1 | import tabipy
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/99175a45-e90d-4cec-bf4e-5e47e21afe22/metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "collapsed": false
3 | }
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/99175a45-e90d-4cec-bf4e-5e47e21afe22/output1-metadata.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/99175a45-e90d-4cec-bf4e-5e47e21afe22/output1.html:
--------------------------------------------------------------------------------
1 |
2 | n | square |
3 | 1 | 1 |
4 | 2 | 4 |
5 | 3 | 9 |
6 | 4 | 16 |
7 | 5 | 25 |
8 | 6 | 36 |
9 | 7 | 49 |
10 | 8 | 64 |
11 | 9 | 81 |
12 | 10 | 100 |
13 | 11 | 121 |
14 | 12 | 144 |
15 |
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/99175a45-e90d-4cec-bf4e-5e47e21afe22/output1.tex:
--------------------------------------------------------------------------------
1 | \begin{tabular}{*{2}{l}}
2 | \hline
3 | n & square\\
4 | \hline
5 |
6 | 1 & 1\\
7 |
8 | 2 & 4\\
9 |
10 | 3 & 9\\
11 |
12 | 4 & 16\\
13 |
14 | 5 & 25\\
15 |
16 | 6 & 36\\
17 |
18 | 7 & 49\\
19 |
20 | 8 & 64\\
21 |
22 | 9 & 81\\
23 |
24 | 10 & 100\\
25 |
26 | 11 & 121\\
27 |
28 | 12 & 144\\
29 | \hline
30 | \end{tabular}
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/99175a45-e90d-4cec-bf4e-5e47e21afe22/output1.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/99175a45-e90d-4cec-bf4e-5e47e21afe22/outputs_sequence:
--------------------------------------------------------------------------------
1 | text/html, text/latex, text/plain (8)
2 |
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/99175a45-e90d-4cec-bf4e-5e47e21afe22/source.py:
--------------------------------------------------------------------------------
1 | t = tabipy.Table(tabipy.TableHeaderRow('n', 'square'))
2 | for n in range(1, 13):
3 | t.append_row((n, n**2))
4 | t
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/a375ba32-f258-4ce4-b97e-d6da4c11e23a/metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "collapsed": false,
3 | "test_key": "value"
4 | }
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/a375ba32-f258-4ce4-b97e-d6da4c11e23a/output1.txt:
--------------------------------------------------------------------------------
1 | some code
2 |
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/a375ba32-f258-4ce4-b97e-d6da4c11e23a/output2-metadata.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/a375ba32-f258-4ce4-b97e-d6da4c11e23a/output2.txt:
--------------------------------------------------------------------------------
1 | 'With output'
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/a375ba32-f258-4ce4-b97e-d6da4c11e23a/outputs_sequence:
--------------------------------------------------------------------------------
1 | stdout
2 | text/plain (1)
3 |
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/a375ba32-f258-4ce4-b97e-d6da4c11e23a/source.py:
--------------------------------------------------------------------------------
1 | print("some code")
2 | "With output"
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/ba77c498-ff03-4b85-820d-02a1683ffe88/source.md:
--------------------------------------------------------------------------------
1 | Here is a Markdown cell
2 |
3 | With *another* line
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/cells_sequence:
--------------------------------------------------------------------------------
1 | ba77c498-ff03-4b85-820d-02a1683ffe88
2 | a375ba32-f258-4ce4-b97e-d6da4c11e23a
3 | e74cbffc-8e83-4899-abe6-697e5f09eee0
4 | 1d163ccf-0d53-43c8-8abb-b2cd81ff2e02
5 | 2c09983a-f7be-483c-bc29-cca89e79b9fd
6 | 99175a45-e90d-4cec-bf4e-5e47e21afe22
7 |
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/e74cbffc-8e83-4899-abe6-697e5f09eee0/metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "collapsed": false
3 | }
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/e74cbffc-8e83-4899-abe6-697e5f09eee0/output1.txt:
--------------------------------------------------------------------------------
1 | Populating the interactive namespace from numpy and matplotlib
2 | foo
3 |
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/e74cbffc-8e83-4899-abe6-697e5f09eee0/output2-metadata.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/e74cbffc-8e83-4899-abe6-697e5f09eee0/output2.txt:
--------------------------------------------------------------------------------
1 | []
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/e74cbffc-8e83-4899-abe6-697e5f09eee0/output3-metadata.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/e74cbffc-8e83-4899-abe6-697e5f09eee0/output3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/takluyver/nbexplode/986309cc881e89a887e206b259024b3de55f19fe/Sample_test.ipynb.exploded/e74cbffc-8e83-4899-abe6-697e5f09eee0/output3.png
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/e74cbffc-8e83-4899-abe6-697e5f09eee0/output3.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/e74cbffc-8e83-4899-abe6-697e5f09eee0/outputs_sequence:
--------------------------------------------------------------------------------
1 | stdout
2 | text/plain (4)
3 | image/png, text/plain
4 |
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/e74cbffc-8e83-4899-abe6-697e5f09eee0/source.py:
--------------------------------------------------------------------------------
1 | %pylab inline
2 | print('foo')
3 | plot(range(10))
--------------------------------------------------------------------------------
/Sample_test.ipynb.exploded/metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "celltoolbar": "Edit Metadata",
3 | "kernelspec": {
4 | "display_name": "Python 3",
5 | "language": "python",
6 | "name": "python3"
7 | },
8 | "language_info": {
9 | "codemirror_mode": {
10 | "name": "ipython",
11 | "version": 3
12 | },
13 | "file_extension": ".py",
14 | "mimetype": "text/x-python",
15 | "name": "python",
16 | "nbconvert_exporter": "python",
17 | "pygments_lexer": "ipython3",
18 | "version": "3.4.2"
19 | }
20 | }
--------------------------------------------------------------------------------
/nbexplode.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import pathlib
3 | import json
4 | import re
5 | import shutil
6 | from uuid import uuid4
7 |
8 | from IPython import nbformat as nbf
9 |
10 | _mime_to_ext = {
11 | 'text/plain': '.txt',
12 | 'text/html': '.html',
13 | 'text/latex': '.tex',
14 | 'image/png': '.png',
15 | 'image/jpeg': '.jpg',
16 | }
17 |
18 | def _is_binary(mime):
19 | return mime.startswith(('image/', 'application/', 'audio'))
20 |
21 | def explode_output(output, cell_dir, i):
22 | optype = output.pop('output_type')
23 | if optype == 'stream':
24 | with (cell_dir / ('output%d.txt' % i)).open(
25 | 'w', encoding='utf-8') as f:
26 | f.write(output.text)
27 |
28 | return output.name
29 |
30 | elif optype == 'error':
31 | with (cell_dir / ('error%d.json' % i)).open('w') as f:
32 | json.dump(output, f, indent=2, sort_keys=True)
33 | return 'error'
34 |
35 | elif optype in {'execute_result', 'display_data'}:
36 | with (cell_dir / ('output%d-metadata.json' % i)).open(
37 | 'w', encoding='utf-8') as f:
38 | json.dump(output.metadata, f, indent=2, sort_keys=True)
39 | mimetypes = ", ".join(sorted(output.data))
40 |
41 | for mimetype, data in output.data.items():
42 | file = cell_dir / 'output{}{}'.format(i, _mime_to_ext[mimetype])
43 | if _is_binary(mimetype):
44 | with file.open('wb') as f:
45 | f.write(base64.b64decode(data.encode('ascii')))
46 | else:
47 | with file.open('w', encoding='utf-8') as f:
48 | f.write(data)
49 |
50 | if optype == 'execute_result':
51 | return mimetypes + (' (%d)' % output.execution_count)
52 | else:
53 | return mimetypes
54 |
55 | def explode(nbnode, directory):
56 | directory = pathlib.Path(directory)
57 | with (directory / 'metadata.json').open('w') as f:
58 | json.dump(nbnode.metadata, f, indent=2, sort_keys=True)
59 |
60 | cell_ids = []
61 | for cell in nbnode.cells:
62 | if 'nbexplode_cell_id' in cell.metadata:
63 | cell_id = cell.metadata.pop('nbexplode_cell_id')
64 | else:
65 | cell_id = str(uuid4())
66 |
67 | cell_ids.append(cell_id)
68 |
69 | cell_dir = directory / cell_id
70 | cell_dir.mkdir()
71 |
72 | if cell.cell_type == 'markdown':
73 | source_file = 'source.md'
74 | elif cell.cell_type == 'raw':
75 | source_file = 'source.txt'
76 | else:
77 | # Code cell
78 | source_file = 'source' + nbnode.metadata.language_info.file_extension
79 |
80 | with (cell_dir / source_file).open('w', encoding='utf-8') as f:
81 | f.write(cell.source)
82 |
83 | if cell.metadata:
84 | with (cell_dir / 'metadata.json').open('w') as f:
85 | json.dump(cell.metadata, f, indent=2, sort_keys=True)
86 |
87 | if not cell.get('outputs'):
88 | continue
89 |
90 | output_counter = 0
91 | outputs_seq = []
92 | for output in cell.outputs:
93 | output_counter += 1
94 | outputs_seq.append(explode_output(output, cell_dir, output_counter))
95 |
96 | with (cell_dir / 'outputs_sequence').open('w') as f:
97 | for l in outputs_seq:
98 | f.write(l + '\n')
99 |
100 |
101 | with (directory / 'cells_sequence').open('w') as f:
102 | for c in cell_ids:
103 | f.write(c + '\n')
104 |
105 | _exec_result_re = re.compile(r'\((\d+)\)$')
106 |
107 | def recombine_output(cell_dir, i, info):
108 | if info in {'stdout', 'stderr'}:
109 | with (cell_dir / ('output%d.txt' % i)).open() as f:
110 | return nbf.v4.new_output('stream', name=info, text=f.read())
111 |
112 | elif info == 'error':
113 | with (cell_dir / ('error%d.json' % i)).open() as f:
114 | err_data = json.load(f)
115 | return nbf.v4.new_output('error', **err_data)
116 |
117 | else:
118 | m = _exec_result_re.search(info)
119 | if m:
120 | info = info[:-(len(m.group(0))+1)]
121 | op = nbf.v4.new_output('execute_result', execution_count=int(m.group(1)))
122 | else:
123 | op = nbf.v4.new_output('display_data')
124 |
125 | mimebundle = {}
126 | for mimetype in info.split(", "):
127 | file = "output{}{}".format(i, _mime_to_ext[mimetype])
128 | if _is_binary(mimetype):
129 | with (cell_dir / file).open('rb') as f:
130 | mimebundle[mimetype] = base64.b64encode(f.read()).decode('ascii')
131 |
132 | else:
133 | with (cell_dir / file).open() as f:
134 | mimebundle[mimetype] = f.read()
135 |
136 | op.data = nbf.from_dict(mimebundle)
137 |
138 | metadata_file = cell_dir / "output{}-metadata.json".format(i)
139 | if metadata_file.exists():
140 | with metadata_file.open() as f:
141 | op.metadata = nbf.from_dict(json.load(f))
142 |
143 | return op
144 |
145 | def recombine(directory):
146 | directory = pathlib.Path(directory)
147 | with (directory / 'metadata.json').open() as f:
148 | metadata = json.load(f)
149 |
150 | nb = nbf.v4.new_notebook(metadata=metadata)
151 |
152 | with (directory / 'cells_sequence').open() as f:
153 | cells_sequence = f.read().splitlines()
154 |
155 | for cell_id in cells_sequence:
156 | cell_dir = directory / cell_id
157 |
158 | source_file = list(cell_dir.glob('source.*'))[0]
159 | if source_file.suffix == '.md':
160 | with source_file.open() as f:
161 | cell = nbf.v4.new_markdown_cell(f.read())
162 | elif source_file.suffix == '.txt':
163 | with source_file.open() as f:
164 | cell = nbf.NotebookNode(cell_type='raw',
165 | source=f.read(),
166 | metadata=nbf.NotebookNode())
167 | else:
168 | with source_file.open() as f:
169 | cell = nbf.v4.new_code_cell(f.read())
170 |
171 | nb.cells.append(cell)
172 |
173 | if (cell_dir / 'metadata.json').exists():
174 | with (cell_dir / 'metadata.json').open() as f:
175 | cell.metadata = nbf.from_dict(json.load(f))
176 |
177 | cell.metadata['nbexplode_cell_id'] = cell_id
178 |
179 | if not (cell_dir / 'outputs_sequence').exists():
180 | continue
181 |
182 | with (cell_dir / 'outputs_sequence').open() as f:
183 | outputs_seq = f.read().splitlines()
184 |
185 | cell.outputs = [recombine_output(cell_dir, i, info)
186 | for (i, info) in enumerate(outputs_seq, start=1)]
187 |
188 | return nb
189 |
190 | def main(argv=None):
191 | import argparse
192 | ap = argparse.ArgumentParser()
193 | ap.add_argument('-r', '--recombine', action='store_true')
194 | ap.add_argument('file')
195 | args = ap.parse_args(argv)
196 |
197 | if args.recombine:
198 | assert args.file.endswith('.ipynb.exploded')
199 | nb = recombine(args.file)
200 | nbf.write(nb, args.file[:-len('.exploded')])
201 | else:
202 | assert args.file.endswith('.ipynb')
203 | nb = nbf.read(args.file, as_version=4)
204 | directory = pathlib.Path(args.file + '.exploded')
205 | if directory.is_dir():
206 | shutil.rmtree(str(directory))
207 | directory.mkdir()
208 | explode(nb, directory)
209 |
210 | if __name__ == '__main__':
211 | main()
--------------------------------------------------------------------------------