├── .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 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
nsquare
11
24
39
416
525
636
749
864
981
10100
11121
12144
-------------------------------------------------------------------------------- /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() --------------------------------------------------------------------------------