├── LICENSE ├── README.md ├── config-example.ini ├── jupy2wp ├── __init__.py ├── jupy2wp.py └── templates │ ├── __init__.py │ └── basicx.tpl └── setup.py /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Pybonacci 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | jupy2wp 2 | ======= 3 | 4 | Publish a Jupyter notebook on a wordpress site using xmlrpc. 5 | 6 | This tool, formerly known as [ipy2wp](https://github.com/Pybonacci/ipy2wp), 7 | allows you to publish a Jupyter notebook from the command line or from the notebook 8 | itself on a wordpress site using xmlrpc. 9 | 10 | Installation 11 | ============ 12 | 13 | Download the repo 14 | 15 | cd jupy2wp 16 | pip install . 17 | 18 | or 19 | 20 | pip install git+https://github.com/Pybonacci/jupy2wp.git 21 | 22 | Usage 23 | ===== 24 | 25 | There are two ways to use this tool: 26 | 27 | From the command line 28 | --------------------- 29 | 30 | python -m jupy2wp.jupy2wp [options] 31 | 32 | You have the following options: 33 | 34 | * --xmlrpc-url: The url to xmlrpc.php on your site 35 | * --user: The user who will publish the post 36 | * --password: The password of the user who will publish the post 37 | * --nb: the path to the Jupyter notebook 38 | * --title: The title of the post 39 | * --categories: The categories for the post (the categories should be defined previously in the blog) 40 | * --tags: tags for the post 41 | * --template: The template to be used. If no template is provided then the basic Jupyter notebook html template is used. [See the templates section for more info](https://github.com/Pybonacci/jupy2wp#templates). 42 | 43 | A complete example would be: 44 | 45 | python -m jupy2wp.jupy2wp --xmlrpc-url http://pybonacci.org/xmlrpc.php --user kiko --password 1_2_oh_my_god!!! --nb 'dummy.ipynb' --title 'The best post ever' --categories articles tutorials --tags strawberry lucy jupyter --template basicx 46 | 47 | * It works on Jupyter 4.0+ and Python 2.7+ and 3.3+* 48 | 49 | Notebook inline images 50 | ====================== 51 | 52 | If there are inline images in your notebook, them will be converted and uploaded to your wordpress blog ('wp-content/uploads') and the html code will be changed to link to the uploaded images. 53 | 54 | Result 55 | ====== 56 | 57 | The result will be a draft on your wordpress site. Please, check the draft before you publish the post as some advanced functionality could not be solved satisfactorily. If you find something wrong, please, open an issue. 58 | 59 | Templates 60 | ========= 61 | 62 | Right now you can choose between the **basic** and the **basicx** templates. 63 | 64 | * The **basic** template is that used by nbconvert. 65 | * The **basicx** template is similar to the **basic** template but it eliminates the input and output prompt numbers, most of the css classes and injects some css code to highlight the code cells as in the notebook. 66 | 67 | If you want to provide new templates just send a PR or open an issue describing your needs. 68 | 69 | License 70 | ======= 71 | 72 | MIT, do whatever you want with it. 73 | -------------------------------------------------------------------------------- /config-example.ini: -------------------------------------------------------------------------------- 1 | [wordpress] 2 | xmlrpc-url = http://pybonacci.org/xmlrpc.php 3 | user = admin 4 | 5 | [template] 6 | template = /path/to/template 7 | 8 | [latex] 9 | size = 0 10 | background-color = ffffff 11 | foreground-color = 000000 12 | -------------------------------------------------------------------------------- /jupy2wp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pybonacci/jupy2wp/d9dcc13fa89a3841fae9d6f2f354028a312a7eba/jupy2wp/__init__.py -------------------------------------------------------------------------------- /jupy2wp/jupy2wp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ## Based on the work made by brave people and stored on 4 | ## https://github.com/ipython-contrib/IPython-notebook-extensions 5 | 6 | import datetime 7 | import argparse 8 | from binascii import a2b_base64 9 | import re 10 | import os 11 | import shutil 12 | try: 13 | import xmlrpc.client as xmlrpclib #python3 14 | except: 15 | import xmlrpclib # python2 16 | 17 | import nbconvert as nbc 18 | from traitlets.config import Config 19 | 20 | c = Config({'HTMLExporter':{'template_path':['.', '/']}}) 21 | CSS_CODE = """.highlight .hll {background-color:#ffffcc}.highlight {background:#f8f8f8;}.highlight .c {color:#408080;font-style:italic}.highlight .err {border:1px solid #FF0000}.highlight .k {color:#008000;font-weight:bold}.highlight .o {color:#666666}.highlight .cm {color:#408080;font-style:italic}.highlight .cp {color:#BC7A00}.highlight .c1 {color:#408080;font-style:italic}.highlight .cs {color:#408080;font-style:italic}.highlight .gd {color:#A00000}.highlight .ge {font-style:italic}.highlight .gr {color:#FF0000}.highlight .gh {color:#000080;font-weight:bold}.highlight .gi {color:#00A000}.highlight .go {color:#888888}.highlight .gp {color:#000080;font-weight:bold}.highlight .gs {font-weight:bold}.highlight .gu {color:#800080;font-weight:bold}.highlight .gt {color:#0044DD}.highlight .kc {color:#008000;font-weight:bold}.highlight .kd {color:#008000;font-weight:bold}.highlight .kn {color:#008000;font-weight:bold}.highlight .kp {color:#008000}.highlight .kr {color:#008000;font-weight:bold}.highlight .kt {color:#B00040}.highlight .m {color:#666666}.highlight .s {color:#BA2121}.highlight .na {color:#7D9029}.highlight .nb {color:#008000}.highlight .nc {color:#0000FF;font-weight:bold}.highlight .no {color:#880000}.highlight .nd {color:#AA22FF}.highlight .ni {color:#999999;font-weight:bold}.highlight .ne {color:#D2413A;font-weight:bold}.highlight .nf {color:#0000FF}.highlight .nl {color:#A0A000}.highlight .nn {color:#0000FF;font-weight:bold}.highlight .nt {color:#008000;font-weight:bold}.highlight .nv {color:#19177C}.highlight .ow {color:#AA22FF;font-weight:bold}.highlight .w {color:#bbbbbb}.highlight .mb {color:#666666}.highlight .mf {color:#666666}.highlight .mh {color:#666666}.highlight .mi {color:#666666}.highlight .mo {color:#666666}.highlight .sb {color:#BA2121}.highlight .sc {color:#BA2121}.highlight .sd {color:#BA2121;font-style:italic}.highlight .s2 {color:#BA2121}.highlight .se {color:#BB6622;font-weight:bold}.highlight .sh {color:#BA2121}.highlight .si {color:#BB6688;font-weight:bold}.highlight .sx {color:#008000}.highlight .sr {color:#BB6688}.highlight .s1 {color:#BA2121}.highlight .ss {color:#19177C}.highlight .bp {color:#008000}.highlight .vc {color:#19177C}.highlight .vg {color:#19177C}.highlight .vi {color:#19177C}.highlight .il {color:#666666}""" 22 | 23 | def extract_upload_images(post, 24 | server, 25 | title, 26 | user, 27 | password): 28 | """Extract the images from a Jupyter notebook and upload them to 29 | the defined wordpress server. 30 | 31 | Params: 32 | ======= 33 | 34 | post : str 35 | The converted information from the notebook to HTML. 36 | server: obj 37 | A `xmlrpclib.ServerProxy` instance. 38 | title : str 39 | Title for the post 40 | 41 | Returns: 42 | ======== 43 | 44 | A string with the converted HTML once the images has been extracted 45 | and replaced with urls to the wordpress site. 46 | """ 47 | # Let's extract the images and upload to wp 48 | pat = re.compile('src="data:image/(.*?);base64,(.*?)"', re.DOTALL) 49 | count = 1 50 | postnew = post 51 | for (ext, data) in pat.findall(post): 52 | datab = a2b_base64(data) 53 | datab = xmlrpclib.Binary(datab) 54 | imgtitle = title.replace(' ','_').replace('.','-') 55 | out = {'name': imgtitle + str(count) + '.' + ext, 56 | 'type': 'image/' + ext, 57 | 'bits': datab, 58 | 'overwrite': 'true'} 59 | count += 1 60 | image_id = server.wp.uploadFile("", 61 | user, 62 | password, 63 | out) 64 | urlimg = image_id['url'] 65 | postnew = postnew.replace('data:image/' + ext + ';base64,' + data, 66 | urlimg) 67 | return postnew 68 | 69 | def create_draft(post, 70 | title, 71 | categories, 72 | tags, 73 | user, 74 | password): 75 | """It will create the draft post to the defined wordpress site. 76 | 77 | Params: 78 | ======= 79 | 80 | post: str 81 | A string with the converted Jupyter notebook. 82 | title: str 83 | The title for the post. 84 | categories: str 85 | The categories related with the post. They should already exist. 86 | tags: str 87 | The tags for the post. 88 | user: str 89 | The admin of the wordpress site. 90 | password: str 91 | The password for the admin of the wordpress site. 92 | """ 93 | date_created = xmlrpclib.DateTime(datetime.datetime.now()) 94 | status_published = 0 95 | wp_blogid = "" 96 | data = {'title': title, 97 | 'description': post, 98 | 'post_type': 'post', 99 | 'dateCreated': date_created, 100 | 'mt_allow_comments': 'open', 101 | 'mt_allow_pings': 'open', 102 | 'post_status': 'draft', 103 | 'categories': categories, 104 | 'mt_keywords': tags} 105 | post_id = server.metaWeblog.newPost(wp_blogid, 106 | user, 107 | password, 108 | data, 109 | status_published) 110 | print() 111 | print('It seems all worked fine!! Check your wordpress site admin.') 112 | print() 113 | 114 | if __name__ == '__main__': 115 | 116 | ######################## 117 | # command line options # 118 | ######################## 119 | parser = argparse.ArgumentParser(description=('Publish a Jupyter ' 120 | 'notebook as a draft ' 121 | 'post to a wordpress ' 122 | 'site.')) 123 | parser.add_argument('--xmlrpc-url', 124 | help="The XML-RPC server/path url") 125 | parser.add_argument('--user', 126 | help="The wordpress user") 127 | parser.add_argument('--password', 128 | help="The wordpress user password") 129 | parser.add_argument('--nb', 130 | help="The path and notebook filename") 131 | parser.add_argument('--title', 132 | help="The title for the post in the site") 133 | parser.add_argument('--categories', nargs='+', 134 | help="A list of categories separated by space") 135 | parser.add_argument('--tags', nargs='+', 136 | help="A list of tags separated by spaces") 137 | parser.add_argument('--template', 138 | help="The template to be used, if none then basic is used") 139 | args = parser.parse_args() 140 | 141 | err_msg = "You should provide a value for the option --{}" 142 | 143 | if args.xmlrpc_url: 144 | server = xmlrpclib.ServerProxy(args.xmlrpc_url) 145 | else: 146 | raise Exception(err_msg.format('xmlrpc-url')) 147 | 148 | if args.user: 149 | user = args.user 150 | else: 151 | raise Exception(err_msg.format('user')) 152 | 153 | if args.password: 154 | password = args.password 155 | else: 156 | raise Exception(err_msg.format('password')) 157 | 158 | if args.template: 159 | tpl = args.template 160 | if tpl in ['basicx']: 161 | pathtpl, _ = os.path.split(os.path.abspath(__file__)) 162 | pathtpl = os.path.join(pathtpl, 'templates', "{}.tpl".format(tpl)) 163 | post = """\n""".format(CSS_CODE) 164 | else: 165 | pathtpl = "basic" 166 | post = "" 167 | 168 | if args.nb: 169 | post += nbc.export_html(nb = args.nb, 170 | template_file = pathtpl, 171 | config = c)[0] 172 | else: 173 | raise Exception(err_msg.format('nb')) 174 | 175 | if args.title: 176 | title = args.title 177 | else: 178 | raise Exception(err_msg.format('title')) 179 | 180 | if args.categories: 181 | categories = args.categories 182 | else: 183 | categories = ['Uncategorized'] 184 | 185 | if args.tags: 186 | tags = args.tags 187 | else: 188 | tags = '' 189 | 190 | ################################################# 191 | # Publishing the post from the Jupyter notebook # 192 | ################################################# 193 | postnew = extract_upload_images(post, server, title, user, password) 194 | create_draft(postnew, 195 | title, 196 | categories, 197 | tags, 198 | user, 199 | password) 200 | -------------------------------------------------------------------------------- /jupy2wp/templates/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pybonacci/jupy2wp/d9dcc13fa89a3841fae9d6f2f354028a312a7eba/jupy2wp/templates/__init__.py -------------------------------------------------------------------------------- /jupy2wp/templates/basicx.tpl: -------------------------------------------------------------------------------- 1 | {%- extends 'display_priority.tpl' -%} 2 | 3 | 4 | {% block codecell %} 5 |
6 | {{ super() }} 7 |
8 | {%- endblock codecell %} 9 | 10 | {% block input_group -%} 11 |
12 | {{ super() }} 13 |
14 | {% endblock input_group %} 15 | 16 | {% block output_group %} 17 |
18 |
19 | {{ super() }} 20 |
21 |
22 | {% endblock output_group %} 23 | 24 | {% block in_prompt -%} 25 | {%- endblock in_prompt %} 26 | 27 | {% block empty_in_prompt -%} 28 |
29 |
30 | {%- endblock empty_in_prompt %} 31 | 32 | {# 33 | output_prompt doesn't do anything in HTML, 34 | because there is a prompt div in each output area (see output block) 35 | #} 36 | {% block output_prompt %} 37 | {% endblock output_prompt %} 38 | 39 | {% block input %} 40 |
41 |
42 | {{ cell.source | highlight_code(metadata=cell.metadata) }} 43 |
44 |
45 | {%- endblock input %} 46 | 47 | {% block output %} 48 |
49 | {%- if output.output_type == 'execute_result' -%} 50 |
51 | {%- else -%} 52 |
53 | {%- endif -%} 54 |
55 | {{ super() }} 56 |
57 | {% endblock output %} 58 | 59 | {% block markdowncell scoped %} 60 |
61 | {{ self.empty_in_prompt() }} 62 |
63 |
64 | {{ cell.source | markdown2html | strip_files_prefix }} 65 |
66 |
67 |
68 | {%- endblock markdowncell %} 69 | 70 | {% block unknowncell scoped %} 71 | unknown type {{ cell.type }} 72 | {% endblock unknowncell %} 73 | 74 | {% block execute_result -%} 75 | {%- set extra_class="output_execute_result" -%} 76 | {% block data_priority scoped %} 77 | {{ super() }} 78 | {% endblock %} 79 | {%- set extra_class="" -%} 80 | {%- endblock execute_result %} 81 | 82 | {% block stream_stdout -%} 83 |
84 |
 85 | {{- output.text | ansi2html -}}
 86 | 
87 |
88 | {%- endblock stream_stdout %} 89 | 90 | {% block stream_stderr -%} 91 |
92 |
 93 | {{- output.text | ansi2html -}}
 94 | 
95 |
96 | {%- endblock stream_stderr %} 97 | 98 | {% block data_svg scoped -%} 99 |
100 | {%- if output.svg_filename %} 101 | 106 | {%- endblock data_svg %} 107 | 108 | {% block data_html scoped -%} 109 |
110 | {{ output.data['text/html'] }} 111 |
112 | {%- endblock data_html %} 113 | 114 | {% block data_markdown scoped -%} 115 |
116 | {{ output.data['text/markdown'] | markdown2html }} 117 |
118 | {%- endblock data_markdown %} 119 | 120 | {% block data_png scoped %} 121 |
122 | {%- if 'image/png' in output.metadata.get('filenames', {}) %} 123 | 134 |
135 | {%- endblock data_png %} 136 | 137 | {% block data_jpg scoped %} 138 |
139 | {%- if 'image/jpeg' in output.metadata.get('filenames', {}) %} 140 | 151 |
152 | {%- endblock data_jpg %} 153 | 154 | {% block data_latex scoped %} 155 |
156 | {{ output.data['text/latex'] }} 157 |
158 | {%- endblock data_latex %} 159 | 160 | {% block error -%} 161 |
162 |
163 | {{- super() -}}
164 | 
165 |
166 | {%- endblock error %} 167 | 168 | {%- block traceback_line %} 169 | {{ line | ansi2html }} 170 | {%- endblock traceback_line %} 171 | 172 | {%- block data_text scoped %} 173 |
174 |
175 | {{- output.data['text/plain'] | ansi2html -}}
176 | 
177 |
178 | {%- endblock -%} 179 | 180 | {%- block data_javascript scoped %} 181 |
182 | 185 |
186 | {%- endblock -%} 187 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | try: 2 | from setuptools import setup, find_packages 3 | except ImportError: 4 | from distutils.core import setup 5 | import io 6 | from os import path 7 | 8 | here = path.abspath(path.dirname(__file__)) 9 | 10 | # Get the long description from the relevant file 11 | with io.open(path.join(here, 'README.md'), encoding = 'utf-8') as f: 12 | long_description = f.read() 13 | 14 | install_requires = [ 15 | 'nbconvert>=4.0.0', 16 | 'traitlets>=4.0.0' 17 | ] 18 | 19 | setup( 20 | name = 'jupy2wp', 21 | version = '1.0.0', 22 | description = 'Command line tool to create a draft post on a wordpress site from a Jupyter notebook.', 23 | long_description = long_description, 24 | url = 'https://github.com/pybonacci/jupy2wp', 25 | 26 | # Author details 27 | author = 'Kikocorreoso', 28 | author_email = '', 29 | 30 | # Choose your license 31 | license = 'MIT', 32 | 33 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 34 | classifiers = [ 35 | 'Topic :: Text Processing :: Markup :: HTML', 36 | 'License :: OSI Approved :: MIT License', 37 | 'Programming Language :: Python :: 2', 38 | 'Programming Language :: Python :: 2.7', 39 | 'Programming Language :: Python :: 3', 40 | 'Programming Language :: Python :: 3.3', 41 | 'Programming Language :: Python :: 3.4', 42 | 'Programming Language :: Python :: Implementation :: PyPy', 43 | ], 44 | 45 | # What does your project relate to? 46 | keywords='wordpress ipython jupyter notebook', 47 | packages = find_packages(), 48 | package_data = {'': ['*.tpl']}, 49 | install_requires = install_requires, 50 | ) 51 | --------------------------------------------------------------------------------