├── .gitignore ├── README.md ├── create_interactive_notebooks.py ├── jupyter_to_blog.py └── notebooks.css /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jupyterblog 2 | A helper repository for converting Jupyter notebooks into a blog-friendly format. It consists of a few scripts that can be called from the shell. 3 | 4 | # Scripts 5 | **`jupyter_to_blog.py`** 6 | Takes as input a path to a jupyter notebook. It will then strip the notebook of cell count numbers, warnings, and some other outputs that look ugly in a blog. Finally, it either converts the notebook to html format (if no `--inplace` flag is given) or modifies the notebook in place (if `--inplace` is given, in which case the notebook will simply be cleaned up). 7 | 8 | **`create_interactive_notebooks.py`** 9 | This is for moving a notebook to a new folder that's meant for "interactive" notebooks hosted with mybinder. It take a path to a jupyter notebook and a path to an output "interactive" folder, as well as paths for changing any relative links that are in the notebook (e.g. to images). It will copy the notebook and replace relative links. 10 | 11 | # How to use it 12 | * Copy/paste the code in `notebooks.css` into your website's CSS code. 13 | * Run `jupyter_to_blog.py /path/to/notebook.ipynb`. 14 | * Go into the created `html` folder and find the post that was created. 15 | * Copy/paste this html into wordpress' html editor 16 | * Preview the post. The formatting should be taken care of by the CSS you pasted. -------------------------------------------------------------------------------- /create_interactive_notebooks.py: -------------------------------------------------------------------------------- 1 | from glob import glob 2 | import os 3 | import os.path as op 4 | import shutil as sh 5 | import argparse 6 | import nbformat as nb 7 | 8 | parser = argparse.ArgumentParser( 9 | description=('Check whether jupyter notebooks have an Interactive property' 10 | ' and copy them to an interactive output folder if so.')) 11 | parser.add_argument('file_path', metavar='F', type=str, nargs='+', 12 | help='The path to the notebook files to be checked') 13 | parser.add_argument('output_path', metavar='F', type=str, nargs='+', 14 | help='The path to the notebook files to be checked') 15 | parser.add_argument('old_rel_path', metavar='F', type=str, nargs='+', 16 | help='The relative path to replace') 17 | parser.add_argument('new_rel_path', metavar='F', type=str, nargs='+', 18 | help='Replace all instances of the old path with this one') 19 | 20 | 21 | args = parser.parse_args() 22 | 23 | files_input = args.file_path 24 | path_output = args.output_path[0] 25 | old_rel_path = args.old_rel_path[0] 26 | new_rel_path = args.new_rel_path[0] 27 | 28 | print('Replacing {} with {}'.format(old_rel_path, new_rel_path)) 29 | 30 | 31 | n_copied = 0 32 | for file in files_input: 33 | if not op.exists(file + '-meta'): 34 | continue 35 | with open(file + '-meta', 'r') as ff: 36 | interactive = False 37 | for ln in ff.readlines(): 38 | if ln.startswith('Interactive'): 39 | interactive = ln.split(': ')[-1] 40 | if interactive == 'True': 41 | file_name = op.basename(file) 42 | ntbk = nb.read(file, nb.NO_CONVERT) 43 | for cell in ntbk['cells']: 44 | cell['source'] = cell['source'].replace(old_rel_path, new_rel_path) 45 | nb.write(ntbk, os.sep.join([path_output, file_name])) 46 | n_copied += 1 47 | 48 | 49 | print('Finished. Copied {} notebooks to {}'.format(n_copied, path_output)) 50 | -------------------------------------------------------------------------------- /jupyter_to_blog.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | from os import path as op 4 | import shutil as sh 5 | from glob import glob 6 | import time 7 | import nbformat as nb 8 | import argparse 9 | from glob import glob 10 | from tqdm import tqdm 11 | 12 | parser = argparse.ArgumentParser( 13 | description='Process a jupyter notebook for blogs.') 14 | parser.add_argument('file_path', metavar='F', type=str, nargs='+', 15 | help='The path to the notebook file to be converted') 16 | parser.add_argument('--inplace', dest='inplace', action='store_const', 17 | const=True, default=False, 18 | help='Whether to simply modify the notebook in place') 19 | 20 | args = parser.parse_args() 21 | inplace = args.inplace 22 | 23 | files = args.file_path 24 | print('Found {} files'.format(len(files))) 25 | 26 | if inplace is True: 27 | print('Overwriting notebook file') 28 | else: 29 | print('Creating new HTML') 30 | 31 | 32 | for file_path in tqdm(files): 33 | file_path = op.abspath(file_path).replace(' ', '\ ') 34 | 35 | root = op.dirname(file_path) 36 | if root == '': 37 | root = '.' 38 | title = op.basename(file_path).split('.ipynb')[0] 39 | 40 | # Check to see if the html folder exists 41 | if not op.exists('{}/html/'.format(root)): 42 | os.mkdir('{}/html/'.format(root)) 43 | 44 | # Remove pre-existing versions of this post if it's there 45 | existing = glob('{}/html/*{}*.html'.format(root, title)) 46 | if len(existing) > 1: 47 | raise ValueError('There should be at most 1 file with this title') 48 | elif len(existing) == 1: 49 | # Keep the date info for the post 50 | existing = existing[0] 51 | date_info = op.basename(existing).split('-')[:3] 52 | year, mo, day = [int(ii) for ii in date_info] 53 | os.remove(existing) 54 | else: 55 | # Create new date info for the post 56 | now = time.localtime() 57 | mo = now.tm_mon 58 | day = now.tm_mday 59 | year = now.tm_year 60 | 61 | # Clean up the notebook 62 | file_path_tmp = file_path + '_TMP' 63 | ntbk = nb.read(file_path, nb.NO_CONVERT) 64 | new_cells = [] 65 | for ii, cell in enumerate(ntbk.cells): 66 | # Don't modify non-code cells and just add 67 | if cell['cell_type'] != 'code': 68 | new_cells.append(cell) 69 | continue 70 | 71 | # Skip the cell if it's empty 72 | if len(cell['source']) == 0: 73 | continue 74 | 75 | # Clean outputs otherwise 76 | outputs = [] 77 | for output in cell['outputs']: 78 | # Remove stderrors in the outputs 79 | if 'name' in list(output.keys()): 80 | if output['name'] != 'sterr': 81 | continue 82 | # Check if we have any object outputs (e.g. a function returned) 83 | if 'data' in list(output.keys()): 84 | if 'text/plain' in output['data'].keys(): 85 | if output['data']['text/plain'].startswith('<'): 86 | _ = output['data'].pop('text/plain') 87 | outputs.append(output) 88 | cell['outputs'] = outputs 89 | cell['execution_count'] = None 90 | new_cells.append(cell) 91 | 92 | # Update the notebook w/ new cells and write it 93 | ntbk['cells'] = new_cells 94 | 95 | # Continue with the conversion or just overwrite in place 96 | if inplace is True: 97 | nb.write(ntbk, file_path, nb.NO_CONVERT) 98 | continue 99 | nb.write(ntbk, file_path_tmp, nb.NO_CONVERT) 100 | 101 | # Now convert to html and move to the `html` folder 102 | curdir = os.path.abspath(os.curdir) 103 | newdir = os.path.dirname(__file__) 104 | cmd_convert = 'jupyter nbconvert --to html --template basic {}'.format(file_path_tmp) 105 | print("Running command:\n{}".format(cmd_convert)) 106 | os.chdir(newdir) 107 | os.system(cmd_convert) 108 | os.chdir(curdir) 109 | 110 | # Now move the HTML to a new folder and delete the temp file 111 | new_folder = '{}/html/'.format(root) 112 | new_file = '{}.html'.format(title) 113 | html_file = '{}-{}-{}-{}.html'.format(year, mo, day, title) 114 | sh.move('{}/{}'.format(root, new_file), '{}{}'.format(new_folder, html_file)) 115 | os.remove(file_path_tmp) 116 | print('New file is at:\n{}'.format(op.abspath(new_folder + html_file))) 117 | print('Finished!') 118 | -------------------------------------------------------------------------------- /notebooks.css: -------------------------------------------------------------------------------- 1 | /* Add the following to your sites main css code */ 2 | /* Jupyter notebook styling */ 3 | .widget { 4 | margin-bottom: 0; 5 | } 6 | 7 | .input_prompt { 8 | color: #8476FF; 9 | } 10 | 11 | .navbar { 12 | margin-bottom: 20px; 13 | border-radius: 0; 14 | } 15 | 16 | .c { 17 | color: #989898; 18 | } 19 | 20 | .k { 21 | color: #338822; 22 | font-weight: bold; 23 | } 24 | 25 | .kn { 26 | color: #338822; 27 | font-weight: bold; 28 | } 29 | 30 | .mi { 31 | color: #000000; 32 | } 33 | 34 | .o { 35 | color: #000000; 36 | } 37 | 38 | .ow { 39 | color: #BA22FF; 40 | font-weight: bold; 41 | } 42 | 43 | .nb { 44 | color: #338822; 45 | } 46 | 47 | .n { 48 | color: #000000; 49 | } 50 | 51 | .s { 52 | color: #cc2222; 53 | } 54 | 55 | .se { 56 | color: #cc2222; 57 | font-weight: bold; 58 | } 59 | 60 | .si { 61 | color: #C06688; 62 | font-weight: bold; 63 | } 64 | 65 | .nn { 66 | color: #4D00FF; 67 | font-weight: bold; 68 | } 69 | 70 | .output_area pre { 71 | background-color: #FFFFFF; 72 | padding-left: 5%; 73 | } 74 | 75 | .site-description { 76 | margin: 10px auto; 77 | } 78 | 79 | #site-title { 80 | margin-top: 10px; 81 | } 82 | 83 | .blog-header { 84 | padding-top: 20px; 85 | padding-bottom: 20px; 86 | } 87 | 88 | .code_cell { 89 | padding-left: 2%; 90 | } 91 | 92 | .cell { 93 | margin-top: 20px; 94 | margin-bottom: 20px; 95 | } 96 | 97 | br { 98 | line-height: 2; 99 | } 100 | 101 | .cell, .inner_cell h1, h2, h3, h4 { 102 | margin-top: 30px; 103 | margin-bottom: 10px; 104 | } 105 | 106 | .input .text_cell .text_cell_render { 107 | width: 100% !important; 108 | } 109 | 110 | .output_png { 111 | margin: auto; 112 | display: block; 113 | width: 60%; 114 | } 115 | 116 | /* To remove input/output numbers*/ 117 | .input_prompt, .output_prompt, .prompt { 118 | display:none; 119 | } --------------------------------------------------------------------------------